dsh-github-flow 0.3.0 → 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 (94) hide show
  1. package/README.en.md +220 -143
  2. package/README.md +202 -124
  3. package/docs/UPGRADE-NOTES.md +14 -8
  4. package/docs/architecture.md +111 -129
  5. package/lib/api/routes-actions.js +117 -0
  6. package/lib/api/routes-overview.js +94 -2
  7. package/lib/cache.d.ts +4 -0
  8. package/lib/cache.js +14 -0
  9. package/lib/client.d.ts +13 -1
  10. package/lib/client.js +2906 -2475
  11. package/lib/executor/git-parser.d.ts +25 -0
  12. package/lib/executor/git-parser.js +143 -0
  13. package/lib/executor/platform-guide.d.ts +14 -0
  14. package/lib/executor/platform-guide.js +40 -0
  15. package/lib/executor/process-utils.d.ts +51 -0
  16. package/lib/executor/process-utils.js +162 -0
  17. package/lib/executor/version-utils.d.ts +12 -0
  18. package/lib/executor/version-utils.js +27 -0
  19. package/lib/executor.d.ts +45 -48
  20. package/lib/executor.js +184 -353
  21. package/lib/git-watcher.d.ts +36 -0
  22. package/lib/git-watcher.js +91 -0
  23. package/lib/main.js +10 -0
  24. package/lib/renderers/cache.d.ts +8 -0
  25. package/lib/renderers/cache.js +66 -0
  26. package/lib/renderers/common.d.ts +24 -0
  27. package/lib/renderers/common.js +88 -0
  28. package/lib/renderers/gist.d.ts +15 -0
  29. package/lib/renderers/gist.js +99 -0
  30. package/lib/renderers/issue.d.ts +4 -0
  31. package/lib/renderers/issue.js +70 -0
  32. package/lib/renderers/label.d.ts +4 -0
  33. package/lib/renderers/label.js +48 -0
  34. package/lib/renderers/markdown.d.ts +15 -32
  35. package/lib/renderers/markdown.js +15 -624
  36. package/lib/renderers/pr.d.ts +4 -0
  37. package/lib/renderers/pr.js +107 -0
  38. package/lib/renderers/release.d.ts +4 -0
  39. package/lib/renderers/release.js +74 -0
  40. package/lib/renderers/repo.d.ts +4 -0
  41. package/lib/renderers/repo.js +149 -0
  42. package/lib/renderers/ruleset.d.ts +4 -0
  43. package/lib/renderers/ruleset.js +74 -0
  44. package/lib/renderers/run.d.ts +4 -0
  45. package/lib/renderers/run.js +79 -0
  46. package/lib/renderers/skill.d.ts +4 -0
  47. package/lib/renderers/skill.js +61 -0
  48. package/lib/renderers/status.d.ts +23 -0
  49. package/lib/renderers/status.js +124 -0
  50. package/lib/tools/cache.d.ts +64 -0
  51. package/lib/tools/cache.js +186 -0
  52. package/lib/tools/gist.d.ts +155 -0
  53. package/lib/tools/gist.js +190 -0
  54. package/lib/tools/help.js +107 -6
  55. package/lib/tools/image-utils.d.ts +8 -0
  56. package/lib/tools/image-utils.js +34 -0
  57. package/lib/tools/issue.d.ts +8 -1
  58. package/lib/tools/issue.js +26 -4
  59. package/lib/tools/label.d.ts +80 -0
  60. package/lib/tools/label.js +237 -0
  61. package/lib/tools/pr.d.ts +8 -0
  62. package/lib/tools/pr.js +48 -3
  63. package/lib/tools/ruleset.d.ts +80 -0
  64. package/lib/tools/ruleset.js +177 -0
  65. package/lib/tools/status.d.ts +39 -0
  66. package/lib/tools/status.js +53 -0
  67. package/lib/types.d.ts +39 -0
  68. package/lib/ui/auth-guide-card.d.ts +4 -0
  69. package/lib/ui/auth-guide-card.js +161 -0
  70. package/lib/ui/branch-selector.d.ts +10 -0
  71. package/lib/ui/branch-selector.js +163 -0
  72. package/lib/ui/commit-modal.d.ts +8 -0
  73. package/lib/ui/commit-modal.js +89 -0
  74. package/lib/ui/icons.d.ts +7 -0
  75. package/lib/ui/icons.js +53 -0
  76. package/lib/ui/network-banner.d.ts +10 -0
  77. package/lib/ui/network-banner.js +54 -0
  78. package/lib/ui/storage.d.ts +26 -0
  79. package/lib/ui/storage.js +134 -0
  80. package/lib/ui/styles.d.ts +6 -0
  81. package/lib/ui/styles.js +1285 -0
  82. package/lib/ui/sync-settings-modal.d.ts +11 -0
  83. package/lib/ui/sync-settings-modal.js +350 -0
  84. package/lib/ui/sync-status-badge.d.ts +8 -0
  85. package/lib/ui/sync-status-badge.js +64 -0
  86. package/lib/ui/upgrade-card.d.ts +7 -0
  87. package/lib/ui/upgrade-card.js +197 -0
  88. package/lib/ui/utils.d.ts +10 -0
  89. package/lib/ui/utils.js +66 -0
  90. package/lib/ui/workspace-panel.d.ts +4 -0
  91. package/lib/ui/workspace-panel.js +464 -0
  92. package/lib/version.d.ts +1 -1
  93. package/lib/version.js +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ import { renderLabelMarkdown } from '../renderers/markdown.js';
