Files
ycode/scripts/rea/capture-css.mjs

75 lines
6.1 KiB
JavaScript

// Invoke the public repaired REA CLI. This wrapper has no target protocol access.
import fs from 'node:fs/promises'
import path from 'node:path'
import { spawn } from 'node:child_process'
import { fileURLToPath } from 'node:url'
const project = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
process.chdir(project)
const includeVisuals = process.argv.includes('--visuals')
const args = process.argv.slice(2).filter(arg => arg !== '--visuals')
function option(name) { const index=args.indexOf(name);if(index<0)return undefined;const value=args[index+1];if(!value||value.startsWith('--'))throw new Error(`Missing ${name} value`);args.splice(index,2);return value }
const probeFile=option('--probe'),outputDirectory=option('--output'),selectedTargetId=option('--target-id')
const probe=probeFile?JSON.parse(await fs.readFile(probeFile,'utf8')):undefined
if(probe&&!includeVisuals)throw new Error('--probe requires --visuals')
const endpoint = args[0] || 'http://127.0.0.1:9223'
const expectedPage = args[1] || 'C:/Users/Administrator/AppData/Local/Programs/WorkBuddy/resources/app.asar/renderer/index.html'
const launcher = path.resolve('.tmp/rea-css-runtime/scripts/rea.mjs')
await fs.access(launcher).catch(() => { throw new Error('Run npm run rea:css:prepare first') })
async function run(args) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [launcher, ...args, '--json'], { cwd: project, stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true })
const chunks = []
child.stdout.on('data', chunk => chunks.push(chunk))
child.once('error', reject)
child.once('close', async code => {
if (code === 0) return resolve(Buffer.concat(chunks))
await fs.writeFile('.tmp/rea-capture-failure.json', Buffer.concat(chunks))
reject(new Error(`REA exited with ${code}; diagnostic saved to .tmp/rea-capture-failure.json`))
})
})
}
const normalize = value => value.replace(/\\/g, '/').toLowerCase()
const discoveryBytes = await run(['list-electron-targets', endpoint])
const discovery = JSON.parse(discoveryBytes.toString('utf8'))
if (discovery.operation !== 'list_electron_targets') throw new Error('REA did not return target discovery Evidence')
const matches = discovery.normalized_result.targets.filter(target => normalize(target.file_path) === normalize(expectedPage) && (!selectedTargetId || target.target_id === selectedTargetId))
if (matches.length !== 1) throw new Error(`Expected exactly one authorized WorkBuddy page; found ${matches.length}`)
const bytes = await run(['inspect-electron-page', endpoint, matches[0].target_id, '--include-style-sources', ...(includeVisuals ? ['--include-visuals'] : []), ...(probe?['--visual-probe',JSON.stringify(probe)]:[]), '--observation-ms', '250'])
const evidence = JSON.parse(bytes.toString('utf8'))
if (evidence.operation !== 'inspect_electron_page' || evidence.parameters?.include_style_sources !== true || !evidence.normalized_result?.styles) throw new Error('REA did not return CSS Evidence')
await fs.writeFile('.tmp/rea-css-targets.json', discoveryBytes)
await fs.writeFile('.tmp/rea-workbuddy-css-evidence.json', bytes)
await fs.mkdir('.tmp/rea-css-evidence', { recursive: true })
if (!/^ev_[a-f0-9]{64}$/.test(evidence.evidence_id) || !/^ev_[a-f0-9]{64}$/.test(discovery.evidence_id)) throw new Error('Invalid REA Evidence identity')
await fs.writeFile(path.join('.tmp/rea-css-evidence', `${discovery.evidence_id}.json`), discoveryBytes)
const archive = path.join('.tmp/rea-css-evidence', `${evidence.evidence_id}.json`)
await fs.writeFile(archive, bytes)
if (includeVisuals) {
const visuals = evidence.normalized_result.visuals
if (!visuals) throw new Error('REA did not retain visual capture')
const visualDirectory=outputDirectory?path.resolve(outputDirectory):path.resolve('.tmp/rea-visual-reference')
await fs.mkdir(visualDirectory, { recursive: true })
const { createHash } = await import('node:crypto')
const image = Buffer.from(visuals.artifact.data_base64, 'base64')
if (createHash('sha256').update(image).digest('hex') !== visuals.artifact.sha256 || image.length !== visuals.artifact.bytes) throw new Error('REA screenshot digest mismatch')
await fs.writeFile(path.join(visualDirectory,'workbuddy.png'), image)
await fs.writeFile(path.join(visualDirectory,'layout.json'), JSON.stringify({ evidenceId: evidence.evidence_id, archive: path.resolve(archive), ...visuals, artifact: { ...visuals.artifact, data_base64: undefined },probe:visuals.probe?{...visuals.probe,frames:visuals.probe.frames.map(f=>({...f,artifact:{...f.artifact,data_base64:undefined}}))}:undefined }, null, 2))
for(let index=0;index<(visuals.probe?.frames.length??0);index++){
const frame=visuals.probe.frames[index],png=Buffer.from(frame.artifact.data_base64,'base64');
if(createHash('sha256').update(png).digest('hex')!==frame.artifact.sha256)throw new Error('REA scenario frame digest mismatch');
await fs.writeFile(path.join(visualDirectory,`frame-${String(index).padStart(3,'0')}.png`),png)
}
if(visuals.font_samples){
const os=await import('node:os');
const report={evidenceId:evidence.evidence_id,archive:path.resolve(archive),evidenceSha256:createHash('sha256').update(bytes).digest('hex'),version:evidence.normalized_result.browser.user_agent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1]??'unobserved',environment:{platform:os.platform(),osRelease:os.release(),browser:evidence.normalized_result.browser},source:{kind:'rea-workbuddy'},samples:visuals.font_samples,missing:visuals.font_missing??[]};
await fs.writeFile(path.join(visualDirectory,'font-platform-report.json'),JSON.stringify(report,null,2))
await fs.writeFile('.tmp/rea-font-platform-report.json',JSON.stringify(report,null,2))
}
}
const styles = evidence.normalized_result.styles
console.log(JSON.stringify({ evidenceId: evidence.evidence_id, targetId: matches[0].target_id, total: styles.total,
included: styles.items.filter(item => item.source.included).length, excluded: styles.excluded,
sourceFailures: styles.source_failures, truncated: styles.truncated,
output: path.resolve('.tmp/rea-workbuddy-css-evidence.json'), archive: path.resolve(archive) }, null, 2))