dsh-github-flow 0.2.1 → 0.4.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 (102) hide show
  1. package/README.en.md +234 -130
  2. package/README.md +217 -112
  3. package/docs/UPGRADE-NOTES.md +15 -9
  4. package/docs/architecture.md +120 -108
  5. package/docs/images/2.png +0 -0
  6. package/docs/npm-publish-guide.md +12 -1
  7. package/lib/api/routes-actions.js +117 -0
  8. package/lib/api/routes-overview.js +198 -3
  9. package/lib/cache.d.ts +4 -0
  10. package/lib/cache.js +14 -0
  11. package/lib/client.d.ts +13 -1
  12. package/lib/client.js +3162 -2162
  13. package/lib/executor/git-parser.d.ts +25 -0
  14. package/lib/executor/git-parser.js +143 -0
  15. package/lib/executor/platform-guide.d.ts +14 -0
  16. package/lib/executor/platform-guide.js +40 -0
  17. package/lib/executor/process-utils.d.ts +51 -0
  18. package/lib/executor/process-utils.js +162 -0
  19. package/lib/executor/version-utils.d.ts +12 -0
  20. package/lib/executor/version-utils.js +27 -0
  21. package/lib/executor.d.ts +131 -17
  22. package/lib/executor.js +411 -223
  23. package/lib/git-watcher.d.ts +36 -0
  24. package/lib/git-watcher.js +91 -0
  25. package/lib/main.js +11 -0
  26. package/lib/renderers/cache.d.ts +8 -0
  27. package/lib/renderers/cache.js +66 -0
  28. package/lib/renderers/common.d.ts +24 -0
  29. package/lib/renderers/common.js +88 -0
  30. package/lib/renderers/gist.d.ts +15 -0
  31. package/lib/renderers/gist.js +99 -0
  32. package/lib/renderers/issue.d.ts +4 -0
  33. package/lib/renderers/issue.js +70 -0
  34. package/lib/renderers/label.d.ts +4 -0
  35. package/lib/renderers/label.js +48 -0
  36. package/lib/renderers/markdown.d.ts +15 -26
  37. package/lib/renderers/markdown.js +15 -517
  38. package/lib/renderers/pr.d.ts +4 -0
  39. package/lib/renderers/pr.js +107 -0
  40. package/lib/renderers/release.d.ts +4 -0
  41. package/lib/renderers/release.js +74 -0
  42. package/lib/renderers/repo.d.ts +4 -0
  43. package/lib/renderers/repo.js +149 -0
  44. package/lib/renderers/ruleset.d.ts +4 -0
  45. package/lib/renderers/ruleset.js +74 -0
  46. package/lib/renderers/run.d.ts +4 -0
  47. package/lib/renderers/run.js +79 -0
  48. package/lib/renderers/skill.d.ts +4 -0
  49. package/lib/renderers/skill.js +61 -0
  50. package/lib/renderers/status.d.ts +23 -0
  51. package/lib/renderers/status.js +124 -0
  52. package/lib/tools/cache.d.ts +64 -0
  53. package/lib/tools/cache.js +186 -0
  54. package/lib/tools/gist.d.ts +155 -0
  55. package/lib/tools/gist.js +190 -0
  56. package/lib/tools/help.js +233 -11
  57. package/lib/tools/image-utils.d.ts +8 -0
  58. package/lib/tools/image-utils.js +34 -0
  59. package/lib/tools/issue.d.ts +17 -1
  60. package/lib/tools/issue.js +114 -8
  61. package/lib/tools/label.d.ts +80 -0
  62. package/lib/tools/label.js +237 -0
  63. package/lib/tools/pr.d.ts +12 -0
  64. package/lib/tools/pr.js +96 -4
  65. package/lib/tools/repo.d.ts +8 -0
  66. package/lib/tools/repo.js +127 -3
  67. package/lib/tools/ruleset.d.ts +80 -0
  68. package/lib/tools/ruleset.js +177 -0
  69. package/lib/tools/run.d.ts +8 -0
  70. package/lib/tools/run.js +104 -3
  71. package/lib/tools/secret.d.ts +5 -0
  72. package/lib/tools/secret.js +108 -23
  73. package/lib/tools/status.d.ts +39 -0
  74. package/lib/tools/status.js +53 -0
  75. package/lib/types.d.ts +112 -0
  76. package/lib/ui/auth-guide-card.d.ts +4 -0
  77. package/lib/ui/auth-guide-card.js +161 -0
  78. package/lib/ui/branch-selector.d.ts +10 -0
  79. package/lib/ui/branch-selector.js +163 -0
  80. package/lib/ui/commit-modal.d.ts +8 -0
  81. package/lib/ui/commit-modal.js +89 -0
  82. package/lib/ui/icons.d.ts +7 -0
  83. package/lib/ui/icons.js +53 -0
  84. package/lib/ui/network-banner.d.ts +10 -0
  85. package/lib/ui/network-banner.js +54 -0
  86. package/lib/ui/storage.d.ts +26 -0
  87. package/lib/ui/storage.js +134 -0
  88. package/lib/ui/styles.d.ts +6 -0
  89. package/lib/ui/styles.js +1285 -0
  90. package/lib/ui/sync-settings-modal.d.ts +11 -0
  91. package/lib/ui/sync-settings-modal.js +350 -0
  92. package/lib/ui/sync-status-badge.d.ts +8 -0
  93. package/lib/ui/sync-status-badge.js +64 -0
  94. package/lib/ui/upgrade-card.d.ts +7 -0
  95. package/lib/ui/upgrade-card.js +197 -0
  96. package/lib/ui/utils.d.ts +10 -0
  97. package/lib/ui/utils.js +66 -0
  98. package/lib/ui/workspace-panel.d.ts +4 -0
  99. package/lib/ui/workspace-panel.js +464 -0
  100. package/lib/version.d.ts +1 -1
  101. package/lib/version.js +1 -1
  102. package/package.json +1 -1
