Skip to content

Commit 5c3f1be

Browse files
committed
feat(cli): add report command
1 parent d35c20f commit 5c3f1be

7 files changed

Lines changed: 236 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ dvcode --session abc123
339339
| :----------------- | :------------------------------------ |
340340
| `/help` | 显示帮助信息和快速入门指南 |
341341
| `/doctor` | 运行 CLI 快速诊断 |
342+
| `/report` | 生成诊断报告(默认复制到剪切板) |
342343
| `/help-ask` | AI 智能帮助助手,解答使用问题 |
343344
| `/issue <描述>` | 提交 GitHub Issue(自动附带错误日志) |
344345
| `/quit``/exit` | 退出应用,显示会话统计 |

docs/cli/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Slash commands provide meta-level control over the CLI itself.
4848
- **`/doctor`**
4949
- **Description:** Run quick diagnostics for the CLI environment (versions, build status, core dependencies).
5050

51+
- **`/report`**
52+
- **Description:** Generate a diagnostic report for sharing. Copies to clipboard by default.
53+
5154
- **`/mcp`**
5255
- **Description:** List configured Model Context Protocol (MCP) servers, their connection status, server details, and available tools.
5356
- **Sub-commands:**

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
2424
import { helpCommand } from '../ui/commands/helpCommand.js';
2525
import { helpAskCommand } from '../ui/commands/helpAskCommand.js';
2626
import { doctorCommand } from '../ui/commands/doctorCommand.js';
27+
import { reportCommand } from '../ui/commands/reportCommand.js';
2728
import { ideCommand } from '../ui/commands/ideCommand.js';
2829
import { initCommand } from '../ui/commands/initCommand.js';
2930
// import { mcpCommand } from '../ui/commands/mcpCommand.js'; // 已删除
@@ -89,6 +90,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
8990
helpCommand,
9091
helpAskCommand,
9192
doctorCommand,
93+
reportCommand,
9294
hooksCommand,
9395
issueCommand,
9496
ideCommand(this.config),

