mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-30 19:36:01 +00:00
The packaged app resolved whatever codeburn was on PATH — for real users the published npm release, which predates every JSON surface the app calls. The app now ships its own CLI copy under resources/cli (staged production node_modules tree; tsup output is not self-contained) and spawns it with Electron's own binary via ELECTRON_RUN_AS_NODE. No install prerequisite remains. - resolution order: CODEBURN_BIN, dev repo CLI, bundled, persisted path, PATH search - launch.js shim strips the extra argv element commander mis-slices under packaged Electron-as-node (process.versions.electron is set), which reproduced the 'too many arguments' class of error - afterPack hook copies the staged tree (extraResources runs node_modules through the production-dep filter and ships it empty); lands before signing, signature verified intact - packaging always restages from src via root build:cli (tsup only, no network); ~12MB compressed per artifact Verified on the built app: Electron-as-node CLI emits current JSON (providerDetails, currency), and a minimal-PATH GUI launch spawns the bundled CLI with zero external dependencies. 290/290.
114 lines
5 KiB
JavaScript
114 lines
5 KiB
JavaScript
// Stage a self-contained copy of the root codeburn CLI into app/build/cli so
|
|
// electron-builder can ship it as `extraResources`. The packaged app spawns
|
|
// this copy (via Electron-as-node) instead of a globally-installed `codeburn`,
|
|
// guaranteeing the CLI matches the app's version with no install prerequisite.
|
|
//
|
|
// The tsup bundle (dist/main.js) is NOT self-contained: it keeps the runtime
|
|
// dependencies external (chalk, react, ink, undici, zod, @modelcontextprotocol/
|
|
// sdk, ...), and dist/main.js reads `../package.json` for its version string. So
|
|
// the staged layout mirrors what `npm install` produces:
|
|
//
|
|
// build/cli/package.json (root package.json: {version}, type:module)
|
|
// build/cli/dist/cli.js (Node-version-guard launcher → ./main.js)
|
|
// build/cli/dist/main.js (the bundle)
|
|
// build/cli/node_modules/ (production dependency closure)
|
|
//
|
|
// The production closure is copied out of the already-installed root
|
|
// node_modules (offline, no reinstall). Run the root CLI build first so dist/ is
|
|
// fresh — the app `stage-cli` script does exactly that.
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
import { cpSync, copyFileSync, existsSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url)) // app/scripts
|
|
const appDir = join(here, '..') // app
|
|
const root = join(appDir, '..') // repo root
|
|
const dist = join(root, 'dist')
|
|
const rootModules = join(root, 'node_modules')
|
|
const stage = join(appDir, 'build', 'cli')
|
|
|
|
for (const f of ['cli.js', 'main.js']) {
|
|
if (!existsSync(join(dist, f))) {
|
|
throw new Error(`stage-cli: ${join(dist, f)} is missing — build the root CLI first`)
|
|
}
|
|
}
|
|
|
|
rmSync(stage, { recursive: true, force: true })
|
|
mkdirSync(join(stage, 'dist'), { recursive: true })
|
|
|
|
copyFileSync(join(root, 'package.json'), join(stage, 'package.json'))
|
|
copyFileSync(join(dist, 'cli.js'), join(stage, 'dist', 'cli.js'))
|
|
copyFileSync(join(dist, 'main.js'), join(stage, 'dist', 'main.js'))
|
|
|
|
// Desktop-app launch shim (the app spawns this, not cli.js). The packaged app
|
|
// runs the CLI with Electron's own binary as Node (ELECTRON_RUN_AS_NODE=1).
|
|
// Because process.versions.electron is then set, commander's argv auto-detection
|
|
// treats a packaged (non-defaultApp) Electron as `from: 'electron'` and slices
|
|
// argv by 1 instead of 2 — leaving this script's own path among the parsed args
|
|
// and shifting every real argument. Drop that element before handing off so the
|
|
// CLI's `program.parse()` sees the true args. Guarded so the shim also works if
|
|
// ever run under plain Node. Kept in the app (not the shared CLI source) so the
|
|
// published CLI is untouched.
|
|
writeFileSync(
|
|
join(stage, 'dist', 'launch.js'),
|
|
[
|
|
'// Generated by app/scripts/stage-cli.mjs — do not edit.',
|
|
'if (process.versions.electron) process.argv.splice(1, 1)',
|
|
"import('./main.js').catch(err => {",
|
|
' process.stderr.write(String(err?.message ?? err) + \'\\n\')',
|
|
' process.exit(1)',
|
|
'})',
|
|
'',
|
|
].join('\n'),
|
|
)
|
|
|
|
// `npm ls` prints the tree to stdout even when it exits non-zero on peer/
|
|
// extraneous warnings, so capture stdout regardless of exit code.
|
|
let listed = ''
|
|
try {
|
|
listed = execFileSync('npm', ['ls', '--omit=dev', '--all', '--parseable'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
})
|
|
} catch (err) {
|
|
listed = err.stdout ? String(err.stdout) : ''
|
|
}
|
|
|
|
// Every parseable line is an absolute path to a production package instance.
|
|
// Map each back to its top-level node_modules entry (`name` or `@scope/name`),
|
|
// then copy those dirs whole — a package's own nested node_modules comes with
|
|
// it, which is exactly the closure it needs at runtime.
|
|
const prefix = rootModules + '/'
|
|
const topLevel = new Set()
|
|
for (const line of listed.split('\n')) {
|
|
if (!line.startsWith(prefix)) continue
|
|
const rest = line.slice(prefix.length)
|
|
const match = rest.match(/^(@[^/]+\/[^/]+|[^/]+)/)
|
|
if (match) topLevel.add(match[1])
|
|
}
|
|
if (topLevel.size === 0) {
|
|
throw new Error('stage-cli: production dependency closure is empty — is the root `npm install`ed?')
|
|
}
|
|
|
|
mkdirSync(join(stage, 'node_modules'), { recursive: true })
|
|
for (const pkg of topLevel) {
|
|
const src = join(rootModules, pkg)
|
|
if (!existsSync(src) || !statSync(src).isDirectory()) continue
|
|
cpSync(src, join(stage, 'node_modules', pkg), { recursive: true, dereference: true })
|
|
}
|
|
|
|
// A partially-copied CLI that crashes on import is worse than none: fail the
|
|
// build if any of the bundle's external runtime deps did not land.
|
|
const required = [
|
|
'chalk', 'react', 'ink', 'strip-ansi', 'zod', 'undici',
|
|
'selfsigned', 'commander', 'bonjour-service', '@modelcontextprotocol/sdk',
|
|
]
|
|
const missing = required.filter(pkg => !existsSync(join(stage, 'node_modules', pkg)))
|
|
if (missing.length) {
|
|
throw new Error(`stage-cli: staged bundle is missing runtime deps: ${missing.join(', ')}`)
|
|
}
|
|
|
|
console.log(`stage-cli: staged ${topLevel.size} production packages -> ${stage}`)
|