package/lib/tools/repo.js CHANGED
@@ -18,8 +18,8 @@ export function createRepoTool(executor) {
18
18
  properties: {
19
19
  action: {
20
20
  type: 'string',
21
- enum: ['view', 'readme', 'search_code', 'search_repos', 'sync'],
22
- description: '仓库操作类型。readme 读取 README 原文并附带图片清单(可选 image_urls 下载指定图片,不传下载全部截图;默认过滤徽章小图标)。',
21
+ enum: ['view', 'readme', 'search_code', 'search_repos', 'search_issues', 'search_prs', 'search_commits', 'read_file', 'read_dir', 'sync'],
22
+ description: '仓库操作类型。readme 读取 README 原文并附带图片清单(可选 image_urls 下载指定图片,不传下载全部截图;默认过滤徽章小图标)。read_file/read_dir 免 clone 读取任意路径文件与目录(gh ≥2.95,低版本自动降级 API 链路)。',
23
23
  },
24
24
  repo: {
25
25
  type: 'string',
@@ -36,7 +36,15 @@ export function createRepoTool(executor) {
36
36
  },
37
37
  query: {
38
38
  type: 'string',
39
- description: '检索关键词(search_code 或 search_repos 时使用)。',
39
+ description: '检索关键词(search_* 动作使用)。限定符直接写进 query(gh 惯例):如 "repo:owner/name state:open label:bug"。注意 search_code/search_commits 必须包含搜索文本,纯限定符查询(如仅 "repo:x")会被 gh 拒绝。',
40
+ },
41
+ path: {
42
+ type: 'string',
43
+ description: 'read_file 必填 / read_dir 可选(默认仓库根目录):目标文件或目录路径(如 "src/tools/pr.ts"、".github/workflows")。',
44
+ },
45
+ ref: {
46
+ type: 'string',
47
+ description: 'read_file/read_dir 可选:从指定分支/tag/commit 读取(默认默认分支)。',
40
48
  },
41
49
  limit: {
42
50
  type: 'number',
@@ -165,6 +173,122 @@ export function createRepoTool(executor) {
165
173
  throw new Error(res.error);
166
174
  return normalizeOutput(res.data || res.rawOutput);
167
175
  }
176
+ case 'search_issues':
177
+ case 'search_prs': {
178
+ if (!args.query)
179
+ throw new Error(`${args.action} 必须提供 query`);
180
+ // 限定符(repo:/state:/label: 等)直接写进 query 本体(gh search 惯例),
181
+ // 不做独立参数——参数爆炸 + 与 gh 手册心智模型分叉(D3 决策)。
182
+ // prs 比 issues 多 isDraft 字段(实测合法集 18 vs 17)。
183
+ const repoArgs = args.repo ? ['--repo', args.repo] : [];
184
+ const fields = args.action === 'search_prs'
185
+ ? 'number,title,state,author,labels,isDraft,updatedAt,url'
186
+ : 'number,title,state,author,labels,updatedAt,url';
187
+ const res = await executor.run(['search', args.action === 'search_prs' ? 'prs' : 'issues', args.query, ...repoArgs, '--json', fields, '-L', limitStr], runOpts);
188
+ if (!res.ok)
189
+ throw new Error(res.error);
190
+ return normalizeOutput(res.data || res.rawOutput);
191
+ }
192
+ case 'search_commits': {
193
+ if (!args.query)
194
+ throw new Error('search_commits 必须提供 query');
195
+ const repoArgs = args.repo ? ['--repo', args.repo] : [];
196
+ const res = await executor.run(['search', 'commits', args.query, ...repoArgs, '--json', 'sha,commit,author,repository,url', '-L', limitStr], runOpts);
197
+ if (!res.ok)
198
+ throw new Error(res.error);
199
+ return normalizeOutput(res.data || res.rawOutput);
200
+ }
201
+ case 'read_file': {
202
+ if (!args.path)
203
+ throw new Error('read_file 必须提供 path(目标文件路径,如 "package.json")');
204
+ let repoTarget = args.repo;
205
+ if (!repoTarget) {
206
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
207
+ if (meta?.nameWithOwner)
208
+ repoTarget = meta.nameWithOwner;
209
+ }
210
+ if (!repoTarget)
211
+ throw new Error('read_file 必须提供 repo [owner/repo],或在关联 Git 远程的工作区运行');
212
+ const [owner, repoName] = repoTarget.split('/');
213
+ if (!owner || !repoName)
214
+ throw new Error(`read_file 需要 [owner/repo] 形式的仓库名(实际: ${repoTarget})`);
215
+ // 主链路走管道直出(无 --json):gh 把原始内容写 stdout,executor 的
216
+ // 24KB 截断与 UTF-8 处理免费复用;--json 模式 content 是 base64,
217
+ // 中段截断即废且需解码 dance,弃用。
218
+ const refArgs = args.ref ? ['--ref', args.ref] : [];
219
+ const res = await executor.runGuarded(['repo', 'read-file', args.path, '-R', repoTarget, ...refArgs], {
220
+ ...runOpts,
221
+ rawText: true,
222
+ timeoutMs: 20_000,
223
+ minVersion: '2.95.0',
224
+ capability: 'gh repo read-file',
225
+ // 降级链:contents 端点 raw Accept(readme 同款技术),与主链路输出同构。
226
+ // ref 必须走 URL query 而非 -f:gh api 在带 -f 时会把 GET 隐式改写为
227
+ // POST(api.ts:66 同坑),contents 端点 POST 直接 404。
228
+ fallback: async () => {
229
+ const encoded = args.path.split('/').map(encodeURIComponent).join('/');
230
+ const refQuery = args.ref ? `?ref=${encodeURIComponent(args.ref)}` : '';
231
+ return executor.run(['api', `repos/${owner}/${repoName}/contents/${encoded}${refQuery}`, '-H', 'Accept: application/vnd.github.raw'], { cwd, sessionId, rawText: true, timeoutMs: 20_000 });
232
+ },
233
+ });
234
+ if (!res.ok)
235
+ throw new Error(res.error);
236
+ const content = res.rawOutput || '';
237
+ // 二进制防线:NUL 字节是文本内容的可靠否定信号(UTF-8 合法文本不含 NUL)。
238
+ // 不做 base64 直返——那会撑爆 Token 预算,引导 github_api binary channel。
239
+ if (content.includes('\0')) {
240
+ throw new Error(`「${args.path}」是二进制文件(检测到 NUL 字节),不适合文本读取。请改用 github_api(endpoint: 'repos/${owner}/${repoName}/contents/${args.path.split('/').map(encodeURIComponent).join('/')}', raw: true, binary 场景自行落盘) 获取字节流。`);
241
+ }
242
+ return { path: args.path, ref: args.ref, content, size: content.length, ...(res.degraded ? { degraded: res.degraded, degradedReason: res.degradedReason } : {}) };
243
+ }
244
+ case 'read_dir': {
245
+ let repoTarget = args.repo;
246
+ if (!repoTarget) {
247
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
248
+ if (meta?.nameWithOwner)
249
+ repoTarget = meta.nameWithOwner;
250
+ }
251
+ if (!repoTarget)
252
+ throw new Error('read_dir 必须提供 repo [owner/repo],或在关联 Git 远程的工作区运行');
253
+ const [owner, repoName] = repoTarget.split('/');
254
+ if (!owner || !repoName)
255
+ throw new Error(`read_dir 需要 [owner/repo] 形式的仓库名(实际: ${repoTarget})`);
256
+ const refArgs = args.ref ? ['--ref', args.ref] : [];
257
+ const dirPath = args.path || '';
258
+ const res = await executor.runGuarded(['repo', 'read-dir', ...(dirPath ? [dirPath] : []), '-R', repoTarget, ...refArgs], {
259
+ ...runOpts,
260
+ rawText: true,
261
+ timeoutMs: 20_000,
262
+ minVersion: '2.95.0',
263
+ capability: 'gh repo read-dir',
264
+ // 降级链:contents 端点返回 JSON 数组(dir 为含 type 字段的对象)。
265
+ // ref 同样走 URL query(-f 会把 GET 改写成 POST,见 read_file 注释)
266
+ fallback: async () => {
267
+ const encoded = dirPath.split('/').filter(Boolean).map(encodeURIComponent).join('/');
268
+ const refQuery = args.ref ? `?ref=${encodeURIComponent(args.ref)}` : '';
269
+ return executor.run(['api', `repos/${owner}/${repoName}/contents/${encoded}${refQuery}`], { cwd, sessionId, timeoutMs: 20_000 });
270
+ },
271
+ });
272
+ if (!res.ok)
273
+ throw new Error(res.error);
274
+ // 双链路归一为 items 数组:
275
+ // 主链路是 TSV 四列(type/name/mode/size),降级链 data 是 contents JSON 数组
276
+ let items;
277
+ if (res.degraded && Array.isArray(res.data)) {
278
+ items = res.data.map((e) => ({ type: String(e.type || 'file'), name: String(e.name || ''), mode: '-', size: typeof e.size === 'number' ? e.size : undefined }));
279
+ }
280
+ else {
281
+ const tsv = (res.rawOutput || '').trim();
282
+ if (!tsv) {
283
+ return { message: `「${dirPath || '/'}」是空目录或不存在`, items: [], count: 0, ...(res.degraded ? { degraded: res.degraded, degradedReason: res.degradedReason } : {}) };
284
+ }
285
+ items = tsv.split('\n').map((line) => {
286
+ const cols = line.split('\t');
287
+ return { type: cols[0] || 'file', name: cols[1] || '', mode: cols[2] || '-', size: Number(cols[3]) || 0 };
288
+ });
289
+ }
290
+ return { path: dirPath || '/', items, count: items.length, ...(res.degraded ? { degraded: res.degraded, degradedReason: res.degradedReason } : {}) };
291
+ }
168
292
  case 'sync': {
169
293
  let repoTarget = args.repo;
170
294
  if (!repoTarget) {
@@ -0,0 +1,80 @@
1
+ import type { GhExecutor } from '../executor.js';
2
+ export interface ParsedRule {
3
+ name: string;
4
+ params: string;
5
+ source: string;
6
+ }
7
+ export interface ParsedRulesetCheck {
8
+ ruleCount: number;
9
+ branch: string;
10
+ repo: string;
11
+ rules: ParsedRule[];
12
+ rawText: string;
13
+ }
14
+ /**
15
+ * 解析 gh ruleset check 的纯文本输出,提取适用规则数、分支、仓库以及具体规则详情
16
+ */
17
+ export declare function parseRulesetCheckOutput(output: string): ParsedRulesetCheck;
18
+ export declare function createRulesetTool(executor: GhExecutor): {
19
+ name: string;
20
+ description: string;
21
+ parameters: {
22
+ type: string;
23
+ properties: {
24
+ action: {
25
+ type: string;
26
+ enum: string[];
27
+ description: string;
28
+ };
29
+ repo: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ branch: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ default_branch: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ ruleset_id: {
42
+ type: string;
43
+ description: string;
44
+ };
45
+ org: {
46
+ type: string;
47
+ description: string;
48
+ };
49
+ no_parents: {
50
+ type: string;
51
+ description: string;
52
+ };
53
+ limit: {
54
+ type: string;
55
+ description: string;
56
+ };
57
+ };
58
+ required: string[];
59
+ };
60
+ output: {
61
+ schema: {
62
+ type: string;
63
+ };
64
+ render(args: any, value: any): {
65
+ type: string;
66
+ text: string;
67
+ }[];
68
+ };
69
+ execute(args: any, execContext: any): Promise<ParsedRulesetCheck | {
70
+ rawText: string;
71
+ org: any;
72
+ repo: string;
73
+ ruleset_id?: undefined;
74
+ } | {
75
+ ruleset_id: string;
76
+ rawText: string;
77
+ org: any;
78
+ repo: string;
79
+ }>;
80
+ };
@@ -0,0 +1,177 @@
1
+ import { renderRulesetMarkdown } from '../renderers/markdown.js';
2
+ /**
3
+ * 解析 gh ruleset check 的纯文本输出,提取适用规则数、分支、仓库以及具体规则详情
4
+ */
5
+ export function parseRulesetCheckOutput(output) {
6
+ const trimmed = (output || '').trim();
7
+ const result = {
8
+ ruleCount: 0,
9
+ branch: '',
10
+ repo: '',
11
+ rules: [],
12
+ rawText: trimmed,
13
+ };
14
+ if (!trimmed)
15
+ return result;
16
+ // 1. 匹配首行: "N rules apply to branch <branch> in repo <repo>"
17
+ const firstLineMatch = trimmed.match(/^(\d+)\s+rules?\s+apply\s+to\s+branch\s+(\S+)\s+in\s+repo\s+(\S+)/i);
18
+ if (firstLineMatch) {
19
+ result.ruleCount = parseInt(firstLineMatch[1], 10);
20
+ result.branch = firstLineMatch[2];
21
+ result.repo = firstLineMatch[3];
22
+ }
23
+ // 2. 匹配具体规则明细项:形如 "- <name>: <params>\n (configured in <source>)"
24
+ const rulePattern = /-\s*([a-zA-Z0-9_-]+):\s*([^\n\r]+)(?:[\r\n]+\s*\((configured in [^)]+)\))?/g;
25
+ let match;
26
+ while ((match = rulePattern.exec(trimmed)) !== null) {
27
+ result.rules.push({
28
+ name: match[1],
29
+ params: (match[2] || '').trim(),
30
+ source: (match[3] || '').trim(),
31
+ });
32
+ }
33
+ // 若首行未声明数量但解析到了规则列表,自动校正
34
+ if (result.rules.length > 0 && result.ruleCount === 0) {
35
+ result.ruleCount = result.rules.length;
36
+ }
37
+ return result;
38
+ }
39
+ export function createRulesetTool(executor) {
40
+ return {
41
+ name: 'github_ruleset',
42
+ description: '查看与诊断 GitHub 仓库/组织规则集 (Rulesets):检查分支适用的合并策略规则 (check)、查看规则集列表 (list) 与详情 (view)。是诊断“PR 为什么不能合并”的权威规则策略视角。',
43
+ parameters: {
44
+ type: 'object',
45
+ properties: {
46
+ action: {
47
+ type: 'string',
48
+ enum: ['list', 'view', 'check'],
49
+ description: 'Ruleset 操作类型。check 检查分支规则约束(策略排障核心)、list 查看规则集列表、view 查看规则集详情。',
50
+ },
51
+ repo: {
52
+ type: 'string',
53
+ description: '目标仓库 [owner/repo],留空则识别当前工作区仓库。',
54
+ },
55
+ branch: {
56
+ type: 'string',
57
+ description: '要检查的分支名称(check 动作使用,缺省为当前分支)。',
58
+ },
59
+ default_branch: {
60
+ type: 'boolean',
61
+ description: '是否直接检查目标仓库默认分支的规则约束(check 动作可选,映射 --default)。',
62
+ },
63
+ ruleset_id: {
64
+ type: 'string',
65
+ description: '规则集 ID(view 动作必需,避免交互式选择挂起)。',
66
+ },
67
+ org: {
68
+ type: 'string',
69
+ description: '组织名称(list, view 动作可选,查看组织级规则集;需 admin:org 权限)。',
70
+ },
71
+ no_parents: {
72
+ type: 'boolean',
73
+ description: '是否排除上级组织配置的继承规则集(list, view 动作可选,默认包含上级继承;true 时仅看当前层级)。',
74
+ },
75
+ limit: {
76
+ type: 'number',
77
+ description: '返回规则集数量上限(list 动作使用,默认 30)。',
78
+ },
79
+ },
80
+ required: ['action'],
81
+ },
82
+ output: {
83
+ schema: { type: 'object' },
84
+ render(args, value) {
85
+ return [{ type: 'text', text: renderRulesetMarkdown(args, value) }];
86
+ },
87
+ },
88
+ async execute(args, execContext) {
89
+ const cwd = execContext?.cwd || process.cwd();
90
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
91
+ // 解析当前工作区仓库(若未传 org 且需要 repo 维度)
92
+ const resolveRepoTarget = async () => {
93
+ if (args.repo)
94
+ return args.repo;
95
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
96
+ if (meta?.nameWithOwner)
97
+ return meta.nameWithOwner;
98
+ throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
99
+ };
100
+ const runOpts = { cwd, sessionId };
101
+ switch (args.action) {
102
+ case 'check': {
103
+ const repoTarget = await resolveRepoTarget();
104
+ const checkCmd = ['ruleset', 'check'];
105
+ if (args.branch) {
106
+ checkCmd.push(args.branch);
107
+ }
108
+ if (args.default_branch) {
109
+ checkCmd.push('--default');
110
+ }
111
+ checkCmd.push('-R', repoTarget);
112
+ const res = await executor.run(checkCmd, { ...runOpts, rawText: true });
113
+ if (!res.ok)
114
+ throw new Error(res.error);
115
+ const parsed = parseRulesetCheckOutput(res.rawOutput || '');
116
+ if (!parsed.repo)
117
+ parsed.repo = repoTarget;
118
+ if (!parsed.branch && args.branch)
119
+ parsed.branch = args.branch;
120
+ return parsed;
121
+ }
122
+ case 'list': {
123
+ const listCmd = ['ruleset', 'list'];
124
+ let repoTarget = '';
125
+ if (args.org) {
126
+ listCmd.push('--org', args.org);
127
+ }
128
+ else {
129
+ repoTarget = await resolveRepoTarget();
130
+ listCmd.push('-R', repoTarget);
131
+ }
132
+ if (args.no_parents) {
133
+ listCmd.push('--no-parents');
134
+ }
135
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : 30;
136
+ listCmd.push('-L', String(limit));
137
+ const res = await executor.run(listCmd, { ...runOpts, rawText: true });
138
+ if (!res.ok)
139
+ throw new Error(res.error);
140
+ return {
141
+ rawText: (res.rawOutput || '').trim(),
142
+ org: args.org,
143
+ repo: repoTarget,
144
+ };
145
+ }
146
+ case 'view': {
147
+ if (args.ruleset_id === undefined || args.ruleset_id === null || String(args.ruleset_id).trim() === '') {
148
+ throw new Error('查看规则集详情时 `ruleset_id` 参数为必填项(输入规则集数字 ID,如 "4898070")。');
149
+ }
150
+ const viewCmd = ['ruleset', 'view', String(args.ruleset_id)];
151
+ let repoTarget = '';
152
+ if (args.org) {
153
+ viewCmd.push('--org', args.org);
154
+ }
155
+ else {
156
+ repoTarget = await resolveRepoTarget();
157
+ viewCmd.push('-R', repoTarget);
158
+ }
159
+ if (args.no_parents) {
160
+ viewCmd.push('--no-parents');
161
+ }
162
+ const res = await executor.run(viewCmd, { ...runOpts, rawText: true });
163
+ if (!res.ok)
164
+ throw new Error(res.error);
165
+ return {
166
+ ruleset_id: String(args.ruleset_id),
167
+ rawText: (res.rawOutput || '').trim(),
168
+ org: args.org,
169
+ repo: repoTarget,
170
+ };
171
+ }
172
+ default:
173
+ throw new Error(`不支持的 ruleset 操作: "${args.action}"。可选值: check, list, view。`);
174
+ }
175
+ },
176
+ };
177
+ }
@@ -35,6 +35,14 @@ export declare function createRunTool(executor: GhExecutor): {
35
35
  type: string;
36
36
  description: string;
37
37
  };
38
+ all: {
39
+ type: string;
40
+ description: string;
41
+ };
42
+ name: {
43
+ type: string;
44
+ description: string;
45
+ };
38
46
  ref: {
39
47
  type: string;
40
48
  description: string;
package/lib/tools/run.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { compareGhVersions } from '../executor.js';
1
2
  import { renderRunMarkdown } from '../renderers/markdown.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
2
6
  function normalizeOutput(data) {
3
7
  if (data && typeof data === 'object' && !Array.isArray(data)) {
4
8
  return data;
@@ -17,8 +21,8 @@ export function createRunTool(executor) {
17
21
  properties: {
18
22
  action: {
19
23
  type: 'string',
20
- enum: ['list', 'view', 'log_failed', 'rerun', 'cancel', 'trigger'],
21
- description: 'Actions 操作类型。新增 trigger (主动触发 workflow_dispatch)。',
24
+ enum: ['list', 'view', 'log_failed', 'rerun', 'cancel', 'trigger', 'workflow_list', 'workflow_view', 'workflow_enable', 'workflow_disable', 'download'],
25
+ description: 'Actions 操作类型。trigger 主动触发 workflow_dispatch;workflow_* 查询与启停工作流;download 下载构建产物(落盘插件管理目录)。',
22
26
  },
23
27
  run_id: {
24
28
  type: 'string',
@@ -43,7 +47,15 @@ export function createRunTool(executor) {
43
47
  },
44
48
  workflow: {
45
49
  type: 'string',
46
- description: '工作流名称或文件名(trigger 动作使用,例如 "pr-check.yml" 或 "CI")。',
50
+ description: '工作流名称、ID 或文件名(trigger/workflow_view 动作使用,例如 "pr-check.yml"、"CI" 或 "374382120")。',
51
+ },
52
+ all: {
53
+ type: 'boolean',
54
+ description: 'workflow_list 可选:列出全部工作流(含 disabled)。默认仅 active——排查「工作流为何不跑」时 disabled 状态本身就是答案,排障建议开启。',
55
+ },
56
+ name: {
57
+ type: 'string',
58
+ description: 'download 可选:仅下载指定名称的 artifact(可多次过滤,这里取单个名称)。',
47
59
  },
48
60
  ref: {
49
61
  type: 'string',
@@ -132,6 +144,95 @@ export function createRunTool(executor) {
132
144
  }
133
145
  return { result: res.rawOutput || '未找到失败步骤的日志' };
134
146
  }
147
+ case 'workflow_list': {
148
+ // 字段集为 gh 2.96 实测合法全集(仅 4 个,无 updatedAt——勘误 E1)。
149
+ // --all 暴露 disabled(排障语义:默认 active-only 对 Agent 是陷阱,D6 决策)。
150
+ const listCmd = ['workflow', 'list', ...repoArgs, '--json', 'id,name,path,state', '-L', String(typeof args.limit === 'number' && args.limit > 0 ? args.limit : Math.min(executor.defaultListLimit ?? 10, 50))];
151
+ if (args.all === true)
152
+ listCmd.push('--all');
153
+ const res = await executor.run(listCmd, runOpts);
154
+ if (!res.ok)
155
+ throw new Error(res.error);
156
+ return normalizeOutput(res.data || res.rawOutput);
157
+ }
158
+ case 'workflow_view': {
159
+ if (!args.workflow)
160
+ throw new Error('workflow_view 必须提供 workflow(工作流名称、ID 或文件名)');
161
+ // gh workflow view 不支持 --json(勘误 E2,实测 unknown flag)——rawText 直通,
162
+ // 仅头部三行结构化抽取;Recent runs 表保留原文(那是 run list 的投影,
163
+ // Agent 要结构化运行数据走 github_run action:'list',工具各司其职,D4 决策)。
164
+ const res = await executor.run(['workflow', 'view', args.workflow, ...repoArgs], { ...runOpts, rawText: true, timeoutMs: 20_000 });
165
+ if (!res.ok)
166
+ throw new Error(res.error);
167
+ const text = res.rawOutput || '';
168
+ const idMatch = /^ID:\s*(\S+)/m.exec(text);
169
+ const totalMatch = /^Total runs\s+(\S+)/m.exec(text);
170
+ // 首行是「名称 - 文件名」组合行(实测形态),整行保留作 workflow 标识
171
+ const firstLine = text.split('\n', 1)[0]?.trim() || args.workflow;
172
+ const recentIdx = text.indexOf('Recent runs');
173
+ return {
174
+ workflow: firstLine,
175
+ id: idMatch ? idMatch[1] : undefined,
176
+ totalRuns: totalMatch ? totalMatch[1] : undefined,
177
+ recentRuns: recentIdx >= 0 ? text.slice(recentIdx).trim() : text.trim(),
178
+ };
179
+ }
180
+ case 'workflow_enable':
181
+ case 'workflow_disable': {
182
+ if (!args.workflow)
183
+ throw new Error(`${args.action} 必须提供 workflow(工作流名称、ID 或文件名)`);
184
+ // 可逆状态开关免 confirm(S3 方案 D4),但 disable 的后果(trigger 静默失败)
185
+ // 必须写进返回——Agent 不知情会把 CI 故障误判为代码问题
186
+ const res = await executor.run(['workflow', args.action === 'workflow_enable' ? 'enable' : 'disable', args.workflow, ...repoArgs], runOpts);
187
+ if (!res.ok)
188
+ throw new Error(res.error);
189
+ return args.action === 'workflow_disable'
190
+ ? { message: `工作流「${args.workflow}」已禁用。注意:下次 trigger 将静默失败,直至重新 enable(workflow_enable)。` }
191
+ : { message: `工作流「${args.workflow}」已启用,恢复可触发状态。` };
192
+ }
193
+ case 'download': {
194
+ if (!args.run_id)
195
+ throw new Error('download 操作必须提供 run_id');
196
+ if (args.name && typeof args.name !== 'string')
197
+ throw new Error('name 必须是字符串(artifact 名称过滤)');
198
+ // GHSA-39wj-f2f4-978v(gh<2.102 符号链接路径穿越)防线:落盘目录由插件
199
+ // 固定派生(owner/repo/run_id 三级),不暴露 dir 参数给用户——攻击面
200
+ // 收敛到一个 mkdirSync 调用点;用户无法注入 ../ 或符号链接目标。
201
+ const artifactsRoot = path.join(os.homedir(), '.dsh', 'data', 'dsh-github-flow', 'artifacts', ...repoTarget.split('/'), String(args.run_id));
202
+ fs.mkdirSync(artifactsRoot, { recursive: true });
203
+ const dlCmd = ['run', 'download', String(args.run_id), ...repoArgs, '-D', artifactsRoot];
204
+ if (args.name) {
205
+ dlCmd.push('-n', args.name);
206
+ }
207
+ const res = await executor.run(dlCmd, { ...runOpts, timeoutMs: 120_000 });
208
+ if (!res.ok)
209
+ throw new Error(res.error);
210
+ // 落盘清单:递归枚举产物文件(相对路径+大小),供 Agent 直接引用
211
+ const files = [];
212
+ const walk = (dir, prefix) => {
213
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
214
+ const full = path.join(dir, entry.name);
215
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
216
+ if (entry.isDirectory())
217
+ walk(full, rel);
218
+ else
219
+ files.push({ file: rel, size: fs.statSync(full).size });
220
+ }
221
+ };
222
+ walk(artifactsRoot, '');
223
+ // GHSA-39wj-f2f4-978v:2.102.0 修复符号链接路径穿越。低版本不硬拒绝
224
+ // (受控仓库下载是合理需求),但安全提示必须进返回值引导升级。
225
+ const ghVersion = await executor.getGhVersion();
226
+ const securityNote = ghVersion && compareGhVersions(ghVersion, '2.102.0') < 0
227
+ ? `安全提示:当前 gh ${ghVersion} 低于 2.102.0(修复 GHSA-39wj-f2f4-978v 符号链接路径穿越)。建议升级 gh(Windows: winget upgrade GitHub.cli / macOS: brew upgrade gh)。`
228
+ : undefined;
229
+ return {
230
+ message: `Run #${args.run_id} 产物已下载 ${files.length} 个文件${args.name ? `(过滤: ${args.name})` : ''}`,
231
+ directory: artifactsRoot,
232
+ files,
233
+ ...(securityNote ? { securityNote } : {}),
234
+ };
235
+ }
135
236
  case 'trigger': {
136
237
  if (!args.workflow)
137
238
  throw new Error('trigger 操作必须提供 workflow(工作流名称或文件名,例如 "pr-check.yml")');
@@ -10,6 +10,11 @@ export declare function createSecretTool(executor: GhExecutor): {
10
10
  enum: string[];
11
11
  description: string;
12
12
  };
13
+ kind: {
14
+ type: string;
15
+ enum: string[];
16
+ description: string;
17
+ };
13
18
  repo: {
14
19
  type: string;
15
20
  description: string;