feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,176 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const routeEvidencePath = resolve('.tmp/rea-todo20-scheduled-create.json')
|
||||
const outputPath = resolve('src/renderer/src/automations/automation-source-assets.ts')
|
||||
const checkOnly = process.argv.includes('--check')
|
||||
|
||||
const expected = {
|
||||
automation: {
|
||||
sha256: '687f7b062b0df0f353dafd1f17b00cb939685da5a12c8d3ce865b920b94d6710',
|
||||
bytes: 131_537
|
||||
},
|
||||
src: {
|
||||
sha256: '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0',
|
||||
bytes: 1_062_959
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`automation UI recovery: ${message}`)
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return createHash('sha256').update(value).digest('hex')
|
||||
}
|
||||
|
||||
function assertArtifact(label, artifact, sourceText) {
|
||||
const expectedArtifact = expected[label]
|
||||
if (!artifact || !sourceText) fail(`${label} source artifact is missing`)
|
||||
if (artifact.bytes !== expectedArtifact.bytes) {
|
||||
fail(`${label} byte count changed: ${artifact.bytes} != ${expectedArtifact.bytes}`)
|
||||
}
|
||||
if (artifact.sha256 !== expectedArtifact.sha256 || sha256(sourceText) !== expectedArtifact.sha256) {
|
||||
fail(`${label} SHA-256 does not match the pinned REA evidence`)
|
||||
}
|
||||
}
|
||||
|
||||
function findScript(evidence, suffix) {
|
||||
return evidence.normalized_result?.scripts?.items?.find((item) =>
|
||||
item.file_path?.endsWith(suffix)
|
||||
)
|
||||
}
|
||||
|
||||
function extractDataUri(source, variable) {
|
||||
const escaped = variable.replaceAll('$', '\\$')
|
||||
const match = new RegExp(`(?<![\\w$])${escaped}\\s*=\\s*\\x60(data:image/svg\\+xml,[^\\x60]+)\\x60`).exec(source)
|
||||
if (!match) fail(`could not recover ${variable} SVG data URI`)
|
||||
return match[1]
|
||||
}
|
||||
|
||||
function parseJsxPath(attributesSource) {
|
||||
const attributes = Object.fromEntries(
|
||||
[...attributesSource.matchAll(/([A-Za-z][\w-]*)\s*:\s*`([^`]*)`/g)]
|
||||
.map((match) => [match[1], match[2]])
|
||||
)
|
||||
if (!attributes.d) fail('a recovered path is missing its d attribute')
|
||||
return attributes
|
||||
}
|
||||
|
||||
function attributeName(name) {
|
||||
return name
|
||||
.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
||||
}
|
||||
|
||||
function escapeAttribute(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
}
|
||||
|
||||
function pathsToSvg(paths, width, height, viewBox) {
|
||||
const markup = paths.map((attributes) => {
|
||||
const serialized = Object.entries(attributes)
|
||||
.filter(([name]) => name !== 'd')
|
||||
.map(([name, value]) => `${attributeName(name)}="${escapeAttribute(value)}"`)
|
||||
.join(' ')
|
||||
return `<path ${serialized} d="${escapeAttribute(attributes.d)}"/>`
|
||||
}).join('')
|
||||
return `data:image/svg+xml,${encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox}" fill="black">${markup}</svg>`
|
||||
)}`
|
||||
}
|
||||
|
||||
function extractPaths(source, startMarker, endMarker, jsxFactory) {
|
||||
const start = source.indexOf(startMarker)
|
||||
const end = source.indexOf(endMarker, start + startMarker.length)
|
||||
if (start < 0 || end < 0) fail(`could not locate ${startMarker}`)
|
||||
const block = source.slice(start, end)
|
||||
const pattern = new RegExp(`\\(0,${jsxFactory}\\.jsx\\)\\(\\x60path\\x60,\\s*\\{([^}]*)\\}\\)`, 'g')
|
||||
const paths = [...block.matchAll(pattern)].map((match) => parseJsxPath(match[1]))
|
||||
if (!paths.length) fail(`no SVG paths recovered from ${startMarker}`)
|
||||
return paths
|
||||
}
|
||||
|
||||
const evidence = JSON.parse(await readFile(routeEvidencePath, 'utf8'))
|
||||
const srcItem = findScript(evidence, 'src-DPDj7_qD.js')
|
||||
const automationItem = findScript(evidence, 'automation-qDy4JNlv.js')
|
||||
|
||||
assertArtifact('src', srcItem?.source?.artifact, srcItem?.source?.artifact?.text)
|
||||
assertArtifact(
|
||||
'automation',
|
||||
automationItem?.source?.artifact,
|
||||
automationItem?.source?.artifact?.text
|
||||
)
|
||||
|
||||
const src = srcItem.source.artifact.text
|
||||
const automation = automationItem.source.artifact.text
|
||||
|
||||
const templateIcons = {
|
||||
news: extractDataUri(src, 'xb'),
|
||||
languages: extractDataUri(src, 'Yy'),
|
||||
moon: extractDataUri(src, '_b'),
|
||||
'weekly-report': extractDataUri(src, 'Tb'),
|
||||
film: extractDataUri(src, 'Ly'),
|
||||
calendar: extractDataUri(src, 'Ny'),
|
||||
lightbulb: extractDataUri(src, '$y'),
|
||||
image: extractDataUri(src, 'Gy'),
|
||||
hospital: extractDataUri(src, 'Vy'),
|
||||
'messages-square': extractDataUri(src, 'sb'),
|
||||
'list-todo': extractDataUri(src, 'rb')
|
||||
}
|
||||
|
||||
const clockPaths = extractPaths(
|
||||
src,
|
||||
'Gc=(0,Uc.forwardRef)',
|
||||
'Gc.displayName',
|
||||
'Wc'
|
||||
)
|
||||
templateIcons['alarm-clock'] = pathsToSvg(clockPaths, 16, 16, '0 0 16 16')
|
||||
|
||||
const emptyIllustrationPaths = extractPaths(
|
||||
automation,
|
||||
'function TaskEmptyIllustration',
|
||||
'var Ta,Ea',
|
||||
'Ta'
|
||||
)
|
||||
const emptyIllustration = pathsToSvg(
|
||||
emptyIllustrationPaths,
|
||||
64,
|
||||
64,
|
||||
'0 0 64 64'
|
||||
)
|
||||
|
||||
const output = `/**
|
||||
* Generated by scripts/rea/recover-automation-ui.mjs.
|
||||
*
|
||||
* Source: fixed WorkBuddy 5.8.3 REA evidence
|
||||
* ${evidence.evidence_id}
|
||||
*
|
||||
* Do not edit by hand. Re-run the recovery script to verify or refresh the assets.
|
||||
*/
|
||||
export const AUTOMATION_EVIDENCE = {
|
||||
id: ${JSON.stringify(evidence.evidence_id)},
|
||||
automationSha256: ${JSON.stringify(expected.automation.sha256)},
|
||||
srcSha256: ${JSON.stringify(expected.src.sha256)}
|
||||
} as const
|
||||
|
||||
export const AUTOMATION_TEMPLATE_ICON_URLS = ${JSON.stringify(templateIcons, null, 2)} as const
|
||||
|
||||
export type AutomationTemplateIconKey = keyof typeof AUTOMATION_TEMPLATE_ICON_URLS
|
||||
|
||||
export const AUTOMATION_EMPTY_ILLUSTRATION_URL = ${JSON.stringify(emptyIllustration)}
|
||||
`
|
||||
|
||||
if (checkOnly) {
|
||||
const current = await readFile(outputPath, 'utf8').catch(() => '')
|
||||
if (current !== output) fail('generated asset file is stale')
|
||||
console.log(`PASS automation UI assets ${expected.automation.sha256}`)
|
||||
} else {
|
||||
await mkdir(dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, output, 'utf8')
|
||||
console.log(`Wrote ${outputPath}`)
|
||||
}
|
||||
Reference in new issue
Block a user