fix(cli): disable non-interactive auto-install for required tools

This commit is contained in:
suraj-markup 2026-03-26 14:35:44 +05:30
parent c5cc0620e2
commit c19aaf6967
2 changed files with 74 additions and 4 deletions

View File

@ -606,6 +606,72 @@ describe("start command — URL argument", () => {
});
});
describe("start command — non-interactive install safety", () => {
function hasPrivilegedInstallAttempt(): boolean {
return mockExec.mock.calls.some((call) => {
const cmd = String(call[0]);
const args = Array.isArray(call[1]) ? (call[1] as string[]) : [];
const joined = `${cmd} ${args.join(" ")}`;
return joined.includes(" install ") && (cmd === "sudo" || cmd === "brew" || cmd === "winget");
});
}
it("does not auto-install tmux when missing in non-interactive mode", async () => {
const { isHumanCaller } = await import("../../src/lib/caller-context.js");
vi.mocked(isHumanCaller).mockReturnValue(false);
mockConfigRef.current = makeConfig({ "my-app": makeProject() });
mockExecSilent.mockImplementation(async (cmd: string, args: string[] = []) => {
if (cmd === "git" && args[0] === "--version") return "git version 2.43.0";
if (cmd === "tmux" && args[0] === "-V") return null;
if (cmd === "gh" && args[0] === "--version") return null;
if (cmd === "gh" && args[0] === "auth" && args[1] === "status") return null;
return null;
});
await expect(
program.parseAsync(["node", "test", "start", "--no-dashboard", "--no-orchestrator"]),
).rejects.toThrow("process.exit(1)");
expect(hasPrivilegedInstallAttempt()).toBe(false);
expect(mockExec.mock.calls.some((call) => String(call[0]) === "tmux")).toBe(false);
});
it("does not auto-install git when missing in non-interactive URL start", async () => {
const { isHumanCaller } = await import("../../src/lib/caller-context.js");
vi.mocked(isHumanCaller).mockReturnValue(false);
mockCwd(tmpDir);
mockExecSilent.mockImplementation(async (cmd: string, args: string[] = []) => {
if (cmd === "git" && args[0] === "--version") return null;
if (cmd === "tmux" && args[0] === "-V") return "tmux 3.4";
if (cmd === "gh" && args[0] === "--version") return null;
if (cmd === "gh" && args[0] === "auth" && args[1] === "status") return null;
return null;
});
await expect(
program.parseAsync([
"node",
"test",
"start",
"https://github.com/owner/nonexistent",
"--no-dashboard",
"--no-orchestrator",
]),
).rejects.toThrow("process.exit(1)");
expect(hasPrivilegedInstallAttempt()).toBe(false);
expect(
mockExec.mock.calls.some((call) => {
const cmd = String(call[0]);
const args = Array.isArray(call[1]) ? (call[1] as string[]) : [];
return cmd === "git" && args[0] === "clone";
}),
).toBe(false);
});
});
// ---------------------------------------------------------------------------
// waitForPortAndOpen — port polling logic
// ---------------------------------------------------------------------------

View File

@ -148,8 +148,12 @@ function canPromptForInstall(): boolean {
return isHumanCaller() && IS_TTY;
}
async function askYesNo(question: string, defaultYes = true): Promise<boolean> {
if (!canPromptForInstall()) return defaultYes;
async function askYesNo(
question: string,
defaultYes = true,
nonInteractiveDefault = defaultYes,
): Promise<boolean> {
if (!canPromptForInstall()) return nonInteractiveDefault;
const { createInterface } = await import("node:readline/promises");
const rl = createInterface({ input: process.stdin, output: process.stdout });
@ -238,7 +242,7 @@ async function ensureGit(context: string): Promise<void> {
if (hasGit) return;
console.log(chalk.yellow(`⚠ Git is required for ${context}.`));
const shouldInstall = await askYesNo("Install Git now?", true);
const shouldInstall = await askYesNo("Install Git now?", true, false);
if (shouldInstall) {
const installed = await tryInstallWithAttempts(
gitInstallAttempts(),
@ -748,7 +752,7 @@ async function ensureTmux(): Promise<void> {
if (hasTmux) return;
console.log(chalk.yellow("⚠ tmux is required for runtime \"tmux\"."));
const shouldInstall = await askYesNo("Install tmux now?", true);
const shouldInstall = await askYesNo("Install tmux now?", true, false);
if (shouldInstall) {
const installed = await tryInstallWithAttempts(
tmuxInstallAttempts(),