dsh-github-flow 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.en.md +75 -8
  2. package/README.md +75 -8
  3. package/docs/UPGRADE-NOTES.md +9 -12
  4. package/docs/architecture.md +39 -21
  5. package/lib/api/routes-actions.js +132 -0
  6. package/lib/client.d.ts +4 -3
  7. package/lib/client.js +1436 -80
  8. package/lib/commands/gh.d.ts +6 -1
  9. package/lib/commands/gh.js +144 -71
  10. package/lib/executor/git-parser.d.ts +33 -1
  11. package/lib/executor/git-parser.js +116 -4
  12. package/lib/executor.d.ts +27 -2
  13. package/lib/executor.js +189 -5
  14. package/lib/main.js +28 -4
  15. package/lib/renderers/agent-task.d.ts +16 -0
  16. package/lib/renderers/agent-task.js +107 -0
  17. package/lib/renderers/codespace.d.ts +16 -0
  18. package/lib/renderers/codespace.js +103 -0
  19. package/lib/renderers/discussion.d.ts +16 -0
  20. package/lib/renderers/discussion.js +101 -0
  21. package/lib/renderers/markdown.d.ts +4 -0
  22. package/lib/renderers/markdown.js +4 -0
  23. package/lib/renderers/project.d.ts +20 -0
  24. package/lib/renderers/project.js +128 -0
  25. package/lib/renderers/skill.d.ts +30 -0
  26. package/lib/renderers/skill.js +180 -0
  27. package/lib/renderers/status.d.ts +5 -0
  28. package/lib/renderers/status.js +52 -0
  29. package/lib/telemetry.d.ts +70 -0
  30. package/lib/telemetry.js +161 -0
  31. package/lib/tools/agent-task.d.ts +54 -0
  32. package/lib/tools/agent-task.js +135 -0
  33. package/lib/tools/codespace.d.ts +51 -0
  34. package/lib/tools/codespace.js +154 -0
  35. package/lib/tools/discussion.d.ts +70 -0
  36. package/lib/tools/discussion.js +195 -0
  37. package/lib/tools/help.js +109 -9
  38. package/lib/tools/project.d.ts +80 -0
  39. package/lib/tools/project.js +241 -0
  40. package/lib/tools/repo.js +13 -1
  41. package/lib/tools/skill.d.ts +24 -1
  42. package/lib/tools/skill.js +115 -5
  43. package/lib/tools/status.d.ts +3 -2
  44. package/lib/tools/status.js +9 -4
  45. package/lib/types.d.ts +5 -0
  46. package/lib/ui/commit-modal.js +144 -14
  47. package/lib/ui/help-modal.d.ts +15 -0
  48. package/lib/ui/help-modal.js +202 -0
  49. package/lib/ui/storage.d.ts +9 -0
  50. package/lib/ui/storage.js +24 -0
  51. package/lib/ui/styles.d.ts +1 -1
  52. package/lib/ui/styles.js +259 -0
  53. package/lib/ui/sync-settings-modal.js +1 -1
  54. package/lib/ui/upgrade-card.js +39 -14
  55. package/lib/ui/utils.d.ts +35 -0
  56. package/lib/ui/utils.js +160 -0
  57. package/lib/ui/workspace-panel.js +30 -5
  58. package/lib/upgrade-manager.d.ts +5 -1
  59. package/lib/upgrade-manager.js +24 -1
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. package/package.json +2 -2
@@ -1,3 +1,8 @@
1
1
  import type { GhExecutor } from '../executor.js';
2
2
  import type { UpdateChecker } from '../updater.js';
3
- export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker): void;
3
+ import { TelemetryCollector } from '../telemetry.js';
4
+ /**
5
+ * 注册人类 Slash 快捷指令集
6
+ * 采用扁平化一级指令 + 二级参数容错双轨机制,消除用户“敲了 /gh 不会加参数”的交互盲区
7
+ */
8
+ export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker, telemetryCollector?: TelemetryCollector): void;
@@ -1,95 +1,168 @@
1
1
  import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL } from '../version.js';
