Files
orca/config/scripts/verify-cli-require-resolution.mjs
T
Jinjing e9063f106d Move agent-catalog schema migration to shared for CLI
Why: CLI's offline settings writes need AGENT_CATALOG_SCHEMA_VERSION but
cannot import from out/main (electron-vite removes it during rebuild).
Extract pure schema logic to src/shared, FS-bound backup operations to
src/main. Add CLI require-resolution verification to catch MODULE_NOT_FOUND
at build time instead of runtime. Fix hydration-time purge to defer
incomplete repos: was deleting all local owners on remote-only hydration
(incident 2026-07-19).
2026-09-01 03:53:17 -07:00

109 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
// Why: build:electron-vite cleans out/main and re-emits only its rollup input
// entries, so a CLI import of a main module that lacks a matching entry ships
// as a MODULE_NOT_FOUND crash (1.4.150-rc.1.perf shipped a dead CLI this way).
// This walks the compiled CLI's relative require() graph against the final
// out/ tree so the break fails the build instead of the user's first command.
const RELATIVE_REQUIRE_PATTERN = /\brequire\(\s*(["'])(\.{1,2}\/[^"']+)\1\s*\)/g
function listJsFilesRecursively(dir) {
const files = []
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const entryPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...listJsFilesRecursively(entryPath))
} else if (entry.isFile() && entry.name.endsWith('.js')) {
files.push(entryPath)
}
}
return files
}
/** Node CJS resolution for a relative specifier, limited to on-disk shapes tsc
* and rollup emit: exact file, `.js`/`.json` extension, or directory index. */
function resolveRelativeRequire(fromFile, specifier) {
const base = path.resolve(path.dirname(fromFile), specifier)
const candidates = [base, `${base}.js`, `${base}.json`, path.join(base, 'index.js')]
for (const candidate of candidates) {
if (existsSync(candidate) && statSync(candidate).isFile()) {
return candidate
}
}
return null
}
/**
* Walks every relative require() reachable from out/cli and asserts each target
* resolves inside the final out/ tree. Returns the list of missing edges.
*/
export function verifyCliRequireResolution({
projectDir = path.resolve(import.meta.dirname, '..', '..')
} = {}) {
const outDir = path.join(projectDir, 'out')
const cliDir = path.join(outDir, 'cli')
if (!existsSync(cliDir)) {
return { checkedFiles: 0, missing: [], skipped: true }
}
const queue = listJsFilesRecursively(cliDir)
const visited = new Set(queue)
const missing = []
while (queue.length > 0) {
const file = queue.pop()
const source = readFileSync(file, 'utf8')
for (const match of source.matchAll(RELATIVE_REQUIRE_PATTERN)) {
const specifier = match[2]
const resolved = resolveRelativeRequire(file, specifier)
if (resolved === null) {
missing.push({ from: path.relative(projectDir, file), specifier })
continue
}
// Why: transitive edges matter — out/cli requires out/shared modules that
// may themselves require paths electron-vite's clean step removed.
if (resolved.endsWith('.js') && !visited.has(resolved)) {
visited.add(resolved)
queue.push(resolved)
}
}
}
return { checkedFiles: visited.size, missing, skipped: false }
}
function main() {
const result = verifyCliRequireResolution()
if (result.skipped) {
console.log('[cli-requires] out/cli not present; skipping (run build:cli first)')
return
}
if (result.missing.length > 0) {
console.error(
`[cli-requires] ${result.missing.length} relative require(s) in the compiled CLI graph do not resolve in out/:`
)
for (const edge of result.missing) {
console.error(` ${edge.from} -> require('${edge.specifier}')`)
}
console.error(
'[cli-requires] A CLI import of an out/main module needs a matching rollup input entry in electron.vite.config.ts, or the module belongs in src/shared.'
)
process.exit(1)
}
console.log(
`[cli-requires] verified ${result.checkedFiles} compiled module(s), all requires resolve`
)
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
main()
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
}