Skip to content

Commit cdf2d23

Browse files
committed
Merge branch 'implement-cli-doctor'
2 parents 55e67b0 + 96ee619 commit cdf2d23

16 files changed

Lines changed: 2195 additions & 315 deletions

‎docs/configuration.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,12 @@ PLUS 独立配置文件为 `~/.deepcode-plus/settings.json`:
307307
- `on`:直接使用 PLUS key,不执行订阅检查。缺少 PLUS key 时明确报错,不回退普通通道。
308308
- `off`:固定使用普通通道,不执行订阅检查。
309309

310-
也可以在用户级或项目级 `settings.json` 的 `env.PLUS_API_KEY` 中配置 Key。内置图片、视频生成 Skill 仍只读取独立 Plus 配置。CLI也支持运行 `deepcode --plus`,按提示输入 DeepCode Plus API Key。验证通过后,Key 会保存到 `~/.deepcode-plus/settings.json`。
310+
也可以在用户级或项目级 `settings.json` 的 `env.PLUS_API_KEY` 中配置 Key。内置图片、视频生成 Skill 仍只读取独立 Plus 配置。CLI也支持运行 `deepcode --plus`,按提示输入 DeepCode Plus API Key。验证通过后,Key 会保存到 `~/.deepcode-plus/settings.json`。
311+
312+
## 配置诊断
313+
314+
运行 `deepcode --doctor` 检查配置、API 连接和安装环境;使用 `deepcode --doctor -s ./settings.json` 检查指定的配置文件。
315+
316+
报告列出配置文件路径、加载状态、最终值及其来源,并覆盖 DeepCode Plus 配置。密钥和自定义环境变量值不会显示。诊断不修改配置或执行更新。
317+
318+
输出包含 `PASS`、`WARN`、`FAIL` 和 `SKIP`。有 `FAIL` 时退出码为 1,否则为 0;只有警告不代表所有检查均已通过。命令可在非交互终端运行,不支持与 `--json` 或会话参数组合。

‎docs/configuration_en.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,11 @@ Configure PLUS separately in `~/.deepcode-plus/settings.json`:
307307
- `off`: Always use the regular connection without checking the subscription.
308308

309309
You can also set `env.PLUS_API_KEY` in user-level or project-level `settings.json`. The built-in image and video generation skills still read only the separate Plus settings file. The CLI also supports `deepcode --plus`, which prompts you to enter your DeepCode Plus API Key. After successful verification, the key is saved to `~/.deepcode-plus/settings.json`.
310+
311+
## Configuration diagnostics
312+
313+
Run `deepcode --doctor` to check configuration, API access, and the installation environment. Use `deepcode --doctor -s ./settings.json` to diagnose a specific settings file.
314+
315+
The report lists configuration paths, load status, effective values, and their sources, including DeepCode Plus settings. Keys and custom environment values are hidden. Diagnostics do not change settings or install updates.
316+
317+
Results use `PASS`, `WARN`, `FAIL`, and `SKIP`. The exit code is 1 if any check fails, otherwise 0; warnings can still mean a check could not be completed. The command works without a TTY and cannot be combined with `--json` or conversation options.

