// Stage the root CLI into vscode/cli, the way app/scripts/stage-cli.mjs stages // it for the desktop app: the bundle, its parse worker, the package.json it // reads its version from, the Electron launch shim, and the production // dependency closure copied out of the root node_modules. The extension spawns // cli/dist/launch.js with the editor's Node or a system Node. import { execFileSync, spawn } from 'node:child_process' import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { topLevelPackagesFromNpmLs } from '../../app/scripts/stage-cli-packages.mjs' const here = dirname(fileURLToPath(import.meta.url)) const root = join(here, '..', '..') const dist = join(root, 'dist') const rootModules = join(root, 'node_modules') const stage = join(here, '..', 'cli') for (const file of ['cli.js', 'main.js', 'parse-worker.js']) { if (!existsSync(join(dist, file))) throw new Error(`stage-cli: ${join(dist, file)} is missing; run npm run build:cli at the repo root first`) } rmSync(stage, { recursive: true, force: true }) mkdirSync(join(stage, 'dist'), { recursive: true }) copyFileSync(join(root, 'package.json'), join(stage, 'package.json')) for (const file of ['cli.js', 'main.js', 'parse-worker.js']) copyFileSync(join(dist, file), join(stage, 'dist', file)) // Same shim the desktop app generates: under Electron-as-Node, Commander reads // argv[1] as a user argument, so ordinary commands drop the script path; the // resident `serve` command is dispatched from argv[2] before Commander runs. writeFileSync(join(stage, 'dist', 'launch.js'), [ '// Generated by vscode/scripts/stage-cli.mjs; do not edit.', "if (process.versions.electron && process.argv[2] !== 'serve') process.argv.splice(1, 1)", "if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production'", "import('./main.js').catch(err => {", " process.stderr.write(String(err?.message ?? err) + '\\n')", ' process.exit(1)', '})', '', ].join('\n')) const npmCli = process.env.npm_execpath if (!npmCli) throw new Error('stage-cli: run through npm (npm run stage-cli), which sets npm_execpath') let listed = '' try { listed = execFileSync(process.execPath, [npmCli, 'ls', '--omit=dev', '--all', '--parseable'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }) } catch (err) { listed = err.stdout ? String(err.stdout) : '' } const topLevel = topLevelPackagesFromNpmLs(listed, rootModules) if (topLevel.size === 0) throw new Error('stage-cli: production dependency closure is empty; is the root npm installed?') 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, // Packages' own tests and TypeScript sources never run; matched below the package root only. filter: path => !/\.(map|md|ts|mts|cts)$/i.test(path) && !/[\\/](test|tests|__tests__)([\\/]|$)/i.test(path.slice(src.length)), }) } 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(', ')}`) // Smoke both launch paths with this Node against an empty home, so packaging // never reads the developer's own sessions. const launch = join(stage, 'dist', 'launch.js') const home = mkdtempSync(join(tmpdir(), 'codeburn-vscode-stage-')) const env = { PATH: process.env.PATH, HOME: home, USERPROFILE: home, APPDATA: join(home, 'AppData', 'Roaming'), LOCALAPPDATA: join(home, 'AppData', 'Local'), XDG_CACHE_HOME: join(home, '.cache'), XDG_CONFIG_HOME: join(home, '.config'), XDG_DATA_HOME: join(home, '.local', 'share'), CODEBURN_CACHE_DIR: join(home, '.codeburn-cache'), ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}), } try { const help = execFileSync(process.execPath, [launch, 'models', '--help'], { env, encoding: 'utf8', timeout: 30_000 }) if (!/^Usage: codeburn models\b/m.test(help)) throw new Error('stage-cli: the launch shim lost ordinary CLI arguments') await new Promise((resolve, reject) => { const child = spawn(process.execPath, [launch, 'serve', '--stdio'], { env, stdio: ['pipe', 'pipe', 'pipe'] }) let out = '' const timer = setTimeout(() => { child.kill(); reject(new Error('stage-cli: serve sent no ready frame within 10s')) }, 10_000) child.stdout.setEncoding('utf8') child.stdout.on('data', chunk => { out += chunk if (out.split('\n').some(line => { try { return JSON.parse(line).ready === true } catch { return false } })) { clearTimeout(timer) child.kill() resolve() } }) child.on('error', error => { clearTimeout(timer); reject(error) }) }) } finally { rmSync(home, { recursive: true, force: true }) } console.log(`stage-cli: staged ${topLevel.size} production packages -> ${stage}`)