2
2
  import { loadUpdateSettings, describeUpdateMode } from '../update-settings.js';
3
- export function registerGhCommand(commandsService, executor, updateChecker) {
3
+ import { defaultTelemetryCollector } from '../telemetry.js';
4
+ /**
5
+ * 构建 GitHub 状态大盘与诊断报告 Markdown
6
+ */
7
+ async function buildStatusReport(executor, updateChecker, telemetryCollector) {
8
+ // 人类显式诊断必须绕过缓存:用户敲 /gh 往往正是为了确认
9
+ // 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
10
+ const auth = await executor.checkAuth(undefined, true);
11
+ const repo = await executor.getRepoMetadata();
12
+ const updateInfo = updateChecker?.getQuickStatus();
13
+ let md = `### GitHub 集成状态\n\n`;
14
+ // 若检测到新版本,在最上方展示醒目的升级指引横幅
15
+ if (updateInfo?.hasUpdate) {
16
+ md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
17
+ md += `> 输入 \`/gh-upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
18
+ }
19
+ md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
20
+ if (auth.loggedIn) {
21
+ md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
22
+ md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
23
+ }
24
+ else {
25
+ md += `- **登录状态**: ❌ 未登录 GitHub\n`;
26
+ md += `> 提示:请在系统终端执行 \`gh auth login\` 进行登录。\n`;
27
+ }
28
+ if (repo) {
29
+ md += `\n**当前关联仓库**:\n`;
30
+ md += `- 仓库名: [${repo.nameWithOwner}](${repo.url})\n`;
31
+ md += `- 默认分支: \`${repo.defaultBranch}\`\n`;
32
+ if (repo.description)
33
+ md += `- 简介: ${repo.description}\n`;
34
+ }
35
+ else {
36
+ md += `\n*当前目录尚未关联 Git 远程仓库或未托管在 GitHub。*\n`;
37
+ }
38
+ // 遥测运行时健康度与降级率小节
39
+ const collector = telemetryCollector || defaultTelemetryCollector;
40
+ const summary = collector.getSummary();
41
+ let healthBadge = '🟢 极佳';
42
+ if (summary.statusLevel === 'NEEDS_ATTENTION')
43
+ healthBadge = '🔴 需关注';
44
+ else if (summary.statusLevel === 'GOOD')
45
+ healthBadge = '🟡 良好';
46
+ md += `\n**工具链运行时可观测性 (Telemetry)**:\n`;
47
+ md += `- 综合健康度: ${healthBadge} (${summary.healthScore} 分)\n`;
48
+ md += `- 统计样本量: ${summary.totalCalls} 次 Agent 工具调用\n`;
49
+ md += `- API 降级率: \`${summary.degradationRate}%\` (${summary.degradedCount} 次降级)\n`;
50
+ if (summary.recommendations.length > 0 && summary.degradationRate > 0) {
51
+ md += `> 💡 优化建议: ${summary.recommendations[0]}\n`;
52
+ }
53
+ md += `\n> 💡 **快捷指令指引**: 可直接输入 \`/gh-upgrade\` 检查组件更新,输入 \`/gh-help\` 查看 Agent 协作范例。`;
54
+ return md;
55
+ }
56
+ /**
57
+ * 构建插件版本升级指南 Markdown
58
+ */
59
+ async function buildUpgradeReport(updateChecker) {
60
+ const info = updateChecker ? await updateChecker.check(true) : null;
61
+ const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
62
+ const latestVer = info?.latestVersion || curVer;
63
+ const hasUpdate = info?.hasUpdate ?? false;
64
+ const isMac = process.platform === 'darwin';
65
+ const isWin = process.platform === 'win32';
66
+ const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
67
+ let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
68
+ md += `- **当前运行版本**: \`v${curVer}\`\n`;
69
+ // 当前更新源:排查「查不到新版」时用户需可见自己在用哪个源
70
+ const liveMode = updateChecker?.getSourceMode?.() ?? null;
71
+ const effectiveMode = liveMode ?? loadUpdateSettings();
72
+ md += `- **当前更新源**: ${describeUpdateMode(effectiveMode)}\n`;
73
+ md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
74
+ md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
75
+ md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
76
+ md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
77
+ md += `1. **DSH 桌面版 Profile 环境升级(推荐)**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
78
+ md += `2. **DSH Web / CLI 环境**(profile 名按实际环境填写):\n\`\`\`bash\ndsh plugin --profile web add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
79
+ if (isMac) {
80
+ md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
81
+ }
82
+ else {
83
+ md += `\n`;
84
+ }
85
+ md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
86
+ md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
87
+ return md;
88
+ }
89
+ /**
90
+ * 构建使用帮助与 Agent 提示词范例 Markdown
91
+ */
92
+ function buildHelpReport() {
93
+ return [
94
+ '### 🧭 DSH GitHub Flow 快捷指令帮助\n',
95
+ '- `/gh`:查看当前 GitHub CLI 账号认证状态、关联仓库与运行时健康度(直接回车即可)',
96
+ '- `/gh-upgrade`:检查新版本并输出针对当前系统的最佳升级命令',
97
+ '- `/gh-help`:显示此使用指南与 Agent 对话范例\n',
98
+ '*(提示:熟练用户亦可使用二级语法,如 `/gh upgrade`、`/gh status`)*\n',
99
+ '#### 可以在对话框直接让 Agent 操作 GitHub,例如:',
100
+ '- *"帮我看看今天在 GitHub 上有哪些需要我处理的事情?"*',
101
+ '- *"查看当前仓库有哪些开放的 PR,审查 PR #12 的代码差异"*',
102
+ '- *"查看最近一次 CI 失败的原因并尝试自动修复"*',
103
+ '- *"帮我给当前分支创建一个 PR 并附带改动说明"*',
104
+ '- *"在当前仓库发起一个新的 RFC 讨论方案"*',
105
+ ].join('\n');
106
+ }
107
+ /**
108
+ * 注册人类 Slash 快捷指令集
109
+ * 采用扁平化一级指令 + 二级参数容错双轨机制,消除用户“敲了 /gh 不会加参数”的交互盲区
110
+ */
111
+ export function registerGhCommand(commandsService, executor, updateChecker, telemetryCollector) {
4
112
  if (!commandsService || typeof commandsService.register !== 'function')
5
113
  return;
114
+ // 1. 主指令 /gh(默认直接回车输出状态,同时兼容 /gh status, /gh upgrade, /gh help 及常见别名)
6
115
  commandsService.register({
7
116
  name: 'gh',
8
- description: 'GitHub CLI 状态与诊断快捷指令',
117
+ description: '查看 GitHub 账号状态、关联仓库与运行时健康度(直接回车即可执行)',
9
118
  async handler({ rawInput }) {
10
- const trimmed = (rawInput || '').trim();
11
- if (!trimmed || trimmed === 'status') {
12
- // 人类显式诊断必须绕过缓存:用户敲 /gh status 往往正是为了确认
13
- // 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
14
- const auth = await executor.checkAuth(undefined, true);
15
- const repo = await executor.getRepoMetadata();
16
- const updateInfo = updateChecker?.getQuickStatus();
17
- let md = `### GitHub 集成状态\n\n`;
18
- // 若检测到新版本,在最上方展示醒目的升级指引横幅
19
- if (updateInfo?.hasUpdate) {
20
- md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
21
- md += `> 输入 \`/gh upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
22
- }
23
- md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
24
- if (auth.loggedIn) {
25
- md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
26
- md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
27
- }
28
- else {
29
- md += `- **登录状态**: ❌ 未登录 GitHub\n`;
30
- md += `> 提示:请在系统终端执行 \`gh auth login\` 进行登录。\n`;
31
- }
32
- if (repo) {
33
- md += `\n**当前关联仓库**:\n`;
34
- md += `- 仓库名: [${repo.nameWithOwner}](${repo.url})\n`;
35
- md += `- 默认分支: \`${repo.defaultBranch}\`\n`;
36
- if (repo.description)
37
- md += `- 简介: ${repo.description}\n`;
38
- }
39
- else {
40
- md += `\n*当前目录尚未关联 Git 远程仓库或未托管在 GitHub。*\n`;
41
- }
119
+ const trimmed = (rawInput || '').trim().toLowerCase();
120
+ // 智能容错:无参数、status、info、auth 均直接直出状态大盘
121
+ if (!trimmed || trimmed === 'status' || trimmed === 'info' || trimmed === 'auth' || trimmed === 'check') {
42
122
  return {
43
123
  kind: 'success',
44
- text: md,
124
+ text: await buildStatusReport(executor, updateChecker, telemetryCollector),
45
125
  };
46
126
  }
47
- if (trimmed === 'upgrade') {
48
- const info = updateChecker ? await updateChecker.check(true) : null;
49
- const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
50
- const latestVer = info?.latestVersion || curVer;
51
- const hasUpdate = info?.hasUpdate ?? false;
52
- const isMac = process.platform === 'darwin';
53
- const isWin = process.platform === 'win32';
54
- const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
55
- let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
56
- md += `- **当前运行版本**: \`v${curVer}\`\n`;
57
- // 当前更新源:排查「查不到新版」时(镜像同步滞后)用户需可见自己在用哪个源。
58
- // 优先读 checker 内存态(store 写盘失败时磁盘是旧值,活实例才是事实源),
59
- // 无 checker 或未初始化(sourceMode=null 的 legacy 路径)回落磁盘 store。
60
- const liveMode = updateChecker?.getSourceMode?.() ?? null;
61
- const effectiveMode = liveMode ?? loadUpdateSettings();
62
- md += `- **当前更新源**: ${describeUpdateMode(effectiveMode)}\n`;
63
- md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
64
- md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
65
- md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
66
- md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
67
- // 官方 CLI 语义:--profile 为必填参数(dsh plugin --profile <name> add <pkg>),
68
- // 任何不带 --profile 的形态(含 pnpm dsh plugin add)都会直接报 required option 错误。
69
- md += `1. **DSH 桌面版 Profile 环境升级(推荐)**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
70
- md += `2. **DSH Web / CLI 环境**(profile 名按实际环境填写):\n\`\`\`bash\ndsh plugin --profile web add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
71
- if (isMac) {
72
- md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
73
- }
74
- else {
75
- md += `\n`;
76
- }
77
- md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
78
- md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
127
+ // 二级 upgrade / update 别名容错
128
+ if (trimmed === 'upgrade' || trimmed === 'update' || trimmed === 'up' || trimmed === 'version') {
79
129
  return {
80
130
  kind: 'success',
81
- text: md,
131
+ text: await buildUpgradeReport(updateChecker),
82
132
  };
83
133
  }
84
- if (trimmed === 'help') {
134
+ // 二级 help / ? 别名容错
135
+ if (trimmed === 'help' || trimmed === 'h' || trimmed === '?') {
85
136
  return {
86
137
  kind: 'success',
87
- text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI 账号、仓库连接与插件版本\n- \`/gh upgrade\`: 检查新版本并显示全场景升级命令指引\n- \`/gh help\`: 显示此帮助信息\n\n可以在对话中直接让 Agent 操作 GitHub,如:\n- *"查看当前仓库有哪些开放的 PR"* \n- *"查看最近一次 CI 失败的原因"* \n- *"帮我给当前分支创建 PR"*`,
138
+ text: buildHelpReport(),
88
139
  };
89
140
  }
90
141
  return {
91
142
  kind: 'error',
92
- text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh upgrade, /gh help`,
143
+ text: `未知命令选项: /gh ${rawInput}。可用快捷指令: /gh(查看状态)、/gh-upgrade(检查升级)、/gh-help(帮助)。`,
144
+ };
145
+ },
146
+ });
147
+ // 2. 扁平化一级指令 /gh-upgrade(下拉菜单一键可见,无需手动输入空格与子命令)
148
+ commandsService.register({
149
+ name: 'gh-upgrade',
150
+ description: '检查插件新版本并查看一键自动升级指引',
151
+ async handler() {
152
+ return {
153
+ kind: 'success',
154
+ text: await buildUpgradeReport(updateChecker),
155
+ };
156
+ },
157
+ });
158
+ // 3. 扁平化一级指令 /gh-help(下拉菜单一键可见,查看指南与 Agent 提示词范例)
159
+ commandsService.register({
160
+ name: 'gh-help',
161
+ description: '查看 GitHub 协作帮助与 Agent 提示词范例',
162
+ async handler() {
163
+ return {
164
+ kind: 'success',
165
+ text: buildHelpReport(),
93
166
  };
94
167
  },
95
168
  });
@@ -9,7 +9,7 @@ import type { GitLocalStatus, GitCommitEntry, GitBranchesResult } from '../types
9
9
  * `## master` / `## HEAD (no branch)` 无 upstream(ahead/behind 未知=-1)
10
10
  * 后续每行 `<XY> <path>` 为 porcelain 变更项(XY 两字符状态码,?? = untracked)。
11
11
  */
12
- export declare function parseGitStatusOutput(output: string): GitLocalStatus | null;
12
+ export declare function parseGitStatusOutput(output: string, maxFiles?: number): GitLocalStatus | null;
13
13
  /**
14
14
  * 解析 `git log` 输出为提交条目数组。
15
15
  * 支持两种形态:
@@ -23,3 +23,35 @@ export declare function parseGitLogLines(output: string): GitCommitEntry[];
23
23
  * 格式为: %(refname:short)|%(HEAD)|%(upstream:short)|%(committerdate:relative)|%(objectname:short)|%(subject)
24
24
  */
25
25
  export declare function parseGitBranchLines(output: string): GitBranchesResult;
26
+ /**
27
+ * 解析 unified diff 输出,统计增删行数与区块数量。
28
+ * 忽略 diff 文件头(--- 和 +++),仅统计真正的改动行(+ 与 -)。
29
+ */
30
+ export declare function parseUncommittedDiff(rawDiff: string): {
31
+ additions: number;
32
+ deletions: number;
33
+ hunksCount: number;
34
+ };
35
+ export interface GitStatFileInfo {
36
+ path: string;
37
+ total: number;
38
+ additions: number;
39
+ deletions: number;
40
+ isBinary: boolean;
41
+ rawRight?: string;
42
+ }
43
+ export interface GitStatSummary {
44
+ filesChanged: number;
45
+ insertions: number;
46
+ deletions: number;
47
+ }
48
+ export interface GitStatParsedResult {
49
+ files: GitStatFileInfo[];
50
+ summary: GitStatSummary | null;
51
+ }
52
+ /**
53
+ * 解析 `git show --stat` 或 `git log --stat` 的统计文本为结构化列表与增删数字。
54
+ * 能够从 `19 ++++++++++++++++++-` 精准推导 additions=18, deletions=1,
55
+ * 提取文件路径与 `X files changed, Y insertions(+), Z deletions(-)` 汇总。
56
+ */
57
+ export declare function parseGitStatText(statText: string): GitStatParsedResult | null;
@@ -8,7 +8,7 @@
8
8
  * `## master` / `## HEAD (no branch)` 无 upstream(ahead/behind 未知=-1)
9
9
  * 后续每行 `<XY> <path>` 为 porcelain 变更项(XY 两字符状态码,?? = untracked)。
10
10
  */
11
- export function parseGitStatusOutput(output) {
11
+ export function parseGitStatusOutput(output, maxFiles = 20) {
12
12
  const lines = (output || '').split('\n').filter((l) => l.length > 0);
13
13
  if (lines.length === 0 || !lines[0].startsWith('## '))
14
14
  return null;
@@ -34,16 +34,20 @@ export function parseGitStatusOutput(output) {
34
34
  // 仅 ahead 无 behind 时 behind=0(反之亦然)——git 输出只列非零侧
35
35
  }
36
36
  const dirtyFiles = [];
37
- for (let i = 1; i < lines.length && dirtyFiles.length < 20; i++) {
37
+ const limit = maxFiles > 0 ? maxFiles : Number.MAX_SAFE_INTEGER;
38
+ for (let i = 1; i < lines.length && dirtyFiles.length < limit; i++) {
38
39
  const line = lines[i];
39
40
  if (line.length < 4)
40
41
  continue;
41
42
  const status = line.slice(0, 2).trim() || 'M';
42
43
  // 路径可能含空格(git 不加引号场景);rename 形态 `R old -> new` 取 new
43
- let p = line.slice(3);
44
+ let p = line.slice(3).trim();
44
45
  const renameArrow = p.indexOf(' -> ');
45
46
  if (renameArrow >= 0)
46
- p = p.slice(renameArrow + 4);
47
+ p = p.slice(renameArrow + 4).trim();
48
+ if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
49
+ p = p.slice(1, -1);
50
+ }
47
51
  dirtyFiles.push({ status, path: p });
48
52
  }
49
53
  return {
@@ -141,3 +145,111 @@ export function parseGitBranchLines(output) {
141
145
  }
142
146
  return { current, branches };
143
147
  }
148
+ /**
149
+ * 解析 unified diff 输出,统计增删行数与区块数量。
150
+ * 忽略 diff 文件头(--- 和 +++),仅统计真正的改动行(+ 与 -)。
151
+ */
152
+ export function parseUncommittedDiff(rawDiff) {
153
+ let additions = 0;
154
+ let deletions = 0;
155
+ let hunksCount = 0;
156
+ if (!rawDiff || typeof rawDiff !== 'string') {
157
+ return { additions, deletions, hunksCount };
158
+ }
159
+ const lines = rawDiff.split('\n');
160
+ for (const line of lines) {
161
+ if (line.startsWith('@@')) {
162
+ hunksCount++;
163
+ }
164
+ else if (line.startsWith('+') && !line.startsWith('+++')) {
165
+ additions++;
166
+ }
167
+ else if (line.startsWith('-') && !line.startsWith('---')) {
168
+ deletions++;
169
+ }
170
+ }
171
+ return { additions, deletions, hunksCount };
172
+ }
173
+ /**
174
+ * 解析 `git show --stat` 或 `git log --stat` 的统计文本为结构化列表与增删数字。
175
+ * 能够从 `19 ++++++++++++++++++-` 精准推导 additions=18, deletions=1,
176
+ * 提取文件路径与 `X files changed, Y insertions(+), Z deletions(-)` 汇总。
177
+ */
178
+ export function parseGitStatText(statText) {
179
+ if (!statText || typeof statText !== 'string')
180
+ return null;
181
+ const lines = statText.split('\n');
182
+ const files = [];
183
+ let summary = null;
184
+ for (const line of lines) {
185
+ const trimmed = line.trim();
186
+ if (!trimmed)
187
+ continue;
188
+ // 检查汇总行(如 6 files changed, 62 insertions(+), 6 deletions(-))
189
+ const summaryMatch = trimmed.match(/(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/i);
190
+ if (summaryMatch) {
191
+ summary = {
192
+ filesChanged: parseInt(summaryMatch[1], 10),
193
+ insertions: parseInt(summaryMatch[2] || '0', 10),
194
+ deletions: parseInt(summaryMatch[3] || '0', 10),
195
+ };
196
+ continue;
197
+ }
198
+ // 检查文件变更行(包含 | 分隔符)
199
+ const pipeIdx = trimmed.indexOf('|');
200
+ if (pipeIdx > 0) {
201
+ let left = trimmed.slice(0, pipeIdx).trim();
202
+ const right = trimmed.slice(pipeIdx + 1).trim();
203
+ // 去除文件名两端可能的双引号
204
+ if (left.startsWith('"') && left.endsWith('"') && left.length >= 2) {
205
+ left = left.slice(1, -1);
206
+ }
207
+ if (/^Bin\b/i.test(right)) {
208
+ files.push({ path: left, total: 0, additions: 0, deletions: 0, isBinary: true, rawRight: right });
209
+ }
210
+ else {
211
+ const numMatch = right.match(/^(\d+)(.*)$/);
212
+ if (numMatch) {
213
+ const total = parseInt(numMatch[1], 10);
214
+ const graph = numMatch[2].trim();
215
+ let plusCount = 0;
216
+ let minusCount = 0;
217
+ for (let i = 0; i < graph.length; i++) {
218
+ if (graph[i] === '+')
219
+ plusCount++;
220
+ else if (graph[i] === '-')
221
+ minusCount++;
222
+ }
223
+ let additions = 0;
224
+ let deletions = 0;
225
+ if (plusCount === 0 && minusCount === 0) {
226
+ additions = total;
227
+ }
228
+ else if (minusCount === 0) {
229
+ additions = total;
230
+ deletions = 0;
231
+ }
232
+ else if (plusCount === 0) {
233
+ additions = 0;
234
+ deletions = total;
235
+ }
236
+ else {
237
+ if (plusCount + minusCount === total) {
238
+ additions = plusCount;
239
+ deletions = minusCount;
240
+ }
241
+ else {
242
+ const addExact = Math.round((total * plusCount) / (plusCount + minusCount));
243
+ additions = addExact;
244
+ deletions = Math.max(0, total - addExact);
245
+ }
246
+ }
247
+ files.push({ path: left, total, additions, deletions, isBinary: false });
248
+ }
249
+ }
250
+ }
251
+ }
252
+ if (files.length === 0 && !summary)
253
+ return null;
254
+ return { files, summary };
255
+ }
package/lib/executor.d.ts CHANGED
@@ -2,9 +2,9 @@ import type { GhExecutionOptions, GhResult, AuthStatus, RepoMetadata, PluginConf
2
2
  import type { ProxyManager } from './proxy.js';
3
3
  import { parseGhVersion, compareGhVersions } from './executor/version-utils.js';
4
4
  import { spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput } from './executor/process-utils.js';
5
- import { parseGitStatusOutput, parseGitLogLines, parseGitBranchLines } from './executor/git-parser.js';
5
+ import { parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText } from './executor/git-parser.js';
6
6
  import { getPlatformInstallGuide, getGhUpgradeHint } from './executor/platform-guide.js';
7
- export { parseGhVersion, compareGhVersions, spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput, parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, getPlatformInstallGuide, getGhUpgradeHint, };
7
+ export { parseGhVersion, compareGhVersions, spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput, parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText, getPlatformInstallGuide, getGhUpgradeHint, };
8
8
  export declare class GhExecutor {
9
9
  ghPath: string;
10
10
  defaultTimeoutMs: number;
@@ -114,6 +114,31 @@ export declare class GhExecutor {
114
114
  * 符合 global-overview 端点「工作区探测不发网络」的性能红线。
115
115
  */
116
116
  getLocalGitStatus(cwd?: string): Promise<GitLocalStatus | null>;
117
+ /**
118
+ * 分页获取本地未提交文件列表(无网络开销,纯本地毫秒级响应)
119
+ */
120
+ getUncommittedFiles(cwd?: string, limit?: number, skip?: number): Promise<{
121
+ items: Array<{
122
+ status: string;
123
+ path: string;
124
+ }>;
125
+ totalCount: number;
126
+ hasMore: boolean;
127
+ }>;
128
+ /**
129
+ * 获取指定文件的未提交 Git Diff 对比(Unified Diff 格式)。
130
+ * 严格实施路径穿越安全校验;支持已跟踪修改文件、暂存文件与未跟踪新增文件(??)。
131
+ */
132
+ getFileDiff(cwd: string | undefined, relativePath: string): Promise<{
133
+ ok: boolean;
134
+ filePath: string;
135
+ status?: string;
136
+ diff?: string;
137
+ additions?: number;
138
+ deletions?: number;
139
+ isBinary?: boolean;
140
+ error?: string;
141
+ }>;
117
142
  /**
118
143
  * 本地 Git 提交记录(三段一次返回:本地领先 / 远程领先 / 最近提交)。
119
144
  *