‎packages/cli/src/cli-app.tsx‎

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { render } from "ink";
2+
import { readFileSync } from "node:fs";
3+
import { join, resolve } from "node:path";
4+
import { homedir } from "node:os";
5+
import { setShellIfWindows, getProjectCode, resolveCurrentSettings } from "@vegamo/deepcode-core";
6+
import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check";
7+
import { AppContainer } from "./ui";
8+
import type { ParsedCliArgs } from "./cli-args";
9+
import { writeStderrLine, writeStdoutLine } from "./utils/stdio-helpers";
10+
import type { PackageJson } from "./utils/package";
11+
import { CLI_VERSION } from "./generated/git-commit";
12+
import { runExecMode } from "./exec-runner";
13+
14+
export async function runCliApp(parsed: ParsedCliArgs, packageInfo: PackageJson): Promise<void> {
15+
const projectRoot = process.cwd();
16+
const settingsFile = parsed.settingsFile === undefined ? undefined : resolve(projectRoot, parsed.settingsFile);
17+
if (settingsFile !== undefined) {
18+
try {
19+
resolveCurrentSettings(projectRoot, settingsFile);
20+
} catch (error) {
21+
writeStderrLine(`deepcode: ${error instanceof Error ? error.message : String(error)}`);
22+
process.exitCode = 1;
23+
return;
24+
}
25+
}
26+
27+
// Configure Windows shell AFTER --version/--help handling.
28+
// On Windows without Git Bash, setShellIfWindows() throws and calls process.exit(1).
29+
// If called before argument parsing, --help and --version would fail on those machines.
30+
configureWindowsShell();
31+
32+
let initialPrompt = parsed.prompt;
33+
let resumeSessionId = parsed.resume;
34+
let forkSessionId = parsed.fork;
35+
// Resolve --last to the most recent session ID for the current project
36+
if (parsed.last) {
37+
const projectCode = getProjectCode(projectRoot);
38+
const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json");
39+
try {
40+
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
41+
const entries: { id: string; updateTime: string }[] = Array.isArray(index?.entries) ? index.entries : [];
42+
if (entries.length === 0) {
43+
writeStderrLine("No previous sessions found for the current project.\n");
44+
process.exit(1);
45+
}
46+
const mostRecent = entries.reduce((a, b) => (a.updateTime > b.updateTime ? a : b));
47+
resumeSessionId = mostRecent.id;
48+
} catch {
49+
writeStderrLine("No previous sessions found for the current project.\n");
50+
process.exit(1);
51+
}
52+
}
53+
54+
if (forkSessionId === true) {
55+
const projectCode = getProjectCode(projectRoot);
56+
const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json");
57+
try {
58+
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
59+
const entries: { id: string; updateTime: string }[] = Array.isArray(index?.entries) ? index.entries : [];
60+
if (entries.length === 0) {
61+
writeStderrLine("No previous sessions found for the current project.\n");
62+
process.exit(1);
63+
}
64+
const mostRecent = entries.reduce((a, b) => (a.updateTime > b.updateTime ? a : b));
65+
forkSessionId = mostRecent.id;
66+
} catch {
67+
writeStderrLine("No previous sessions found for the current project.\n");
68+
process.exit(1);
69+
}
70+
}
71+
72+
if (parsed.exec) {
73+
process.exitCode = await runExecMode({
74+
prompt: parsed.prompt!,
75+
projectRoot,
76+
settingsFile,
77+
sessionId: parsed.sessionId,
78+
json: parsed.json,
79+
resumeSessionId: typeof resumeSessionId === "string" ? resumeSessionId : undefined,
80+
forkSessionId: typeof forkSessionId === "string" ? forkSessionId : undefined,
81+
});
82+
return;
83+
}
84+
85+
if (!process.stdin.isTTY) {
86+
writeStderrLine("deepcode requires an interactive terminal (TTY). Re-run from a real terminal session.\n");
87+
process.exit(1);
88+
}
89+
90+
// Validate --resume <sessionId> before entering TUI
91+
if (typeof resumeSessionId === "string") {
92+
const projectCode = getProjectCode(projectRoot);
93+
const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json");
94+
try {
95+
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
96+
const found =
97+
Array.isArray(index?.entries) && index.entries.some((e: { id: string }) => e.id === resumeSessionId);
98+
if (!found) {
99+
writeStderrLine(`No saved session found with ID "${resumeSessionId}".\n`);
100+
process.exit(1);
101+
}
102+
} catch {
103+
writeStderrLine(`No saved session found with ID "${resumeSessionId}".\n`);
104+
process.exit(1);
105+
}
106+
}
107+
108+
if (typeof forkSessionId === "string") {
109+
const projectCode = getProjectCode(projectRoot);
110+
const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json");
111+
try {
112+
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
113+
const found = Array.isArray(index?.entries) && index.entries.some((e: { id: string }) => e.id === forkSessionId);
114+
if (!found) {
115+
writeStderrLine(`No saved session found with ID "${forkSessionId}".\n`);
116+
process.exit(1);
117+
}
118+
} catch {
119+
writeStderrLine(`No saved session found with ID "${forkSessionId}".\n`);
120+
process.exit(1);
121+
}
122+
}
123+
124+
const updatePromptResult = await promptForPendingUpdate(packageInfo);
125+
if (updatePromptResult.installed) {
126+
process.exit(0);
127+
}
128+
129+
const restartRef: { current: (() => void) | null } = { current: null };
130+
131+
function startApp(): void {
132+
let restarting = false;
133+
const appInitialPrompt = initialPrompt;
134+
initialPrompt = undefined;
135+
const appResumeSessionId = resumeSessionId;
136+
resumeSessionId = undefined;
137+
const appForkSessionId = forkSessionId;
138+
forkSessionId = undefined;
139+
const inkInstance = render(
140+
<AppContainer
141+
projectRoot={projectRoot}
142+
settingsFile={settingsFile}
143+
version={packageInfo?.version ?? CLI_VERSION}
144+
initialPrompt={appInitialPrompt}
145+
resumeSessionId={appResumeSessionId}
146+
forkSessionId={typeof appForkSessionId === "string" ? appForkSessionId : undefined}
147+
onRestart={() => restartRef.current?.()}
148+
/>,
149+
{ exitOnCtrlC: false }
150+
);
151+
152+
restartRef.current = () => {
153+
restarting = true;
154+
writeStdoutLine("\u001B[2J\u001B[3J\u001B[H");
155+
inkInstance.unmount();
156+
startApp();
157+
};
158+
159+
inkInstance.waitUntilExit().then(() => {
160+
if (!restarting) {
161+
restartRef.current = null;
162+
process.exit(0);
163+
}
164+
});
165+
}
166+
167+
void checkForNpmUpdate(packageInfo);
168+
169+
startApp();
170+
}
171+
172+
/**
173+
* Configure shell environment for Windows.
174+
* Sets NoDefaultCurrentDirectoryInExePath and resolves Git Bash path.
175+
* Must be called after --version/--help handling to avoid blocking those
176+
* commands on Windows machines without Git Bash installed.
177+
*/
178+
function configureWindowsShell(): void {
179+
process.env.NoDefaultCurrentDirectoryInExePath = "1";
180+
try {
181+
setShellIfWindows();
182+
} catch (error) {
183+
const message = error instanceof Error ? error.message : String(error);
184+
writeStderrLine(`deepcode: ${message}\n`);
185+
process.exit(1);
186+
}
187+
}

