dsh-github-flow 0.1.5 → 0.1.7
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 +12 -7
- package/README.md +12 -7
- package/docs/architecture.md +18 -3
- package/lib/api/routes.js +149 -10
- package/lib/client/panel.js +28 -1
- package/lib/client.js +424 -13
- package/lib/executor.d.ts +8 -5
- package/lib/executor.js +58 -14
- package/lib/tools/api.js +2 -1
- package/lib/tools/issue.js +13 -8
- package/lib/tools/pr.js +14 -9
- package/lib/tools/repo.js +6 -4
- package/lib/tools/run.js +12 -7
- package/lib/types.d.ts +53 -9
- package/lib/workspace.d.ts +6 -3
- package/lib/workspace.js +24 -9
- package/package.json +1 -1
package/lib/executor.js
CHANGED
|
@@ -119,22 +119,39 @@ export class GhExecutor {
|
|
|
119
119
|
/**
|
|
120
120
|
* 智能解析最可靠的工作区物理路径:
|
|
121
121
|
* 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
|
|
122
|
-
* 2.
|
|
123
|
-
* 3.
|
|
124
|
-
* 4.
|
|
122
|
+
* 2. 使用显式传入的 sessionId 或环境变量 DSH_SESSION_ID 精确反查工作区
|
|
123
|
+
* 3. 递归探测:若传入了 explicitCwd(如聚合父目录),自动检查其直系子目录中是否包含 Git 仓库
|
|
124
|
+
* 4. 匹配 ~/.dsh/storages/workspace.json 中最近活跃且存在 .git 的仓库工作区
|
|
125
|
+
* 5. 回退到 process.cwd()
|
|
125
126
|
*/
|
|
126
|
-
resolveWorkspaceCwd(explicitCwd) {
|
|
127
|
-
//
|
|
128
|
-
// 此时继续往下走按会话/最近活跃反查,比盲信传入值更准。
|
|
127
|
+
resolveWorkspaceCwd(explicitCwd, explicitSessionId) {
|
|
128
|
+
// 1. 显式路径优先,若有效且直接包含 .git 目录,则直接使用
|
|
129
129
|
if (explicitCwd && hasGitDir(explicitCwd)) {
|
|
130
130
|
return explicitCwd;
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
// 2. 使用显式传入的 sessionId 或环境变量中的 DSH_SESSION_ID 反查
|
|
133
|
+
const targetSessionId = explicitSessionId || process.env.DSH_SESSION_ID;
|
|
134
|
+
if (targetSessionId) {
|
|
135
|
+
const bySession = findWorkspacePathBySession(targetSessionId);
|
|
136
|
+
if (bySession && hasGitDir(bySession))
|
|
136
137
|
return bySession;
|
|
137
138
|
}
|
|
139
|
+
// 3. 递归探测:若传入了 explicitCwd(如非 Git 的父工作区目录),检查其一级子目录中是否存在 Git 仓库
|
|
140
|
+
if (explicitCwd && fs.existsSync(explicitCwd)) {
|
|
141
|
+
try {
|
|
142
|
+
const entries = fs.readdirSync(explicitCwd, { withFileTypes: true });
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
const subDir = path.join(explicitCwd, entry.name);
|
|
146
|
+
if (hasGitDir(subDir)) {
|
|
147
|
+
return subDir;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
}
|
|
154
|
+
// 4. 从工作区配置中匹配最近活跃且明确具备 .git 的仓库路径
|
|
138
155
|
const recent = findMostRecentWorkspacePath();
|
|
139
156
|
if (recent)
|
|
140
157
|
return recent;
|
|
@@ -145,7 +162,7 @@ export class GhExecutor {
|
|
|
145
162
|
* 采用 execFile 参数数组隔离,彻底杜绝 Shell 字符串拼接注入漏洞
|
|
146
163
|
*/
|
|
147
164
|
async run(args, options = {}) {
|
|
148
|
-
const cwd = this.resolveWorkspaceCwd(options.cwd);
|
|
165
|
+
const cwd = this.resolveWorkspaceCwd(options.cwd, options.sessionId);
|
|
149
166
|
const timeout = options.timeoutMs || this.defaultTimeoutMs;
|
|
150
167
|
try {
|
|
151
168
|
const { stdout, stderr } = await execFileAsync(this.ghPath, args, {
|
|
@@ -222,19 +239,46 @@ export class GhExecutor {
|
|
|
222
239
|
}
|
|
223
240
|
return value;
|
|
224
241
|
}
|
|
242
|
+
/** 生成当前操作系统平台的推荐安装引导与命令 */
|
|
243
|
+
getPlatformInstallGuide() {
|
|
244
|
+
const p = process.platform;
|
|
245
|
+
if (p === 'win32') {
|
|
246
|
+
return {
|
|
247
|
+
command: 'winget install --id GitHub.cli',
|
|
248
|
+
altCommand: 'scoop install gh',
|
|
249
|
+
downloadUrl: 'https://cli.github.com/',
|
|
250
|
+
description: '推荐使用 Windows Package Manager (winget) 一键安装,或下载官方 .msi 安装包。',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (p === 'darwin') {
|
|
254
|
+
return {
|
|
255
|
+
command: 'brew install gh',
|
|
256
|
+
altCommand: 'port install gh',
|
|
257
|
+
downloadUrl: 'https://cli.github.com/',
|
|
258
|
+
description: '推荐使用 Homebrew 安装 GitHub CLI。',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
command: 'sudo apt install gh',
|
|
263
|
+
altCommand: 'sudo dnf install gh',
|
|
264
|
+
downloadUrl: 'https://cli.github.com/',
|
|
265
|
+
description: '推荐使用系统包管理器安装(Debian/Ubuntu 运行 apt,Fedora/CentOS 运行 dnf)。',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
225
268
|
/** 实际执行 `gh auth status` 并解析结果,不含缓存逻辑 */
|
|
226
269
|
async probeAuth(cwd) {
|
|
227
270
|
const res = await this.run(['auth', 'status'], { cwd, timeoutMs: 10_000, rawText: true });
|
|
228
271
|
const raw = (res.rawOutput || '') + '\n' + (res.error || '');
|
|
229
272
|
const loggedIn = raw.includes('Logged in to') || raw.includes('✓');
|
|
230
273
|
// 只要已登录或有常规输出,则必然安装了 gh
|
|
231
|
-
const isNotInstalled = Boolean(res.error && res.error.includes('系统未检测到 GitHub CLI'));
|
|
274
|
+
const isNotInstalled = Boolean(res.error && (res.error.includes('系统未检测到 GitHub CLI') || res.error.includes('ENOENT')));
|
|
232
275
|
const installed = loggedIn || !isNotInstalled;
|
|
233
276
|
if (!installed) {
|
|
234
277
|
return {
|
|
235
278
|
installed: false,
|
|
236
279
|
loggedIn: false,
|
|
237
280
|
platform: process.platform,
|
|
281
|
+
installGuide: this.getPlatformInstallGuide(),
|
|
238
282
|
raw: res.error,
|
|
239
283
|
};
|
|
240
284
|
}
|
|
@@ -312,8 +356,8 @@ export class GhExecutor {
|
|
|
312
356
|
* 获取当前目录(工作区)关联的远程仓库元数据
|
|
313
357
|
* 采用 gh repo view + 本地 git remote 双保险机制,彻底杜绝已配置远程却误判未关联的问题
|
|
314
358
|
*/
|
|
315
|
-
async getRepoMetadata(cwd) {
|
|
316
|
-
const targetCwd = this.resolveWorkspaceCwd(cwd);
|
|
359
|
+
async getRepoMetadata(cwd, sessionId) {
|
|
360
|
+
const targetCwd = this.resolveWorkspaceCwd(cwd, sessionId);
|
|
317
361
|
// 1. 优先尝试由 gh 直接根据当前上下文识别
|
|
318
362
|
const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
319
363
|
if (res.ok && res.data && res.data.nameWithOwner) {
|
package/lib/tools/api.js
CHANGED
|
@@ -54,6 +54,7 @@ export function createApiTool(executor) {
|
|
|
54
54
|
},
|
|
55
55
|
async execute(args, execContext) {
|
|
56
56
|
const cwd = execContext?.cwd || process.cwd();
|
|
57
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
57
58
|
const cmd = ['api', args.endpoint];
|
|
58
59
|
if (args.method && args.method !== 'GET') {
|
|
59
60
|
cmd.push('-X', args.method);
|
|
@@ -81,7 +82,7 @@ export function createApiTool(executor) {
|
|
|
81
82
|
cmd.push('-f', `${k}=${String(v)}`);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
|
-
const res = await executor.run(cmd, { cwd, rawText: args.raw === true });
|
|
85
|
+
const res = await executor.run(cmd, { cwd, sessionId, rawText: args.raw === true });
|
|
85
86
|
if (!res.ok)
|
|
86
87
|
throw new Error(res.error);
|
|
87
88
|
return normalizeOutput(res.data !== undefined ? res.data : res.rawOutput);
|
package/lib/tools/issue.js
CHANGED
|
@@ -64,17 +64,22 @@ export function createIssueTool(executor) {
|
|
|
64
64
|
},
|
|
65
65
|
async execute(args, execContext) {
|
|
66
66
|
const cwd = execContext?.cwd || process.cwd();
|
|
67
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
67
68
|
let repoTarget = args.repo;
|
|
68
69
|
if (!repoTarget) {
|
|
69
|
-
const meta = await executor.getRepoMetadata(cwd);
|
|
70
|
+
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
70
71
|
if (meta?.nameWithOwner) {
|
|
71
72
|
repoTarget = meta.nameWithOwner;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
if (!repoTarget) {
|
|
76
|
+
throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
|
|
77
|
+
}
|
|
78
|
+
const repoArgs = ['-R', repoTarget];
|
|
79
|
+
const runOpts = { cwd, sessionId };
|
|
75
80
|
switch (args.action) {
|
|
76
81
|
case 'list': {
|
|
77
|
-
const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'],
|
|
82
|
+
const res = await executor.run(['issue', 'list', ...repoArgs, '--json', 'number,title,state,author,labels,updatedAt', '-L', '20'], runOpts);
|
|
78
83
|
if (!res.ok)
|
|
79
84
|
throw new Error(res.error);
|
|
80
85
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -82,7 +87,7 @@ export function createIssueTool(executor) {
|
|
|
82
87
|
case 'view': {
|
|
83
88
|
if (!args.issue_number)
|
|
84
89
|
throw new Error('view 操作必须提供 issue_number');
|
|
85
|
-
const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'],
|
|
90
|
+
const res = await executor.run(['issue', 'view', String(args.issue_number), ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,comments,url'], runOpts);
|
|
86
91
|
if (!res.ok)
|
|
87
92
|
throw new Error(res.error);
|
|
88
93
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -95,7 +100,7 @@ export function createIssueTool(executor) {
|
|
|
95
100
|
if (args.assignees && Array.isArray(args.assignees) && args.assignees.length > 0) {
|
|
96
101
|
cmd.push('--assignee', args.assignees.join(','));
|
|
97
102
|
}
|
|
98
|
-
const res = await executor.run(cmd,
|
|
103
|
+
const res = await executor.run(cmd, runOpts);
|
|
99
104
|
if (!res.ok)
|
|
100
105
|
throw new Error(res.error);
|
|
101
106
|
return { message: 'Issue 创建成功', url: res.rawOutput };
|
|
@@ -103,7 +108,7 @@ export function createIssueTool(executor) {
|
|
|
103
108
|
case 'comment': {
|
|
104
109
|
if (!args.issue_number || !args.body)
|
|
105
110
|
throw new Error('comment 必须提供 issue_number 和 body');
|
|
106
|
-
const res = await executor.run(['issue', 'comment', String(args.issue_number), ...repoArgs, '--body', args.body],
|
|
111
|
+
const res = await executor.run(['issue', 'comment', String(args.issue_number), ...repoArgs, '--body', args.body], runOpts);
|
|
107
112
|
if (!res.ok)
|
|
108
113
|
throw new Error(res.error);
|
|
109
114
|
return { message: '评论发布成功', output: res.rawOutput };
|
|
@@ -111,7 +116,7 @@ export function createIssueTool(executor) {
|
|
|
111
116
|
case 'close': {
|
|
112
117
|
if (!args.issue_number)
|
|
113
118
|
throw new Error('close 操作必须提供 issue_number');
|
|
114
|
-
const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs],
|
|
119
|
+
const res = await executor.run(['issue', 'close', String(args.issue_number), ...repoArgs], runOpts);
|
|
115
120
|
if (!res.ok)
|
|
116
121
|
throw new Error(res.error);
|
|
117
122
|
return { message: `Issue #${args.issue_number} 已成功关闭` };
|
|
@@ -119,7 +124,7 @@ export function createIssueTool(executor) {
|
|
|
119
124
|
case 'reopen': {
|
|
120
125
|
if (!args.issue_number)
|
|
121
126
|
throw new Error('reopen 操作必须提供 issue_number');
|
|
122
|
-
const res = await executor.run(['issue', 'reopen', String(args.issue_number), ...repoArgs],
|
|
127
|
+
const res = await executor.run(['issue', 'reopen', String(args.issue_number), ...repoArgs], runOpts);
|
|
123
128
|
if (!res.ok)
|
|
124
129
|
throw new Error(res.error);
|
|
125
130
|
return { message: `Issue #${args.issue_number} 已重新开启` };
|
package/lib/tools/pr.js
CHANGED
|
@@ -84,38 +84,43 @@ export function createPrTool(executor) {
|
|
|
84
84
|
},
|
|
85
85
|
async execute(args, execContext) {
|
|
86
86
|
const cwd = execContext?.cwd || process.cwd();
|
|
87
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
87
88
|
let repoTarget = args.repo;
|
|
88
89
|
if (!repoTarget) {
|
|
89
|
-
const meta = await executor.getRepoMetadata(cwd);
|
|
90
|
+
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
90
91
|
if (meta?.nameWithOwner) {
|
|
91
92
|
repoTarget = meta.nameWithOwner;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
+
if (!repoTarget) {
|
|
96
|
+
throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
|
|
97
|
+
}
|
|
98
|
+
const repoArgs = ['-R', repoTarget];
|
|
99
|
+
const runOpts = { cwd, sessionId };
|
|
95
100
|
switch (args.action) {
|
|
96
101
|
case 'list': {
|
|
97
|
-
const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'],
|
|
102
|
+
const res = await executor.run(['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt', '-L', '20'], runOpts);
|
|
98
103
|
if (!res.ok)
|
|
99
104
|
throw new Error(res.error);
|
|
100
105
|
return normalizeOutput(res.data || res.rawOutput);
|
|
101
106
|
}
|
|
102
107
|
case 'view': {
|
|
103
108
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
104
|
-
const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'],
|
|
109
|
+
const res = await executor.run(['pr', 'view', ...target, ...repoArgs, '--json', 'number,title,body,state,author,labels,assignees,reviewDecision,mergeable,statusCheckRollup,url'], runOpts);
|
|
105
110
|
if (!res.ok)
|
|
106
111
|
throw new Error(res.error);
|
|
107
112
|
return normalizeOutput(res.data || res.rawOutput);
|
|
108
113
|
}
|
|
109
114
|
case 'checks': {
|
|
110
115
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
111
|
-
const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'],
|
|
116
|
+
const res = await executor.run(['pr', 'checks', ...target, ...repoArgs, '--json', 'name,state,conclusion,detailsUrl'], runOpts);
|
|
112
117
|
if (!res.ok)
|
|
113
118
|
throw new Error(res.error);
|
|
114
119
|
return normalizeOutput(res.data || res.rawOutput);
|
|
115
120
|
}
|
|
116
121
|
case 'diff': {
|
|
117
122
|
const target = args.pr_number ? [String(args.pr_number)] : [];
|
|
118
|
-
const res = await executor.run(['pr', 'diff', ...target, ...repoArgs],
|
|
123
|
+
const res = await executor.run(['pr', 'diff', ...target, ...repoArgs], runOpts);
|
|
119
124
|
if (!res.ok)
|
|
120
125
|
throw new Error(res.error);
|
|
121
126
|
return { result: res.rawOutput || '无差异内容' };
|
|
@@ -134,7 +139,7 @@ export function createPrTool(executor) {
|
|
|
134
139
|
const dryRun = args.dry_run === true;
|
|
135
140
|
if (dryRun)
|
|
136
141
|
createCmd.push('--dry-run');
|
|
137
|
-
const res = await executor.run(createCmd,
|
|
142
|
+
const res = await executor.run(createCmd, runOpts);
|
|
138
143
|
if (!res.ok)
|
|
139
144
|
throw new Error(res.error);
|
|
140
145
|
return dryRun
|
|
@@ -153,7 +158,7 @@ export function createPrTool(executor) {
|
|
|
153
158
|
reviewCmd.push('--comment');
|
|
154
159
|
if (args.body)
|
|
155
160
|
reviewCmd.push('--body', args.body);
|
|
156
|
-
const res = await executor.run(reviewCmd,
|
|
161
|
+
const res = await executor.run(reviewCmd, runOpts);
|
|
157
162
|
if (!res.ok)
|
|
158
163
|
throw new Error(res.error);
|
|
159
164
|
return { message: 'Review 提交成功', detail: res.rawOutput };
|
|
@@ -167,7 +172,7 @@ export function createPrTool(executor) {
|
|
|
167
172
|
// 而 gh 默认不删。替用户做掉它违背「可撤销的直接做、不可撤销的先问」。
|
|
168
173
|
if (args.delete_branch === true)
|
|
169
174
|
mergeCmd.push('--delete-branch');
|
|
170
|
-
const res = await executor.run(mergeCmd,
|
|
175
|
+
const res = await executor.run(mergeCmd, runOpts);
|
|
171
176
|
if (!res.ok)
|
|
172
177
|
throw new Error(res.error);
|
|
173
178
|
return { message: 'PR 合并完成', output: res.rawOutput };
|
package/lib/tools/repo.js
CHANGED
|
@@ -50,19 +50,21 @@ export function createRepoTool(executor) {
|
|
|
50
50
|
},
|
|
51
51
|
async execute(args, execContext) {
|
|
52
52
|
const cwd = execContext?.cwd || process.cwd();
|
|
53
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
53
54
|
const limitStr = String(args.limit || 10);
|
|
55
|
+
const runOpts = { cwd, sessionId };
|
|
54
56
|
switch (args.action) {
|
|
55
57
|
case 'view': {
|
|
56
58
|
let repoTarget = args.repo;
|
|
57
59
|
let meta = null;
|
|
58
60
|
if (!repoTarget) {
|
|
59
|
-
meta = await executor.getRepoMetadata(cwd);
|
|
61
|
+
meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
60
62
|
if (meta?.nameWithOwner) {
|
|
61
63
|
repoTarget = meta.nameWithOwner;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
const repoArgs = repoTarget ? [repoTarget] : [];
|
|
65
|
-
const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'],
|
|
67
|
+
const res = await executor.run(['repo', 'view', ...repoArgs, '--json', 'nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,forkCount,url'], runOpts);
|
|
66
68
|
if (!res.ok) {
|
|
67
69
|
// 若执行 gh 失败(如离线或环境限制),但本地已成功识别仓库元数据,进行优雅兜底返回
|
|
68
70
|
if (meta && meta.nameWithOwner) {
|
|
@@ -76,7 +78,7 @@ export function createRepoTool(executor) {
|
|
|
76
78
|
if (!args.query)
|
|
77
79
|
throw new Error('search_code 必须提供 query');
|
|
78
80
|
const repoArgs = args.repo ? ['--repo', args.repo] : [];
|
|
79
|
-
const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr],
|
|
81
|
+
const res = await executor.run(['search', 'code', args.query, ...repoArgs, '--json', 'path,repository,textMatches', '-L', limitStr], runOpts);
|
|
80
82
|
if (!res.ok)
|
|
81
83
|
throw new Error(res.error);
|
|
82
84
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -84,7 +86,7 @@ export function createRepoTool(executor) {
|
|
|
84
86
|
case 'search_repos': {
|
|
85
87
|
if (!args.query)
|
|
86
88
|
throw new Error('search_repos 必须提供 query');
|
|
87
|
-
const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr],
|
|
89
|
+
const res = await executor.run(['search', 'repos', args.query, '--json', 'fullName,description,stargazersCount,updatedAt', '-L', limitStr], runOpts);
|
|
88
90
|
if (!res.ok)
|
|
89
91
|
throw new Error(res.error);
|
|
90
92
|
return normalizeOutput(res.data || res.rawOutput);
|
package/lib/tools/run.js
CHANGED
|
@@ -50,17 +50,22 @@ export function createRunTool(executor) {
|
|
|
50
50
|
},
|
|
51
51
|
async execute(args, execContext) {
|
|
52
52
|
const cwd = execContext?.cwd || process.cwd();
|
|
53
|
+
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
53
54
|
let repoTarget = args.repo;
|
|
54
55
|
if (!repoTarget) {
|
|
55
|
-
const meta = await executor.getRepoMetadata(cwd);
|
|
56
|
+
const meta = await executor.getRepoMetadata(cwd, sessionId);
|
|
56
57
|
if (meta?.nameWithOwner) {
|
|
57
58
|
repoTarget = meta.nameWithOwner;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
if (!repoTarget) {
|
|
62
|
+
throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
|
|
63
|
+
}
|
|
64
|
+
const repoArgs = ['-R', repoTarget];
|
|
65
|
+
const runOpts = { cwd, sessionId };
|
|
61
66
|
switch (args.action) {
|
|
62
67
|
case 'list': {
|
|
63
|
-
const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)],
|
|
68
|
+
const res = await executor.run(['run', 'list', ...repoArgs, '--json', 'databaseId,name,status,conclusion,event,headBranch,createdAt', '-L', String(args.limit || 10)], runOpts);
|
|
64
69
|
if (!res.ok)
|
|
65
70
|
throw new Error(res.error);
|
|
66
71
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -68,7 +73,7 @@ export function createRunTool(executor) {
|
|
|
68
73
|
case 'view': {
|
|
69
74
|
if (!args.run_id)
|
|
70
75
|
throw new Error('view 操作必须提供 run_id');
|
|
71
|
-
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'],
|
|
76
|
+
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--json', 'databaseId,name,status,conclusion,jobs,url'], runOpts);
|
|
72
77
|
if (!res.ok)
|
|
73
78
|
throw new Error(res.error);
|
|
74
79
|
return normalizeOutput(res.data || res.rawOutput);
|
|
@@ -78,7 +83,7 @@ export function createRunTool(executor) {
|
|
|
78
83
|
throw new Error('log_failed 操作必须提供 run_id');
|
|
79
84
|
// gh 原生支持仅提取失败的日志!对 Agent 故障诊断价值极高
|
|
80
85
|
const res = await executor.run(['run', 'view', String(args.run_id), ...repoArgs, '--log-failed'], {
|
|
81
|
-
|
|
86
|
+
...runOpts,
|
|
82
87
|
timeoutMs: 60_000,
|
|
83
88
|
rawText: true,
|
|
84
89
|
});
|
|
@@ -89,7 +94,7 @@ export function createRunTool(executor) {
|
|
|
89
94
|
case 'rerun': {
|
|
90
95
|
if (!args.run_id)
|
|
91
96
|
throw new Error('rerun 操作必须提供 run_id');
|
|
92
|
-
const res = await executor.run(['run', 'rerun', String(args.run_id), ...repoArgs, '--failed'],
|
|
97
|
+
const res = await executor.run(['run', 'rerun', String(args.run_id), ...repoArgs, '--failed'], runOpts);
|
|
93
98
|
if (!res.ok)
|
|
94
99
|
throw new Error(res.error);
|
|
95
100
|
return { message: '已成功重新触发失败任务', output: res.rawOutput };
|
|
@@ -97,7 +102,7 @@ export function createRunTool(executor) {
|
|
|
97
102
|
case 'cancel': {
|
|
98
103
|
if (!args.run_id)
|
|
99
104
|
throw new Error('cancel 操作必须提供 run_id');
|
|
100
|
-
const res = await executor.run(['run', 'cancel', String(args.run_id), ...repoArgs],
|
|
105
|
+
const res = await executor.run(['run', 'cancel', String(args.run_id), ...repoArgs], runOpts);
|
|
101
106
|
if (!res.ok)
|
|
102
107
|
throw new Error(res.error);
|
|
103
108
|
return { message: '已取消该工作流任务', output: res.rawOutput };
|
package/lib/types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface PluginConfig {
|
|
|
18
18
|
export interface GhExecutionOptions {
|
|
19
19
|
/** 命令执行的工作目录,优先使用当前 Agent 会话所在的 Workspace 物理路径 */
|
|
20
20
|
cwd?: string;
|
|
21
|
+
/** 当前调用的会话 ID,用于精确关联物理工作区路径 */
|
|
22
|
+
sessionId?: string;
|
|
21
23
|
/** 进程超时限制(毫秒),默认 30,000ms */
|
|
22
24
|
timeoutMs?: number;
|
|
23
25
|
/** 附加环境变量 */
|
|
@@ -33,6 +35,16 @@ export interface GhResult<T = unknown> {
|
|
|
33
35
|
/** 是否因超出安全阈值而被截断 */
|
|
34
36
|
truncated?: boolean;
|
|
35
37
|
}
|
|
38
|
+
export interface AuthInstallGuide {
|
|
39
|
+
/** 推荐包管理器命令,例如 Windows 下 "winget install --id GitHub.cli" */
|
|
40
|
+
command: string;
|
|
41
|
+
/** 备选命令(如 choco/scoop/brew) */
|
|
42
|
+
altCommand?: string;
|
|
43
|
+
/** 官方下载与安装文档链接 */
|
|
44
|
+
downloadUrl: string;
|
|
45
|
+
/** 简明指引提示 */
|
|
46
|
+
description?: string;
|
|
47
|
+
}
|
|
36
48
|
export interface AuthStatus {
|
|
37
49
|
installed: boolean;
|
|
38
50
|
loggedIn: boolean;
|
|
@@ -42,6 +54,7 @@ export interface AuthStatus {
|
|
|
42
54
|
activeAccount?: boolean;
|
|
43
55
|
protocol?: string;
|
|
44
56
|
scopes?: string[];
|
|
57
|
+
installGuide?: AuthInstallGuide;
|
|
45
58
|
raw?: string;
|
|
46
59
|
}
|
|
47
60
|
export interface RepoMetadata {
|
|
@@ -75,6 +88,19 @@ export interface PrItem {
|
|
|
75
88
|
url: string;
|
|
76
89
|
updatedAt: string;
|
|
77
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Issue 分类维度契约:
|
|
93
|
+
* - reported: 用户提起的(他人向我名下项目或本仓库提交的 Issue 反馈)
|
|
94
|
+
* - assigned: 分配给我的(指派给当前账号的待办任务,跨仓库通用)
|
|
95
|
+
* - created: 我创建的(由当前账号亲自发起的 Issue)
|
|
96
|
+
*/
|
|
97
|
+
export type IssueCategory = 'reported' | 'assigned' | 'created';
|
|
98
|
+
export interface IssueStats {
|
|
99
|
+
total: number;
|
|
100
|
+
reported: number;
|
|
101
|
+
assigned: number;
|
|
102
|
+
created: number;
|
|
103
|
+
}
|
|
78
104
|
export interface IssueItem {
|
|
79
105
|
number: number;
|
|
80
106
|
title: string;
|
|
@@ -91,6 +117,8 @@ export interface IssueItem {
|
|
|
91
117
|
}>;
|
|
92
118
|
url: string;
|
|
93
119
|
updatedAt: string;
|
|
120
|
+
/** 命中的所有身份分类 */
|
|
121
|
+
categories?: IssueCategory[];
|
|
94
122
|
}
|
|
95
123
|
export interface WorkflowRunItem {
|
|
96
124
|
databaseId: number;
|
|
@@ -107,6 +135,7 @@ export interface GitHubOverviewData {
|
|
|
107
135
|
repo?: RepoMetadata;
|
|
108
136
|
pullRequests: PrItem[];
|
|
109
137
|
issues: IssueItem[];
|
|
138
|
+
issueStats?: IssueStats;
|
|
110
139
|
runs: WorkflowRunItem[];
|
|
111
140
|
lastUpdated: string;
|
|
112
141
|
error?: string;
|
|
@@ -129,6 +158,28 @@ export interface WorkspaceMatrixItem {
|
|
|
129
158
|
repo?: RepoMetadata;
|
|
130
159
|
sessionCount: number;
|
|
131
160
|
}
|
|
161
|
+
export interface GlobalIssueItem {
|
|
162
|
+
number: number;
|
|
163
|
+
title: string;
|
|
164
|
+
repository: {
|
|
165
|
+
name?: string;
|
|
166
|
+
nameWithOwner: string;
|
|
167
|
+
};
|
|
168
|
+
author?: {
|
|
169
|
+
login: string;
|
|
170
|
+
};
|
|
171
|
+
assignees?: Array<{
|
|
172
|
+
login: string;
|
|
173
|
+
}>;
|
|
174
|
+
labels?: Array<{
|
|
175
|
+
name: string;
|
|
176
|
+
color: string;
|
|
177
|
+
}>;
|
|
178
|
+
url: string;
|
|
179
|
+
updatedAt: string;
|
|
180
|
+
state?: 'OPEN' | 'CLOSED';
|
|
181
|
+
categories: IssueCategory[];
|
|
182
|
+
}
|
|
132
183
|
export interface GlobalOverviewData {
|
|
133
184
|
auth: AuthStatus;
|
|
134
185
|
userRepos: UserRepoItem[];
|
|
@@ -142,15 +193,8 @@ export interface GlobalOverviewData {
|
|
|
142
193
|
url: string;
|
|
143
194
|
updatedAt: string;
|
|
144
195
|
}>;
|
|
145
|
-
myIssues:
|
|
146
|
-
|
|
147
|
-
title: string;
|
|
148
|
-
repository: {
|
|
149
|
-
nameWithOwner: string;
|
|
150
|
-
};
|
|
151
|
-
url: string;
|
|
152
|
-
updatedAt: string;
|
|
153
|
-
}>;
|
|
196
|
+
myIssues: GlobalIssueItem[];
|
|
197
|
+
issueStats?: IssueStats;
|
|
154
198
|
lastUpdated: string;
|
|
155
199
|
error?: string;
|
|
156
200
|
/** 工作区矩阵读取失败的原因。区分「没有工作区」与「读不到工作区」,避免把故障显示成空状态 */
|
package/lib/workspace.d.ts
CHANGED
|
@@ -27,9 +27,12 @@ export declare function tryReadWorkspaces(): {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function findWorkspacePathBySession(sessionId: string): string | undefined;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* 匹配最近活跃且包含 Git 仓库的工作区路径。
|
|
31
|
+
*
|
|
32
|
+
* 为什么必须优先检查 hasGitDir:workspace.json 中可能记录了父级纯文件夹工作区(例如工程聚合根目录,本身非 Git 仓库),
|
|
33
|
+
* 当它被打开过导致 updatedAt 较新时,若不检查 .git 就会误把该父目录当成当前仓库执行路径,
|
|
34
|
+
* 进而导致后续 gh 命令报出 "fatal: not a git repository"。
|
|
35
|
+
* 策略:优先选取最近活跃且明确具备 .git 的工作区;若均无 .git 再优雅回退到任意普通工作区。
|
|
33
36
|
*/
|
|
34
37
|
export declare function findMostRecentWorkspacePath(): string | undefined;
|
|
35
38
|
export declare function hasGitDir(dir: string): boolean;
|
package/lib/workspace.js
CHANGED
|
@@ -79,24 +79,39 @@ export function findWorkspacePathBySession(sessionId) {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* 匹配最近活跃且包含 Git 仓库的工作区路径。
|
|
83
|
+
*
|
|
84
|
+
* 为什么必须优先检查 hasGitDir:workspace.json 中可能记录了父级纯文件夹工作区(例如工程聚合根目录,本身非 Git 仓库),
|
|
85
|
+
* 当它被打开过导致 updatedAt 较新时,若不检查 .git 就会误把该父目录当成当前仓库执行路径,
|
|
86
|
+
* 进而导致后续 gh 命令报出 "fatal: not a git repository"。
|
|
87
|
+
* 策略:优先选取最近活跃且明确具备 .git 的工作区;若均无 .git 再优雅回退到任意普通工作区。
|
|
85
88
|
*/
|
|
86
89
|
export function findMostRecentWorkspacePath() {
|
|
87
90
|
try {
|
|
88
91
|
const { workspaces } = tryReadWorkspaces();
|
|
89
|
-
let
|
|
90
|
-
let
|
|
92
|
+
let bestGitWs = null;
|
|
93
|
+
let bestGitAt = -1;
|
|
94
|
+
let fallbackWs = null;
|
|
95
|
+
let fallbackAt = -1;
|
|
91
96
|
for (const ws of workspaces) {
|
|
97
|
+
if (!ws.path || !fs.existsSync(ws.path))
|
|
98
|
+
continue;
|
|
92
99
|
const at = Date.parse(ws.updatedAt || '');
|
|
93
100
|
const score = Number.isFinite(at) ? at : 0;
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if (hasGitDir(ws.path)) {
|
|
102
|
+
if (score > bestGitAt) {
|
|
103
|
+
bestGitWs = ws;
|
|
104
|
+
bestGitAt = score;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (score > fallbackAt) {
|
|
109
|
+
fallbackWs = ws;
|
|
110
|
+
fallbackAt = score;
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
|
-
return
|
|
114
|
+
return bestGitWs?.path || fallbackWs?.path;
|
|
100
115
|
}
|
|
101
116
|
catch {
|
|
102
117
|
return undefined;
|
package/package.json
CHANGED