packages/cli/src/test-utils/mockCommandContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const createMockCommandContext = (
5353
setPendingItem: vi.fn(),
5454
loadHistory: vi.fn(),
5555
toggleCorgiMode: vi.fn(),
56+
toggleVimEnabled: vi.fn().mockResolvedValue(true),
57+
history: [],
5658
},
5759
session: {
5860
stats: {
@@ -70,6 +72,8 @@ export const createMockCommandContext = (
7072
},
7173
},
7274
} as SessionStatsState,
75+
cumulativeCredits: 0,
76+
totalSessionCredits: 0,
7377
},
7478
};
7579

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8+
import { reportCommand } from './reportCommand.js';
9+
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
10+
import * as versionUtils from '../../utils/version.js';
11+
import * as commandUtils from '../utils/commandUtils.js';
12+
import { AuthType } from 'deepv-code-core';
13+
14+
vi.mock('../../utils/version.js', () => ({
15+
getCliVersion: vi.fn(),
16+
}));
17+
18+
vi.mock('../utils/commandUtils.js', () => ({
19+
copyToClipboard: vi.fn(),
20+
}));
21+
22+
describe('reportCommand', () => {
23+
const originalEnv = { ...process.env };
24+
25+
beforeEach(() => {
26+
vi.clearAllMocks();
27+
vi.mocked(versionUtils.getCliVersion).mockResolvedValue('test-version');
28+
});
29+
30+
afterEach(() => {
31+
process.env = originalEnv;
32+
});
33+
34+
it('should copy report to clipboard by default', async () => {
35+
const context = createMockCommandContext({
36+
services: {
37+
config: {
38+
getProjectRoot: () => '/mock/root',
39+
},
40+
settings: {
41+
merged: {
42+
selectedAuthType: AuthType.USE_PROXY_AUTH,
43+
},
44+
},
45+
},
46+
});
47+
48+
if (!reportCommand.action) {
49+
throw new Error('report command must have an action');
50+
}
51+
52+
const result = await reportCommand.action(context, '');
53+
54+
expect(commandUtils.copyToClipboard).toHaveBeenCalledTimes(1);
55+
const copied = vi.mocked(commandUtils.copyToClipboard).mock.calls[0]?.[0];
56+
expect(copied).toContain('DeepV Code Report');
57+
expect(copied).toContain('CLI version: test-version');
58+
expect(result?.type).toBe('message');
59+
});
60+
});
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import process from 'node:process';
4+
import { getCliVersion } from '../../utils/version.js';
5+
import { copyToClipboard } from '../utils/commandUtils.js';
6+
import { CommandKind, SlashCommand } from './types.js';
7+
import { t } from '../utils/i18n.js';
8+
9+
type ReportOptions = {
10+
copy: boolean;
11+
full: boolean;
12+
};
13+
14+
const parseReportOptions = (args: string): ReportOptions => {
15+
const tokens = args.split(/\s+/).filter(Boolean);
16+
let copy = true;
17+
let full = false;
18+
19+
for (let i = 0; i < tokens.length; i += 1) {
20+
const token = tokens[i];
21+
if (token === '--no-copy') {
22+
copy = false;
23+
continue;
24+
}
25+
if (token === '--copy') {
26+
copy = true;
27+
continue;
28+
}
29+
if (token === '--full') {
30+
full = true;
31+
}
32+
}
33+
34+
return { copy, full };
35+
};
36+
37+
const formatCheck = (label: string, ok: boolean, detail?: string): string => {
38+
const status = ok ? 'ok' : 'missing';
39+
if (detail) {
40+
return `- ${label}: ${status} (${detail})`;
41+
}
42+
return `- ${label}: ${status}`;
43+
};
44+
45+
const formatRecentLines = (title: string, items: string[]): string => {
46+
if (items.length === 0) {
47+
return `${title}: (none)`;
48+
}
49+
return [title, ...items.map((item) => `- ${item}`)].join('\n');
50+
};
51+
52+
export const reportCommand: SlashCommand = {
53+
name: 'report',
54+
description: t('command.report.description'),
55+
kind: CommandKind.BUILT_IN,
56+
action: async (context, args) => {
57+
const options = parseReportOptions(args);
58+
const cliVersion = await getCliVersion();
59+
const config = context.services.config;
60+
const projectRoot = config?.getProjectRoot() ?? process.cwd();
61+
const settings = context.services.settings.merged;
62+
63+
const cliBuildStamp = path.join(
64+
projectRoot,
65+
'packages',
66+
'cli',
67+
'dist',
68+
'.last_build',
69+
);
70+
const coreBuildStamp = path.join(
71+
projectRoot,
72+
'packages',
73+
'core',
74+
'dist',
75+
'.last_build',
76+
);
77+
const genaiTypes = path.join(
78+
projectRoot,
79+
'node_modules',
80+
'@google',
81+
'genai',
82+
'dist',
83+
'genai.d.ts',
84+
);
85+
86+
const reportLines: string[] = [
87+
'# DeepV Code Report',
88+
'',
89+
'## Environment',
90+
`- CLI version: ${cliVersion}`,
91+
`- Node: ${process.version}`,
92+
`- Platform: ${process.platform} ${process.arch}`,
93+
`- Project root: ${projectRoot}`,
94+
`- Auth type: ${settings.selectedAuthType ?? 'not configured'}`,
95+
`- Server URL: ${process.env.DEEPX_SERVER_URL ?? 'default'}`,
96+
'',
97+
'## Build & Dependencies',
98+
formatCheck(
99+
'CLI build stamp',
100+
fs.existsSync(cliBuildStamp),
101+
'packages/cli/dist/.last_build',
102+
),
103+
formatCheck(
104+
'Core build stamp',
105+
fs.existsSync(coreBuildStamp),
106+
'packages/core/dist/.last_build',
107+
),
108+
formatCheck(
109+
'GenAI types',
110+
fs.existsSync(genaiTypes),
111+
'node_modules/@google/genai/dist/genai.d.ts',
112+
),
113+
];
114+
115+
if (options.full) {
116+
const recentErrors = (context.ui.history ?? [])
117+
.filter((item) => item.type === 'error' && item.text)
118+
.slice(-5)
119+
.map((item) => item.text ?? '');
120+
121+
const recentDebug = (context.ui.debugMessages ?? [])
122+
.slice(-5)
123+
.map((item) => item.content ?? '')
124+
.filter((item) => item);
125+
126+
reportLines.push(
127+
'',
128+
'## Recent Errors',
129+
formatRecentLines('Errors', recentErrors),
130+
'',
131+
'## Recent Debug Messages',
132+
formatRecentLines('Debug', recentDebug),
133+
);
134+
}
135+
136+
const report = reportLines.join('\n');
137+
138+
if (options.copy) {
139+
try {
140+
await copyToClipboard(report);
141+
} catch (error) {
142+
const message =
143+
error instanceof Error ? error.message : String(error ?? 'unknown');
144+
return {
145+
type: 'message',
146+
messageType: 'error',
147+
content: `${t('command.report.copy_failed')} ${message}`,
148+
};
149+
}
150+
}
151+
152+
return {
153+
type: 'message',
154+
messageType: 'info',
155+
content: options.copy
156+
? `${report}\n\n${t('command.report.copied')}`
157+
: report,
158+
};
159+
},
160+
};