2
+ /**
3
+ * 校验并归一化 6 位十六进制颜色(去除前导 #)
4
+ */
5
+ export function normalizeHexColor(rawColor) {
6
+ if (!rawColor)
7
+ return undefined;
8
+ const clean = rawColor.trim().replace(/^#/, '').trim();
9
+ if (!/^[0-9a-fA-F]{6}$/.test(clean)) {
10
+ throw new Error(`标签颜色必须为 6 位十六进制字符串(如 "d73a4a" 或 "#d73a4a"),当前为: "${rawColor}"`);
11
+ }
12
+ return clean;
13
+ }
14
+ function normalizeOutput(data) {
15
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
16
+ return data;
17
+ }
18
+ if (Array.isArray(data)) {
19
+ return { items: data, count: data.length };
20
+ }
21
+ return { result: data !== undefined && data !== null ? data : '' };
22
+ }
23
+ export function createLabelTool(executor) {
24
+ return {
25
+ name: 'github_label',
26
+ description: '管理 GitHub 仓库标签体系 (Labels):查看标签列表、创建新标签、编辑修改现有标签、克隆跨仓库标签体系 (clone) 以及安全删除标签。删除操作不可逆且会解绑议题,需 confirm: true。',
27
+ parameters: {
28
+ type: 'object',
29
+ properties: {
30
+ action: {
31
+ type: 'string',
32
+ enum: ['list', 'create', 'edit', 'delete', 'clone'],
33
+ description: 'Label 操作类型。list 查看列表、create 新建、edit 修改、delete 物理删除(需 confirm)、clone 跨仓库克隆同步。',
34
+ },
35
+ repo: {
36
+ type: 'string',
37
+ description: '目标仓库 [owner/repo],留空则识别当前工作区仓库。',
38
+ },
39
+ name: {
40
+ type: 'string',
41
+ description: '标签名称(create, edit, delete 动作必需)。',
42
+ },
43
+ new_name: {
44
+ type: 'string',
45
+ description: '新标签名称(edit 动作重命名标签时使用)。',
46
+ },
47
+ color: {
48
+ type: 'string',
49
+ description: '6 位十六进制色值(create, edit 动作可选,例如 "d73a4a" 或 "#d73a4a")。',
50
+ },
51
+ description: {
52
+ type: 'string',
53
+ description: '标签描述(create, edit 动作可选)。',
54
+ },
55
+ force: {
56
+ type: 'boolean',
57
+ description: '是否强制覆盖(create 动作同名时更新、clone 动作冲突时覆盖;默认 false)。',
58
+ },
59
+ source_repo: {
60
+ type: 'string',
61
+ description: '源仓库全名 [owner/repo](clone 动作必需,例如 "cli/cli")。',
62
+ },
63
+ limit: {
64
+ type: 'number',
65
+ description: '返回标签数量上限(list 动作使用,默认 30)。',
66
+ },
67
+ search: {
68
+ type: 'string',
69
+ description: '按标签名称或描述过滤检索(list 动作使用)。',
70
+ },
71
+ sort: {
72
+ type: 'string',
73
+ enum: ['created', 'name'],
74
+ description: '排序字段(list 动作使用,默认 created)。',
75
+ },
76
+ order: {
77
+ type: 'string',
78
+ enum: ['asc', 'desc'],
79
+ description: '排序方向(list 动作使用,默认 asc)。',
80
+ },
81
+ confirm: {
82
+ type: 'boolean',
83
+ description: '高危破坏性操作确认标志(delete 动作必需)。未传 true 时输出影响评估报告并拦截。',
84
+ },
85
+ },
86
+ required: ['action'],
87
+ },
88
+ output: {
89
+ schema: { type: 'object' },
90
+ render(args, value) {
91
+ return [{ type: 'text', text: renderLabelMarkdown(args, value) }];
92
+ },
93
+ },
94
+ async execute(args, execContext) {
95
+ const cwd = execContext?.cwd || process.cwd();
96
+ const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
97
+ let repoTarget = args.repo;
98
+ if (!repoTarget) {
99
+ const meta = await executor.getRepoMetadata(cwd, sessionId);
100
+ if (meta?.nameWithOwner) {
101
+ repoTarget = meta.nameWithOwner;
102
+ }
103
+ }
104
+ if (!repoTarget) {
105
+ throw new Error('未能自动识别当前工作区关联的 GitHub 仓库。请通过 `repo` 参数显式指定目标仓库 [owner/repo](例如: "ItQianChen/dsh-github-flow"),或在当前工作区目录下关联 Git 远程仓库。');
106
+ }
107
+ const repoArgs = ['-R', repoTarget];
108
+ const runOpts = { cwd, sessionId };
109
+ switch (args.action) {
110
+ case 'list': {
111
+ const listCmd = ['label', 'list', ...repoArgs, '--json', 'name,color,description'];
112
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : 30;
113
+ listCmd.push('-L', String(limit));
114
+ if (args.search) {
115
+ listCmd.push('-S', String(args.search));
116
+ }
117
+ if (args.sort) {
118
+ listCmd.push('--sort', args.sort);
119
+ }
120
+ if (args.order) {
121
+ listCmd.push('--order', args.order);
122
+ }
123
+ const res = await executor.run(listCmd, runOpts);
124
+ if (!res.ok)
125
+ throw new Error(res.error);
126
+ let rawData = res.data;
127
+ if (!rawData && typeof res.rawOutput === 'string') {
128
+ try {
129
+ rawData = JSON.parse(res.rawOutput);
130
+ }
131
+ catch {
132
+ rawData = [];
133
+ }
134
+ }
135
+ return normalizeOutput(rawData || []);
136
+ }
137
+ case 'create': {
138
+ if (!args.name) {
139
+ throw new Error('创建标签时 `name` 参数为必填项。');
140
+ }
141
+ const color = normalizeHexColor(args.color);
142
+ const createCmd = ['label', 'create', args.name, ...repoArgs];
143
+ if (color) {
144
+ createCmd.push('-c', color);
145
+ }
146
+ if (args.description) {
147
+ createCmd.push('-d', args.description);
148
+ }
149
+ if (args.force) {
150
+ createCmd.push('-f');
151
+ }
152
+ const res = await executor.run(createCmd, runOpts);
153
+ if (!res.ok)
154
+ throw new Error(res.error);
155
+ return {
156
+ success: true,
157
+ name: args.name,
158
+ message: `✅ 成功在仓库 ${repoTarget} 创建标签 "${args.name}"`,
159
+ detail: res.rawOutput || '',
160
+ };
161
+ }
162
+ case 'edit': {
163
+ if (!args.name) {
164
+ throw new Error('编辑标签时 `name` 参数为必填项。');
165
+ }
166
+ const color = normalizeHexColor(args.color);
167
+ const editCmd = ['label', 'edit', args.name, ...repoArgs];
168
+ if (args.new_name) {
169
+ editCmd.push('-n', args.new_name);
170
+ }
171
+ if (color) {
172
+ editCmd.push('-c', color);
173
+ }
174
+ if (args.description !== undefined) {
175
+ editCmd.push('-d', args.description);
176
+ }
177
+ const res = await executor.run(editCmd, runOpts);
178
+ if (!res.ok)
179
+ throw new Error(res.error);
180
+ return {
181
+ success: true,
182
+ name: args.new_name || args.name,
183
+ message: `✅ 成功更新仓库 ${repoTarget} 标签 "${args.name}"` + (args.new_name ? `(重命名为 "${args.new_name}")` : ''),
184
+ detail: res.rawOutput || '',
185
+ };
186
+ }
187
+ case 'delete': {
188
+ if (!args.name) {
189
+ throw new Error('删除标签时 `name` 参数为必填项。');
190
+ }
191
+ if (args.confirm !== true) {
192
+ return {
193
+ status: 'AWAITING_CONFIRMATION',
194
+ action: 'delete',
195
+ name: args.name,
196
+ repo: repoTarget,
197
+ warning: `⚠️ 高危破坏性操作:即将删除仓库 ${repoTarget} 的标签 "${args.name}"`,
198
+ impact: `删除标签不可逆,且会导致目标仓库内所有已打上该标签的 Issue 与 PR 自动解绑该标记。`,
199
+ requiredAction: `如确认彻底删除,请在参数中附加 \`confirm: true\` 重新调用。示例:\`github_label(action: 'delete', name: '${args.name}', confirm: true)\``,
200
+ };
201
+ }
202
+ const deleteCmd = ['label', 'delete', args.name, '--yes', ...repoArgs];
203
+ const res = await executor.run(deleteCmd, runOpts);
204
+ if (!res.ok)
205
+ throw new Error(res.error);
206
+ return {
207
+ success: true,
208
+ name: args.name,
209
+ message: `🗑️ 成功在仓库 ${repoTarget} 删除标签 "${args.name}"`,
210
+ detail: res.rawOutput || '',
211
+ };
212
+ }
213
+ case 'clone': {
214
+ if (!args.source_repo) {
215
+ throw new Error('克隆标签时 `source_repo` 源仓库参数为必填项(格式: [owner/repo])。');
216
+ }
217
+ const cloneCmd = ['label', 'clone', args.source_repo, ...repoArgs];
218
+ if (args.force) {
219
+ cloneCmd.push('-f');
220
+ }
221
+ const res = await executor.run(cloneCmd, runOpts);
222
+ if (!res.ok)
223
+ throw new Error(res.error);
224
+ return {
225
+ success: true,
226
+ source: args.source_repo,
227
+ destination: repoTarget,
228
+ message: `🏷️ 成功将源仓库 ${args.source_repo} 的标签体系克隆同步到 ${repoTarget}` + (args.force ? '(已覆盖同名冲突标签)' : ''),
229
+ detail: res.rawOutput || '',
230
+ };
231
+ }
232
+ default:
233
+ throw new Error(`不支持的 label 操作: "${args.action}"。可选值: list, create, edit, delete, clone。`);
234
+ }
235
+ },
236
+ };
237
+ }
package/lib/tools/pr.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { GhExecutor } from '../executor.js';
2
+ export { extractImageUrls as extractPrImageUrls, validateAttachPaths } from './image-utils.js';
2
3
  export declare function createPrTool(executor: GhExecutor): {
3
4
  name: string;
4
5
  description: string;
@@ -88,6 +89,13 @@ export declare function createPrTool(executor: GhExecutor): {
88
89
  };
89
90
  description: string;
90
91
  };
