dsh-github-flow 0.3.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.
- package/README.en.md +287 -143
- package/README.md +270 -125
- package/docs/UPGRADE-NOTES.md +12 -9
- package/docs/architecture.md +126 -126
- package/lib/api/routes-actions.js +249 -0
- package/lib/api/routes-overview.js +94 -2
- package/lib/cache.d.ts +4 -0
- package/lib/cache.js +14 -0
- package/lib/client.d.ts +14 -1
- package/lib/client.js +4287 -2500
- package/lib/commands/gh.d.ts +6 -1
- package/lib/commands/gh.js +144 -71
- package/lib/executor/git-parser.d.ts +57 -0
- package/lib/executor/git-parser.js +255 -0
- package/lib/executor/platform-guide.d.ts +14 -0
- package/lib/executor/platform-guide.js +40 -0
- package/lib/executor/process-utils.d.ts +51 -0
- package/lib/executor/process-utils.js +162 -0
- package/lib/executor/version-utils.d.ts +12 -0
- package/lib/executor/version-utils.js +27 -0
- package/lib/executor.d.ts +70 -48
- package/lib/executor.js +371 -356
- package/lib/git-watcher.d.ts +36 -0
- package/lib/git-watcher.js +91 -0
- package/lib/main.js +37 -3
- package/lib/renderers/agent-task.d.ts +16 -0
- package/lib/renderers/agent-task.js +107 -0
- package/lib/renderers/cache.d.ts +8 -0
- package/lib/renderers/cache.js +66 -0
- package/lib/renderers/codespace.d.ts +16 -0
- package/lib/renderers/codespace.js +103 -0
- package/lib/renderers/common.d.ts +24 -0
- package/lib/renderers/common.js +88 -0
- package/lib/renderers/discussion.d.ts +16 -0
- package/lib/renderers/discussion.js +101 -0
- package/lib/renderers/gist.d.ts +15 -0
- package/lib/renderers/gist.js +99 -0
- package/lib/renderers/issue.d.ts +4 -0
- package/lib/renderers/issue.js +70 -0
- package/lib/renderers/label.d.ts +4 -0
- package/lib/renderers/label.js +48 -0
- package/lib/renderers/markdown.d.ts +19 -32
- package/lib/renderers/markdown.js +19 -624
- package/lib/renderers/pr.d.ts +4 -0
- package/lib/renderers/pr.js +107 -0
- package/lib/renderers/project.d.ts +20 -0
- package/lib/renderers/project.js +128 -0
- package/lib/renderers/release.d.ts +4 -0
- package/lib/renderers/release.js +74 -0
- package/lib/renderers/repo.d.ts +4 -0
- package/lib/renderers/repo.js +149 -0
- package/lib/renderers/ruleset.d.ts +4 -0
- package/lib/renderers/ruleset.js +74 -0
- package/lib/renderers/run.d.ts +4 -0
- package/lib/renderers/run.js +79 -0
- package/lib/renderers/skill.d.ts +34 -0
- package/lib/renderers/skill.js +241 -0
- package/lib/renderers/status.d.ts +28 -0
- package/lib/renderers/status.js +176 -0
- package/lib/telemetry.d.ts +70 -0
- package/lib/telemetry.js +161 -0
- package/lib/tools/agent-task.d.ts +54 -0
- package/lib/tools/agent-task.js +135 -0
- package/lib/tools/cache.d.ts +64 -0
- package/lib/tools/cache.js +186 -0
- package/lib/tools/codespace.d.ts +51 -0
- package/lib/tools/codespace.js +154 -0
- package/lib/tools/discussion.d.ts +70 -0
- package/lib/tools/discussion.js +195 -0
- package/lib/tools/gist.d.ts +155 -0
- package/lib/tools/gist.js +190 -0
- package/lib/tools/help.js +214 -13
- package/lib/tools/image-utils.d.ts +8 -0
- package/lib/tools/image-utils.js +34 -0
- package/lib/tools/issue.d.ts +8 -1
- package/lib/tools/issue.js +26 -4
- package/lib/tools/label.d.ts +80 -0
- package/lib/tools/label.js +237 -0
- package/lib/tools/pr.d.ts +8 -0
- package/lib/tools/pr.js +48 -3
- package/lib/tools/project.d.ts +80 -0
- package/lib/tools/project.js +241 -0
- package/lib/tools/repo.js +13 -1
- package/lib/tools/ruleset.d.ts +80 -0
- package/lib/tools/ruleset.js +177 -0
- package/lib/tools/skill.d.ts +24 -1
- package/lib/tools/skill.js +115 -5
- package/lib/tools/status.d.ts +40 -0
- package/lib/tools/status.js +58 -0
- package/lib/types.d.ts +44 -0
- package/lib/ui/auth-guide-card.d.ts +4 -0
- package/lib/ui/auth-guide-card.js +161 -0
- package/lib/ui/branch-selector.d.ts +10 -0
- package/lib/ui/branch-selector.js +163 -0
- package/lib/ui/commit-modal.d.ts +8 -0
- package/lib/ui/commit-modal.js +219 -0
- package/lib/ui/help-modal.d.ts +15 -0
- package/lib/ui/help-modal.js +202 -0
- package/lib/ui/icons.d.ts +7 -0
- package/lib/ui/icons.js +53 -0
- package/lib/ui/network-banner.d.ts +10 -0
- package/lib/ui/network-banner.js +54 -0
- package/lib/ui/storage.d.ts +35 -0
- package/lib/ui/storage.js +158 -0
- package/lib/ui/styles.d.ts +6 -0
- package/lib/ui/styles.js +1544 -0
- package/lib/ui/sync-settings-modal.d.ts +11 -0
- package/lib/ui/sync-settings-modal.js +350 -0
- package/lib/ui/sync-status-badge.d.ts +8 -0
- package/lib/ui/sync-status-badge.js +64 -0
- package/lib/ui/upgrade-card.d.ts +7 -0
- package/lib/ui/upgrade-card.js +222 -0
- package/lib/ui/utils.d.ts +45 -0
- package/lib/ui/utils.js +226 -0
- package/lib/ui/workspace-panel.d.ts +4 -0
- package/lib/ui/workspace-panel.js +489 -0
- package/lib/upgrade-manager.d.ts +5 -1
- package/lib/upgrade-manager.js +24 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -2
package/lib/commands/gh.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { GhExecutor } from '../executor.js';
|
|
2
2
|
import type { UpdateChecker } from '../updater.js';
|
|
3
|
-
|
|
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;
|
package/lib/commands/gh.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
117
|
+
description: '查看 GitHub 账号状态、关联仓库与运行时健康度(直接回车即可执行)',
|
|
9
118
|
async handler({ rawInput }) {
|
|
10
|
-
const trimmed = (rawInput || '').trim();
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
124
|
+
text: await buildStatusReport(executor, updateChecker, telemetryCollector),
|
|
45
125
|
};
|
|
46
126
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
131
|
+
text: await buildUpgradeReport(updateChecker),
|
|
82
132
|
};
|
|
83
133
|
}
|
|
84
|
-
|
|
134
|
+
// 二级 help / ? 别名容错
|
|
135
|
+
if (trimmed === 'help' || trimmed === 'h' || trimmed === '?') {
|
|
85
136
|
return {
|
|
86
137
|
kind: 'success',
|
|
87
|
-
text:
|
|
138
|
+
text: buildHelpReport(),
|
|
88
139
|
};
|
|
89
140
|
}
|
|
90
141
|
return {
|
|
91
142
|
kind: 'error',
|
|
92
|
-
text: `未知命令选项: /gh ${
|
|
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
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { GitLocalStatus, GitCommitEntry, GitBranchesResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 解析 `git status -sb --porcelain` 输出为结构化状态。
|
|
4
|
+
* 抽导出纯函数(parseGhVersion 同理):三形态解析是易错边界区,单测钉死。
|
|
5
|
+
*
|
|
6
|
+
* 输出形态(实测):
|
|
7
|
+
* `## master...origin/master [ahead 2, behind 1]` 有 ahead/behind
|
|
8
|
+
* `## master...origin/master` 与远程同步
|
|
9
|
+
* `## master` / `## HEAD (no branch)` 无 upstream(ahead/behind 未知=-1)
|
|
10
|
+
* 后续每行 `<XY> <path>` 为 porcelain 变更项(XY 两字符状态码,?? = untracked)。
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseGitStatusOutput(output: string, maxFiles?: number): GitLocalStatus | null;
|
|
13
|
+
/**
|
|
14
|
+
* 解析 `git log` 输出为提交条目数组。
|
|
15
|
+
* 支持两种形态:
|
|
16
|
+
* 1. 结构化带详情:%x1e 为 commit 间分隔符,%x1f 为字段间分隔符(含 fullSha/email/date/body/stat);
|
|
17
|
+
* 2. 简易管道符:%h|%an|%ar|%s 行格式(兼容旧单测输入)。
|
|
18
|
+
* 空输出返回 []。
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseGitLogLines(output: string): GitCommitEntry[];
|
|
21
|
+
/**
|
|
22
|
+
* 解析 git branch -a --sort=-committerdate 格式化输出。
|
|
23
|
+
* 格式为: %(refname:short)|%(HEAD)|%(upstream:short)|%(committerdate:relative)|%(objectname:short)|%(subject)
|
|
24
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 `git status -sb --porcelain` 输出为结构化状态。
|
|
3
|
+
* 抽导出纯函数(parseGhVersion 同理):三形态解析是易错边界区,单测钉死。
|
|
4
|
+
*
|
|
5
|
+
* 输出形态(实测):
|
|
6
|
+
* `## master...origin/master [ahead 2, behind 1]` 有 ahead/behind
|
|
7
|
+
* `## master...origin/master` 与远程同步
|
|
8
|
+
* `## master` / `## HEAD (no branch)` 无 upstream(ahead/behind 未知=-1)
|
|
9
|
+
* 后续每行 `<XY> <path>` 为 porcelain 变更项(XY 两字符状态码,?? = untracked)。
|
|
10
|
+
*/
|
|
11
|
+
export function parseGitStatusOutput(output, maxFiles = 20) {
|
|
12
|
+
const lines = (output || '').split('\n').filter((l) => l.length > 0);
|
|
13
|
+
if (lines.length === 0 || !lines[0].startsWith('## '))
|
|
14
|
+
return null;
|
|
15
|
+
const branchLine = lines[0].slice(3); // 去掉 '## ' 前缀
|
|
16
|
+
const hasUpstream = branchLine.includes('...');
|
|
17
|
+
const branchPart = hasUpstream ? branchLine.split('...')[0] : branchLine.split(' ')[0];
|
|
18
|
+
let ahead = -1;
|
|
19
|
+
let behind = -1;
|
|
20
|
+
if (hasUpstream) {
|
|
21
|
+
// ahead/behind 段在 upstream 之后(可能与 #id 同现:origin/master #12)。
|
|
22
|
+
// git 输出形态实测:`[ahead 2]` / `[behind 1]` / `[ahead 2, behind 1]`——
|
|
23
|
+
// 单括号内逗号分隔,behind 不必然前邻 '['(正则不可锚定 '[',否则
|
|
24
|
+
// `[ahead 2, behind 1]` 形态的 behind 永远漏解析——单测抓到的真实 Bug)
|
|
25
|
+
const trackingPart = branchLine.split('...')[1] || '';
|
|
26
|
+
ahead = 0;
|
|
27
|
+
behind = 0;
|
|
28
|
+
const aheadMatch = /ahead (\d+)/.exec(trackingPart);
|
|
29
|
+
const behindMatch = /behind (\d+)/.exec(trackingPart);
|
|
30
|
+
if (aheadMatch)
|
|
31
|
+
ahead = Number(aheadMatch[1]);
|
|
32
|
+
if (behindMatch)
|
|
33
|
+
behind = Number(behindMatch[1]);
|
|
34
|
+
// 仅 ahead 无 behind 时 behind=0(反之亦然)——git 输出只列非零侧
|
|
35
|
+
}
|
|
36
|
+
const dirtyFiles = [];
|
|
37
|
+
const limit = maxFiles > 0 ? maxFiles : Number.MAX_SAFE_INTEGER;
|
|
38
|
+
for (let i = 1; i < lines.length && dirtyFiles.length < limit; i++) {
|
|
39
|
+
const line = lines[i];
|
|
40
|
+
if (line.length < 4)
|
|
41
|
+
continue;
|
|
42
|
+
const status = line.slice(0, 2).trim() || 'M';
|
|
43
|
+
// 路径可能含空格(git 不加引号场景);rename 形态 `R old -> new` 取 new
|
|
44
|
+
let p = line.slice(3).trim();
|
|
45
|
+
const renameArrow = p.indexOf(' -> ');
|
|
46
|
+
if (renameArrow >= 0)
|
|
47
|
+
p = p.slice(renameArrow + 4).trim();
|
|
48
|
+
if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
|
|
49
|
+
p = p.slice(1, -1);
|
|
50
|
+
}
|
|
51
|
+
dirtyFiles.push({ status, path: p });
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
branch: branchPart.replace(/\s*\(no branch\)$/, '') || 'HEAD',
|
|
55
|
+
ahead,
|
|
56
|
+
behind,
|
|
57
|
+
dirtyCount: Math.max(0, lines.length - 1),
|
|
58
|
+
hasUpstream,
|
|
59
|
+
dirtyFiles,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 解析 `git log` 输出为提交条目数组。
|
|
64
|
+
* 支持两种形态:
|
|
65
|
+
* 1. 结构化带详情:%x1e 为 commit 间分隔符,%x1f 为字段间分隔符(含 fullSha/email/date/body/stat);
|
|
66
|
+
* 2. 简易管道符:%h|%an|%ar|%s 行格式(兼容旧单测输入)。
|
|
67
|
+
* 空输出返回 []。
|
|
68
|
+
*/
|
|
69
|
+
export function parseGitLogLines(output) {
|
|
70
|
+
const out = (output || '').trim();
|
|
71
|
+
if (!out)
|
|
72
|
+
return [];
|
|
73
|
+
if (out.includes('\x1e') || out.includes('\x1f')) {
|
|
74
|
+
const records = out.split('\x1e').filter((c) => c.trim().length > 0);
|
|
75
|
+
return records.map((chunk) => {
|
|
76
|
+
const parts = chunk.split('\x1f');
|
|
77
|
+
return {
|
|
78
|
+
sha: (parts[0] || '').trim(),
|
|
79
|
+
fullSha: (parts[1] || '').trim() || undefined,
|
|
80
|
+
author: (parts[2] || '').trim(),
|
|
81
|
+
email: (parts[3] || '').trim() || undefined,
|
|
82
|
+
date: (parts[4] || '').trim() || undefined,
|
|
83
|
+
relative: (parts[5] || '').trim(),
|
|
84
|
+
subject: (parts[6] || '').trim(),
|
|
85
|
+
body: (parts[7] || '').trim() || undefined,
|
|
86
|
+
stat: (parts[8] || '').trim() || undefined,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return out.split('\n').filter(Boolean).map((line) => {
|
|
91
|
+
const parts = line.split('|');
|
|
92
|
+
return {
|
|
93
|
+
sha: parts[0] || '',
|
|
94
|
+
author: parts[1] || '',
|
|
95
|
+
relative: parts[2] || '',
|
|
96
|
+
subject: parts.slice(3).join('|'),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 解析 git branch -a --sort=-committerdate 格式化输出。
|
|
102
|
+
* 格式为: %(refname:short)|%(HEAD)|%(upstream:short)|%(committerdate:relative)|%(objectname:short)|%(subject)
|
|
103
|
+
*/
|
|
104
|
+
export function parseGitBranchLines(output) {
|
|
105
|
+
const lines = output.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
106
|
+
let current = '';
|
|
107
|
+
const branches = [];
|
|
108
|
+
const seenNames = new Set();
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
const parts = line.split('|');
|
|
111
|
+
if (parts.length < 5)
|
|
112
|
+
continue;
|
|
113
|
+
const rawRef = parts[0].trim();
|
|
114
|
+
// 过滤掉指向 HEAD 的符号引用(如 origin/HEAD)
|
|
115
|
+
if (rawRef.includes('/HEAD') || rawRef === 'HEAD')
|
|
116
|
+
continue;
|
|
117
|
+
const isCurrent = parts[1].trim() === '*';
|
|
118
|
+
const upstream = parts[2].trim() || undefined;
|
|
119
|
+
const relativeTime = parts[3].trim() || '';
|
|
120
|
+
const shortSha = parts[4].trim() || '';
|
|
121
|
+
const subject = parts.slice(5).join('|').trim();
|
|
122
|
+
// 区分远程分支与本地分支
|
|
123
|
+
const isRemote = rawRef.startsWith('origin/') || rawRef.startsWith('remotes/');
|
|
124
|
+
const cleanName = rawRef.replace(/^remotes\//, '');
|
|
125
|
+
if (seenNames.has(cleanName))
|
|
126
|
+
continue;
|
|
127
|
+
seenNames.add(cleanName);
|
|
128
|
+
if (isCurrent) {
|
|
129
|
+
current = cleanName;
|
|
130
|
+
}
|
|
131
|
+
branches.push({
|
|
132
|
+
name: cleanName,
|
|
133
|
+
isCurrent,
|
|
134
|
+
isRemote,
|
|
135
|
+
upstream,
|
|
136
|
+
shortSha,
|
|
137
|
+
subject,
|
|
138
|
+
relativeTime,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// 若未从 * 解析到当前分支,默认以第一条本地分支兜底
|
|
142
|
+
if (!current && branches.length > 0) {
|
|
143
|
+
const local = branches.find((b) => !b.isRemote);
|
|
144
|
+
current = local ? local.name : branches[0].name;
|
|
145
|
+
}
|
|
146
|
+
return { current, branches };
|
|
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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取当前平台的推荐 gh 安装引导与命令。
|
|
3
|
+
* 覆盖 Windows (winget/scoop)、macOS (Homebrew/MacPorts)、Linux (apt/dnf)。
|
|
4
|
+
*/
|
|
5
|
+
export declare function getPlatformInstallGuide(): {
|
|
6
|
+
command: string;
|
|
7
|
+
altCommand: string;
|
|
8
|
+
downloadUrl: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* gh 升级命令按平台提示(复用三态状态机的平台判断口径)。
|
|
13
|
+
*/
|
|
14
|
+
export declare function getGhUpgradeHint(): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取当前平台的推荐 gh 安装引导与命令。
|
|
3
|
+
* 覆盖 Windows (winget/scoop)、macOS (Homebrew/MacPorts)、Linux (apt/dnf)。
|
|
4
|
+
*/
|
|
5
|
+
export function getPlatformInstallGuide() {
|
|
6
|
+
const p = process.platform;
|
|
7
|
+
if (p === 'win32') {
|
|
8
|
+
return {
|
|
9
|
+
command: 'winget install --id GitHub.cli',
|
|
10
|
+
altCommand: 'scoop install gh',
|
|
11
|
+
downloadUrl: 'https://cli.github.com/',
|
|
12
|
+
description: '推荐使用 Windows Package Manager (winget) 一键安装,或下载官方 .msi 安装包。',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (p === 'darwin') {
|
|
16
|
+
return {
|
|
17
|
+
command: 'brew install gh',
|
|
18
|
+
altCommand: 'port install gh',
|
|
19
|
+
downloadUrl: 'https://cli.github.com/',
|
|
20
|
+
description: '推荐使用 Homebrew 安装 GitHub CLI。',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
command: 'sudo apt install gh',
|
|
25
|
+
altCommand: 'sudo dnf install gh',
|
|
26
|
+
downloadUrl: 'https://cli.github.com/',
|
|
27
|
+
description: '推荐使用系统包管理器安装(Debian/Ubuntu 运行 apt,Fedora/CentOS 运行 dnf)。',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* gh 升级命令按平台提示(复用三态状态机的平台判断口径)。
|
|
32
|
+
*/
|
|
33
|
+
export function getGhUpgradeHint() {
|
|
34
|
+
const p = process.platform;
|
|
35
|
+
if (p === 'win32')
|
|
36
|
+
return '升级命令:`winget upgrade GitHub.cli`(或从 https://cli.github.com/ 下载最新 .msi)。';
|
|
37
|
+
if (p === 'darwin')
|
|
38
|
+
return '升级命令:`brew upgrade gh`(或从 https://cli.github.com/ 下载最新安装包)。';
|
|
39
|
+
return '升级方式:更新系统包(Debian/Ubuntu: `sudo apt update && sudo apt install gh`;Fedora: `sudo dnf upgrade gh`),或见 https://cli.github.com/。';
|
|
40
|
+
}
|