packages/cli/src/ui/utils/i18n.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@ export const translations = {
972972
// Slash command descriptions
973973
'command.help.description': 'Get deepv-code help',
974974
'command.doctor.description': 'Run quick diagnostics for the CLI',
975+
'command.report.description': 'Generate a diagnostic report for sharing',
975976
'command.clear.description':
976977
'Clear terminal screen (keeps conversation context)',
977978
'command.queue.description': 'Manage prompt queue',
@@ -988,6 +989,8 @@ export const translations = {
988989
'command.issue.section.error_logs': 'Error Logs',
989990
'command.issue.no_error_logs': 'No error logs captured in this session.',
990991
'command.issue.opening': 'Opening GitHub issue form in your browser...',
992+
'command.report.copied': 'Report copied to clipboard.',
993+
'command.report.copy_failed': 'Failed to copy report to clipboard:',
991994
'command.issue.open.manual':
992995
'Please open the following URL in your browser to submit the issue:\n{url}',
993996
'command.issue.open.failed': 'Failed to open the issue URL: {error}',
@@ -2572,6 +2575,7 @@ export const translations = {
25722575
// Slash command descriptions
25732576
'command.help.description': '获取 deepv-code 帮助',
25742577
'command.doctor.description': '运行 CLI 快速诊断',
2578+
'command.report.description': '生成可分享的诊断报告',
25752579
'command.clear.description': '清除终端屏幕(保留对话上下文)',
25762580
'command.queue.description': '管理提示队列',
25772581
'command.queue.clear.description': '清空所有排队的提示',
@@ -2586,6 +2590,8 @@ export const translations = {
25862590
'command.issue.section.error_logs': '错误日志',
25872591
'command.issue.no_error_logs': '本次会话未捕获到错误日志。',
25882592
'command.issue.opening': '正在为你打开 GitHub Issue 提交页面...',
2593+
'command.report.copied': '报告已复制到剪切板。',
2594+
'command.report.copy_failed': '复制报告到剪切板失败:',
25892595
'command.issue.open.manual': '请在浏览器中打开以下链接提交 Issue:\n{url}',
25902596
'command.issue.open.failed': '打开 Issue 链接失败:{error}',
25912597
'command.about.description': '显示版本信息',

0 commit comments

Comments
 (0)