Skip to content

Commit 722dd04

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 722dd04

4 files changed

Lines changed: 113 additions & 2 deletions

File tree

docs/quickstart.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ Deep Code 会在当前目录中启动交互式界面。你可以直接输入任
7474
deepcode -p "总结这个项目"
7575
```
7676

77+
### 非交互模式(Headless)
78+
79+
如果你需要在脚本、CI/CD 流水线或 Docker 容器中自动执行任务,可以使用 `--headless` 参数。该模式下 deepcode 不会启动交互界面,执行完 prompt 后自动退出并将结果输出到 stdout:
80+
81+
```bash
82+
deepcode --headless -p "总结这个项目"
83+
84+
# 可与管道组合
85+
deepcode --headless -p "列出所有 API 接口" | grep "auth"
86+
```
87+
88+
`--headless` 必须与 `-p` 搭配使用,且在该模式下所有权限操作均自动批准。
89+
7790
## 第一次可以这样问
7891

7992
可以先从只读任务开始:

docs/quickstart_en.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ To start with an initial prompt:
7474
deepcode -p "Summarize this project"
7575
```
7676

77+
### Non-Interactive Mode (Headless)
78+
79+
For scripts, CI/CD pipelines, or Docker containers where you need automatic execution, use the `--headless` flag. In this mode, deepcode skips the interactive TUI, runs the prompt, prints the result to stdout, and exits:
80+
81+
```bash
82+
deepcode --headless -p "Summarize this project"
83+
84+
# Pipe with other tools
85+
deepcode --headless -p "List all API endpoints" | grep "auth"
86+
```
87+
88+
`--headless` requires `-p`, and all permission operations are auto-approved in this mode.
89+
7790
## Try These First
7891

7992
Start with a read-only task:

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)