‎packages/cli/src/cli-args.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Yargs from "yargs";
88
import { getCliVersion } from "./utils/version";
99
import { writeStderrLine } from "./utils/stdio-helpers";
1010
import { hideBin } from "yargs/helpers";
11-
import { isValidSessionId } from "@vegamo/deepcode-core";
11+
import { isValidSessionId } from "@vegamo/deepcode-core/common/session-id";
1212

1313
export { isValidSessionId };
1414

@@ -17,6 +17,8 @@ export interface ParsedCliArgs {
1717
settingsFile: string | undefined;
1818
/** Configure the DeepCode Plus API key and exit. */
1919
plus: boolean;
20+
/** Diagnose installation, configuration, and API access. */
21+
doctor: boolean;
2022
/** Prompt text from -p / --prompt */
2123
prompt: string | undefined;
2224
/** Run one prompt without starting the interactive TUI. */
@@ -85,6 +87,11 @@ async function configureYargs(argv?: string[]) {
8587
.usage("Usage: $0 [options] [command]\n\nDeep Code - Launch the interactive CLI or run one prompt with --exec")
8688
.command("$0 [query..]", "Launch Deep Code CLI", (yargsInstance: Argv) =>
8789
yargsInstance
90+
.option("doctor", {
91+
type: "boolean",
92+
default: false,
93+
describe: "Check configuration, API access, and installation",
94+
})
8895
.option("plus", {
8996
type: "boolean",
9097
default: false,
@@ -140,6 +147,20 @@ async function configureYargs(argv?: string[]) {
140147
const exec = argv["exec"] === true;
141148
const sessionId = argv["session-id"] as string | undefined;
142149

150+
if (
151+
argv["doctor"] === true &&
152+
(argv["plus"] === true ||
153+
prompt !== undefined ||
154+
hasPositionalQuery ||
155+
exec ||
156+
sessionId !== undefined ||
157+
argv["json"] === true ||
158+
argv["resume"] !== undefined ||
159+
argv["fork"] !== undefined ||
160+
argv["last"] === true)
161+
)
162+
return "--doctor can only be combined with --settings / -s.";
163+
143164
if (
144165
argv["plus"] === true &&
145166
(argv["settings"] !== undefined ||
@@ -208,6 +229,7 @@ async function configureYargs(argv?: string[]) {
208229
})
209230
)
210231
.example("deepcode", "Launch the interactive TUI in the current directory")
232+
.example("deepcode --doctor", "Diagnose configuration, API access, and installation")
211233
.example("deepcode --plus", "Configure your DeepCode Plus API Key")
212234
.example("deepcode -p <prompt>", "Launch the TUI and submit a prompt")
213235
.example("deepcode -x -p <prompt>", "Run one prompt without launching the TUI")
@@ -266,6 +288,7 @@ export async function parseArguments(argv?: string[]): Promise<ParsedCliArgs> {
266288
return {
267289
settingsFile: parsed.settings as string | undefined,
268290
plus: parsed.plus === true,
291+
doctor: parsed.doctor === true,
269292
prompt: parsed.prompt as string | undefined,
270293
exec: parsed.exec === true,
271294
sessionId: (parsed["session-id"] as string | undefined)?.toLowerCase(),

0 commit comments

Comments
 (0)