92
+ attach_images: {
93
+ type: string;
94
+ items: {
95
+ type: string;
96
+ };
97
+ description: string;
98
+ };
91
99
  };
92
100
  required: string[];
93
101
  };
package/lib/tools/pr.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { compareGhVersions } from '../executor.js';
1
2
  import { renderPrMarkdown } from '../renderers/markdown.js';
2
- import { extractImageUrls, downloadImages, buildImageBlocks, filterRequestedUrls } from './image-utils.js';
3
+ import { extractImageUrls, downloadImages, buildImageBlocks, filterRequestedUrls, validateAttachPaths } from './image-utils.js';
3
4
  function normalizeOutput(data) {
4
5
  if (data && typeof data === 'object' && !Array.isArray(data)) {
5
6
  return data;
@@ -9,6 +10,8 @@ function normalizeOutput(data) {
9
10
  }
10
11
  return { result: data !== undefined && data !== null ? data : '' };
11
12
  }
13
+ // 共享图片工具链的同名再导出:保持路径与单测稳定
14
+ export { extractImageUrls as extractPrImageUrls, validateAttachPaths } from './image-utils.js';
12
15
  export function createPrTool(executor) {
13
16
  return {
14
17
  name: 'github_pr',
@@ -18,8 +21,8 @@ export function createPrTool(executor) {
18
21
  properties: {
19
22
  action: {
20
23
  type: 'string',
21
- enum: ['list', 'view', 'create', 'diff', 'checks', 'review', 'merge', 'checkout', 'status', 'view_images', 'ready', 'revert', 'update_branch'],
22
- description: 'PR 操作类型。view_images 下载正文与评论中的图片(UI 变更对比图、复现截图等)到本地并以图片内容块返回,供 Agent 审查时直接查看。merge/revert 需 confirm: true(revert 在远端创建反向提交与新 PR)。',
24
+ enum: ['list', 'view', 'create', 'diff', 'checks', 'review', 'merge', 'checkout', 'status', 'view_images', 'ready', 'revert', 'update_branch', 'comment', 'edit'],
25
+ description: 'PR 操作类型。view_images 下载正文与评论中的图片到本地;create/edit/comment 支持 attach_images 附件上传;merge/revert 需 confirm: true。',
23
26
  },
24
27
  pr_number: {
25
28
  type: 'number',
@@ -97,6 +100,11 @@ export function createPrTool(executor) {
97
100
  items: { type: 'string' },
98
101
  description: 'view_images 可选:仅下载指定 URL 的图片(从 view 返回的 images 清单中选取,不在清单内的会被拒绝)。留空下载全部(正文+评论)。',
99
102
  },
103
+ attach_images: {
104
+ type: 'array',
105
+ items: { type: 'string' },
106
+ description: '本地图片/附件路径列表(create, edit, comment 动作使用,需 gh ≥ 2.99;禁止 URL,单批上限 50 个)。',
107
+ },
100
108
  },
101
109
  required: ['action'],
102
110
  },
@@ -121,6 +129,18 @@ export function createPrTool(executor) {
121
129
  }
122
130
  const repoArgs = ['-R', repoTarget];
123
131
  const runOpts = { cwd, sessionId };
132
+ const appendAttachFlags = async (cmd, attachImages) => {
133
+ const attachPaths = validateAttachPaths(attachImages, cwd);
134
+ if (attachPaths.length > 0) {
135
+ const version = await executor.getGhVersion();
136
+ if (version && compareGhVersions(version, '2.99.0') < 0) {
137
+ throw new Error(`附件上传能力 (\`--attach\`) 需要 GitHub CLI 版本 ≥ 2.99.0,当前版本为: ${version}。\n请升级 gh(Windows: \`winget upgrade GitHub.cli\`,macOS: \`brew upgrade gh\`)后再试。`);
138
+ }
139
+ for (const p of attachPaths) {
140
+ cmd.push('--attach', p);
141
+ }
142
+ }
143
+ };
124
144
  switch (args.action) {
125
145
  case 'list': {
126
146
  const listCmd = ['pr', 'list', ...repoArgs, '--json', 'number,title,author,headRefName,isDraft,state,updatedAt,mergedAt,url'];
@@ -261,6 +281,7 @@ export function createPrTool(executor) {
261
281
  const dryRun = args.dry_run === true;
262
282
  if (dryRun)
263
283
  createCmd.push('--dry-run');
284
+ await appendAttachFlags(createCmd, args.attach_images);
264
285
  const res = await executor.run(createCmd, runOpts);
265
286
  if (!res.ok)
266
287
  throw new Error(res.error);
@@ -350,6 +371,30 @@ export function createPrTool(executor) {
350
371
  throw new Error(res.error);
351
372
  return { message: `PR #${args.pr_number} 分支已从基准分支同步更新`, output: res.rawOutput };
352
373
  }
374
+ case 'comment': {
375
+ if (!args.pr_number || !args.body)
376
+ throw new Error('comment 必须提供 pr_number 和 body');
377
+ const commentCmd = ['pr', 'comment', String(args.pr_number), ...repoArgs, '--body', args.body];
378
+ await appendAttachFlags(commentCmd, args.attach_images);
379
+ const res = await executor.run(commentCmd, runOpts);
380
+ if (!res.ok)
381
+ throw new Error(res.error);
382
+ return { message: 'PR 评论发布成功', output: res.rawOutput };
383
+ }
384
+ case 'edit': {
385
+ if (!args.pr_number)
386
+ throw new Error('edit 操作必须提供 pr_number');
387
+ const editCmd = ['pr', 'edit', String(args.pr_number), ...repoArgs];
388
+ if (args.title)
389
+ editCmd.push('--title', args.title);
390
+ if (args.body)
391
+ editCmd.push('--body', args.body);
392
+ await appendAttachFlags(editCmd, args.attach_images);
393
+ const res = await executor.run(editCmd, runOpts);
394
+ if (!res.ok)
395
+ throw new Error(res.error);
396
+ return { message: `PR #${args.pr_number} 更新成功`, output: res.rawOutput };
397
+ }
353
398
  default:
354
399
  throw new Error(`未知的 PR 操作: ${args.action}`);
355
400
  }
@@ -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
+ }
@@ -0,0 +1,39 @@
1
+ import type { GhExecutor } from '../executor.js';
2
+ export declare function createStatusTool(executor: GhExecutor): {
3
+ name: string;
4
+ description: string;
5
+ parameters: {
6
+ type: string;
7
+ properties: {
8
+ action: {
9
+ type: string;
10
+ enum: string[];
11
+ description: string;
12
+ };
13
+ org: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ exclude: {
18
+ type: string;
19
+ items: {
20
+ type: string;
21
+ };
22
+ description: string;
23
+ };
24
+ };
25
+ };
26
+ output: {
27
+ schema: {
28
+ type: string;
29
+ };
30
+ render(args: any, value: any): {
31
+ type: string;
32
+ text: string;
33
+ }[];
34
+ };
35
+ execute(args: any, execContext: any): Promise<{
36
+ parsed: import("../renderers/status.js").ParsedStatusReport;
37
+ rawText: string;
38
+ }>;
39
+ };