fix: helpers honor --target for multi-app disambiguation

greptile-apps[bot] repro: the multi-app warning recommended --target,
but the helper CLIs never parsed it, so live-poll --target appB still
re-anchored onto the pointer's first choice. enterLiveRoot now consumes
a --target argument (removing it from argv so downstream flag parsers
never see it) and resolves roots against it, making the documented
escape hatch real on every helper. Regression test drives a two-live-app
repo through a child process and asserts both the chdir target and the
argv scrubbing.

This work was produced with AI assistance (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-27 16:52:01 -07:00
co-authored by Claude Code
parent f1d450e6ab
commit baed04a52b
2 changed files with 65 additions and 3 deletions
+30 -3
View File
@@ -393,14 +393,41 @@ export function resolveLiveRoots(cwd = process.cwd(), { targetPath = null } = {}
return { manifest: fresh.manifest, source: 'fresh' };
}
/**
* Consume a `--target <path>` / `--target=<path>` 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 <path>` 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)) {
+35
View File
@@ -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 });
}
});
});