Skip to content

Commit 686ec08

Browse files
committed
feat(cli): add --headless flag for non-interactive run-and-exit mode
Add --headless option that allows deepcode to run a prompt and exit automatically without entering the interactive TUI. This enables: - CI/CD pipeline integration - Shell scripting / automation - Batch processing Changes: - cli-args.ts: add --headless boolean flag and validation (requires --prompt) - cli.tsx: add runHeadless() function that creates SessionManager, submits prompt, streams output to stdout, and exits cleanly. In headless mode, permissions are forced to allowAll since there is no user to ask for approval. Closes #139
1 parent 47d1f03 commit 686ec08

2 files changed

Lines changed: 87 additions & 2 deletions

File tree

packages/cli/src/cli-args.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface ParsedCliArgs {
3333
version: boolean;
3434
/** True when --help / -h was passed */
3535
help: boolean;
36+
/** True when --headless was passed (non-interactive, run-and-exit mode) */
37+
headless: boolean;
3638
}
3739

3840
const EPILOG = [
@@ -73,7 +75,7 @@ async function configureYargs(argv?: string[]) {
7375
.locale("en")
7476
.scriptName("deepcode")
7577
.usage(
76-
"Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode"
78+
"Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt with --headless for non-interactive mode"
7779
)
7880
.command("$0 [query..]", "Launch Deep Code CLI", (yargsInstance: Argv) =>
7981
yargsInstance
@@ -82,6 +84,10 @@ async function configureYargs(argv?: string[]) {
8284
type: "string",
8385
describe: "Submit a prompt on launch",
8486
})
87+
.option("headless", {
88+
type: "boolean",
89+
describe: "Run in non-interactive (headless) mode. Use with --prompt to execute and exit automatically.",
90+
})
8591
.option("resume", {
8692
alias: "r",
8793
type: "string",
@@ -106,6 +112,10 @@ async function configureYargs(argv?: string[]) {
106112
if (argv["prompt"] === "") {
107113
return "--prompt / -p requires a non-empty value.";
108114
}
115+
// headless mode requires --prompt
116+
if (argv["headless"] && !argv["prompt"]) {
117+
return "--headless mode requires --prompt / -p with a non-empty value.";
118+
}
109119
return true;
110120
})
111121
)
@@ -156,5 +166,6 @@ export async function parseArguments(argv?: string[]): Promise<ParsedCliArgs> {
156166
resume,
157167
version: parsed.version === true,
158168
help: parsed.help === true,
169+
headless: parsed.headless === true,
159170
};
160171
}

packages/cli/src/cli.tsx

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { render } from "ink";
33
import { readFileSync } from "node:fs";
44
import { join } from "node:path";
55
import { homedir } from "node:os";
6-
import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core";
6+
import {
7+
setShellIfWindows,
8+
getProjectCode,
9+
SessionManager,
10+
createOpenAIClient,
11+
resolveCurrentSettings,
12+
} from "@vegamo/deepcode-core";
13+
import type { SessionMessage, LlmStreamProgress } from "@vegamo/deepcode-core";
714
import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check";
815
import { AppContainer } from "./ui";
916
import { parseArguments } from "./cli-args";
@@ -32,6 +39,11 @@ async function main(): Promise<void> {
3239
let resumeSessionId = parsed.resume;
3340
const projectRoot = process.cwd();
3441

42+
if (parsed.headless && parsed.prompt) {
43+
await runHeadless(parsed.prompt, projectRoot);
44+
return;
45+
}
46+
3547
if (!process.stdin.isTTY) {
3648
writeStderrLine("deepcode requires an interactive terminal (TTY). Re-run from a real terminal session.\n");
3749
process.exit(1);
@@ -115,3 +127,65 @@ function configureWindowsShell(): void {
115127
process.exit(1);
116128
}
117129
}
130+
131+
/**
132+
* Run in headless (non-interactive) mode.
133+
* Creates a SessionManager, submits the prompt, outputs the final response, and exits.
134+
*/
135+
async function runHeadless(promptText: string, projectRoot: string): Promise<void> {
136+
process.stderr.write("[headless] Starting non-interactive mode...\n");
137+
138+
const assistantMessages: SessionMessage[] = [];
139+
140+
const sessionManager = new SessionManager({
141+
projectRoot,
142+
createOpenAIClient: () => createOpenAIClient(projectRoot),
143+
getResolvedSettings: () => {
144+
const settings = resolveCurrentSettings(projectRoot);
145+
// Headless = no user to ask → auto-approve all permissions
146+
return {
147+
...settings,
148+
permissions: {
149+
allow: [],
150+
deny: [],
151+
ask: [],
152+
defaultMode: "allowAll" as const,
153+
},
154+
};
155+
},
156+
renderMarkdown: (text: string) => text,
157+
onAssistantMessage: (message: SessionMessage) => {
158+
assistantMessages.push(message);
159+
// Stream content to stdout as it arrives
160+
if (message.content) {
161+
process.stdout.write(message.content as string);
162+
}
163+
},
164+
onLlmStreamProgress: (progress: LlmStreamProgress) => {
165+
if (progress.phase === "start") {
166+
process.stderr.write("[headless] Model is thinking...\n");
167+
} else if (progress.phase === "end") {
168+
process.stderr.write("[headless] Response complete.\n");
169+
}
170+
},
171+
});
172+
173+
try {
174+
// Initialize MCP servers from settings
175+
const settings = resolveCurrentSettings(projectRoot);
176+
await sessionManager.initMcpServers(settings.mcpServers);
177+
178+
// Submit the prompt
179+
await sessionManager.handleUserPrompt({ text: promptText });
180+
181+
process.stdout.write("\n");
182+
} catch (error) {
183+
const message = error instanceof Error ? error.message : String(error);
184+
process.stderr.write(`[headless] Error: ${message}\n`);
185+
process.exit(1);
186+
} finally {
187+
sessionManager.dispose();
188+
}
189+
190+
process.exit(0);
191+
}

0 commit comments

Comments
 (0)