diff --git a/skill/scripts/live/roots.mjs b/skill/scripts/live/roots.mjs index 192367df3..bdca69d75 100644 --- a/skill/scripts/live/roots.mjs +++ b/skill/scripts/live/roots.mjs @@ -393,14 +393,41 @@ export function resolveLiveRoots(cwd = process.cwd(), { targetPath = null } = {} return { manifest: fresh.manifest, source: 'fresh' }; } +/** + * Consume a `--target ` / `--target=` pair from an argv array, + * returning the value and removing the tokens so downstream flag parsers + * (which do not know the option) never see them. + */ +export function consumeTargetArg(argv = process.argv) { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--target' && typeof argv[i + 1] === 'string') { + const value = argv[i + 1]; + argv.splice(i, 2); + return value; + } + if (typeof arg === 'string' && arg.startsWith('--target=')) { + const value = arg.slice('--target='.length); + argv.splice(i, 1); + return value; + } + } + return null; +} + /** * Entry-point guard for live CLI scripts: resolve the governing roots and * make appRoot the process cwd so every downstream path derivation agrees - * with the boot. Returns the manifest. Never throws; on selection ambiguity - * it stays in the current directory (the boot flow handles prompting). + * with the boot. An explicit `--target ` on the helper's command line + * overrides pointer resolution, which is what disambiguates a repo with + * several live apps (the multi-app warning names this escape hatch, so it + * has to actually work on every helper). Returns the manifest. Never + * throws; on selection ambiguity it stays in the current directory (the + * boot flow handles prompting). */ export function enterLiveRoot(cwd = process.cwd()) { - const resolved = resolveLiveRoots(cwd); + const targetPath = consumeTargetArg(process.argv); + const resolved = resolveLiveRoots(cwd, targetPath ? { targetPath } : {}); if (!resolved.manifest) return null; const appRoot = resolved.manifest.appRoot; if (path.resolve(cwd) !== path.resolve(appRoot) && isDir(appRoot)) { diff --git a/tests/live-roots.test.mjs b/tests/live-roots.test.mjs index d82410676..4f81d4482 100644 --- a/tests/live-roots.test.mjs +++ b/tests/live-roots.test.mjs @@ -267,3 +267,38 @@ describe('review regressions: stopped-session recovery', () => { } }); }); + +describe('review regressions: helper --target', () => { + it('enterLiveRoot honors --target and strips it from argv', () => { + const repo = realpathSync(mkdtempSync(join(tmpdir(), 'impeccable-roots-target-'))); + try { + mkdirSync(join(repo, '.git'), { recursive: true }); + for (const name of ['appA', 'appB']) { + write(repo, `${name}/vite.config.js`, 'export default {};'); + } + const a = resolveRoots({ cwd: repo, targetPath: join(repo, 'appA/vite.config.js') }).manifest; + const b = resolveRoots({ cwd: repo, targetPath: join(repo, 'appB/vite.config.js') }).manifest; + writeRootsManifest(a); + writeRootsManifest(b); + // Both alive: pointer resolution alone is ambiguous (A? B?); --target + // must decide, and downstream flag parsing must not see the tokens. + write(repo, 'appA/.impeccable/live/server.json', JSON.stringify({ pid: process.pid, port: 1, token: 't' })); + write(repo, 'appB/.impeccable/live/server.json', JSON.stringify({ pid: process.pid, port: 2, token: 't' })); + + const res = spawnSync(process.execPath, [ + '-e', + `import(${JSON.stringify(ROOTS_MODULE)}).then((m) => { + process.argv.push('--target', ${JSON.stringify(join(repo, 'appB'))}); + m.enterLiveRoot(); + console.log(JSON.stringify({ cwd: process.cwd(), argvHasTarget: process.argv.includes('--target') })); + });`, + ], { cwd: repo, encoding: 'utf-8' }); + assert.equal(res.status, 0, res.stderr); + const out = JSON.parse(res.stdout.trim().split('\n').pop()); + assert.equal(realpathSync(out.cwd), join(repo, 'appB')); + assert.equal(out.argvHasTarget, false); + } finally { + rmSync(repo, { recursive: true, force: true }); + } + }); +});