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(`(? [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 ``
}).join('')
return `data:image/svg+xml,${encodeURIComponent(
``
)}`
}
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}`)
}