dsh-github-flow 0.5.0 → 0.7.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 (103) hide show
  1. package/README.en.md +112 -17
  2. package/README.md +115 -17
  3. package/docs/UPGRADE-NOTES.md +12 -11
  4. package/docs/architecture.md +81 -22
  5. package/docs/images/1.png +0 -0
  6. package/lib/api/route-helpers.d.ts +19 -0
  7. package/lib/api/route-helpers.js +54 -0
  8. package/lib/api/routes-actions.js +214 -9
  9. package/lib/api/routes-lifecycle.js +8 -1
  10. package/lib/api/routes-overview.d.ts +26 -0
  11. package/lib/api/routes-overview.js +396 -36
  12. package/lib/api/routes-proxy.js +8 -1
  13. package/lib/api/routes.d.ts +11 -1
  14. package/lib/api/routes.js +15 -2
  15. package/lib/cache.d.ts +56 -4
  16. package/lib/cache.js +77 -7
  17. package/lib/client.d.ts +8 -3
  18. package/lib/client.js +5542 -1391
  19. package/lib/destructive.d.ts +59 -0
  20. package/lib/destructive.js +285 -0
  21. package/lib/executor/args-guard.d.ts +17 -0
  22. package/lib/executor/args-guard.js +23 -0
  23. package/lib/executor/error-classifier.d.ts +55 -0
  24. package/lib/executor/error-classifier.js +199 -0
  25. package/lib/executor/failure-anchors.d.ts +52 -0
  26. package/lib/executor/failure-anchors.js +116 -0
  27. package/lib/executor/git-parser.d.ts +124 -0
  28. package/lib/executor/git-parser.js +397 -0
  29. package/lib/executor/pr-review.d.ts +95 -0
  30. package/lib/executor/pr-review.js +175 -0
  31. package/lib/executor/run-log.d.ts +36 -0
  32. package/lib/executor/run-log.js +78 -0
  33. package/lib/executor.d.ts +277 -6
  34. package/lib/executor.js +1265 -94
  35. package/lib/github-service.d.ts +1 -1
  36. package/lib/main.d.ts +92 -18
  37. package/lib/main.js +186 -95
  38. package/lib/renderers/common.d.ts +0 -3
  39. package/lib/renderers/common.js +8 -44
  40. package/lib/renderers/markdown.d.ts +1 -0
  41. package/lib/renderers/markdown.js +1 -0
  42. package/lib/renderers/pr.js +26 -1
  43. package/lib/renderers/task.d.ts +9 -0
  44. package/lib/renderers/task.js +120 -0
  45. package/lib/review-rules.d.ts +60 -0
  46. package/lib/review-rules.js +204 -0
  47. package/lib/shared/state-badges.d.ts +42 -0
  48. package/lib/shared/state-badges.js +102 -0
  49. package/lib/task-primitives.d.ts +109 -0
  50. package/lib/task-primitives.js +194 -0
  51. package/lib/tool-tiers.d.ts +57 -0
  52. package/lib/tool-tiers.js +168 -0
  53. package/lib/tools/agent-task.js +4 -4
  54. package/lib/tools/api.js +18 -6
  55. package/lib/tools/cache.js +42 -40
  56. package/lib/tools/codespace.js +4 -12
  57. package/lib/tools/discussion.js +4 -4
  58. package/lib/tools/gist.d.ts +1 -39
  59. package/lib/tools/gist.js +4 -14
  60. package/lib/tools/help.js +72 -18
  61. package/lib/tools/image-utils.d.ts +27 -0
  62. package/lib/tools/image-utils.js +73 -3
  63. package/lib/tools/issue.js +16 -28
  64. package/lib/tools/label.js +4 -15
  65. package/lib/tools/pr.d.ts +47 -0
  66. package/lib/tools/pr.js +170 -33
  67. package/lib/tools/project.js +4 -13
  68. package/lib/tools/release.js +11 -22
  69. package/lib/tools/repo.js +4 -4
  70. package/lib/tools/ruleset.js +9 -6
  71. package/lib/tools/run.js +76 -24
  72. package/lib/tools/secret.js +11 -17
  73. package/lib/tools/skill.js +18 -22
  74. package/lib/tools/status.js +4 -4
  75. package/lib/tools/task.d.ts +219 -0
  76. package/lib/tools/task.js +457 -0
  77. package/lib/types.d.ts +514 -1
  78. package/lib/ui/help-modal.d.ts +6 -0
  79. package/lib/ui/help-modal.js +124 -16
  80. package/lib/ui/issue-modal.d.ts +14 -0
  81. package/lib/ui/issue-modal.js +370 -0
  82. package/lib/ui/markdown-lite.d.ts +66 -0
  83. package/lib/ui/markdown-lite.js +312 -0
  84. package/lib/ui/network-banner.d.ts +12 -0
  85. package/lib/ui/network-banner.js +35 -0
  86. package/lib/ui/pr-modal.d.ts +17 -0
  87. package/lib/ui/pr-modal.js +555 -0
  88. package/lib/ui/repo-badges.d.ts +43 -0
  89. package/lib/ui/repo-badges.js +82 -0
  90. package/lib/ui/repo-selector.d.ts +14 -0
  91. package/lib/ui/repo-selector.js +112 -0
  92. package/lib/ui/run-modal.d.ts +26 -0
  93. package/lib/ui/run-modal.js +486 -0
  94. package/lib/ui/storage.d.ts +47 -0
  95. package/lib/ui/storage.js +139 -0
  96. package/lib/ui/styles.d.ts +1 -1
  97. package/lib/ui/styles.js +1330 -44
  98. package/lib/ui/sync-settings-modal.js +1 -1
  99. package/lib/ui/utils.js +13 -1
  100. package/lib/ui/workspace-panel.js +226 -29
  101. package/lib/version.d.ts +1 -1
  102. package/lib/version.js +1 -1
  103. package/package.json +9 -2
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CI 失败日志的「锚点萃取」:把整段日志压成若干段带行号的失败上下文。
3
+ *
4
+ * 为什么需要它:
5
+ * 1. 矩阵构建(matrix)下一个 run 会有 N 个 job 的失败日志,动辄几万行;
6
+ * 「整段给模型」必然触顶默认输出上限,而截断发生在**萃取之前**——
7
+ * 结果就是 Agent 只能看到前 24KB,后面的失败根本不存在于上下文里;
8
+ * 2. 模型真正需要的不是全量日志,而是「每个失败的起点 + 之后几十行」;
9
+ * 3. 锚点全部取自各框架日志的首列特征(实测输出),不含猜测。
10
+ *
11
+ * 纯函数、零 I/O:日志文本进,结构化块出。
12
+ */
13
+ /** 去掉 gh 的 `job\tstep\ttimestamp ` 前缀(没有前缀时原样返回) */
14
+ export declare function stripLogPrefix(line: string): string;
15
+ export interface FailureBlock {
16
+ /** 命中的锚点原文(去掉首尾空白,超长截断) */
17
+ anchor: string;
18
+ /** 该块起始行号(1-based,方便人去日志里定位) */
19
+ startLine: number;
20
+ /** 块正文(含锚点行与其后上下文) */
21
+ text: string;
22
+ }
23
+ export interface FailureAnchorResult {
24
+ blocks: FailureBlock[];
25
+ /** 萃取到的失败块总数(可能大于返回的 blocks.length) */
26
+ totalBlocks: number;
27
+ /** 因 maxBlocks 上限被丢弃的块数 */
28
+ droppedBlocks: number;
29
+ }
30
+ export interface FailureAnchorOptions {
31
+ /** 每个锚点向后取多少行上下文 */
32
+ radius?: number;
33
+ /** 最多返回多少块 */
34
+ maxBlocks?: number;
35
+ }
36
+ /**
37
+ * 从日志文本中萃取失败块。
38
+ *
39
+ * 合并规则:相邻锚点相距不超过 `radius` 行时并入同一块——同一个异常常常先打
40
+ * `FAIL` 再打 `AssertionError`,拆成两块等于把一次失败说两遍。
41
+ * 每块长度上限 `2*radius+1` 行,保证单块不会因为连续堆栈而无限膨胀。
42
+ */
43
+ export declare function extractFailureAnchors(text: string, options?: FailureAnchorOptions): FailureAnchorResult;
44
+ /** 把萃取结果渲染成 Markdown(人可读、模型可读,行号显式) */
45
+ export declare function renderFailureAnchors(result: FailureAnchorResult): string;
46
+ /**
47
+ * 无锚点时的回退:取尾部若干行。
48
+ *
49
+ * 为什么取尾部而不是头部:日志的失败结论在末尾(构建工具通常先刷编译输出、
50
+ * 最后才打印错误汇总),取头部基本只能拿到环境准备信息。
51
+ */
52
+ export declare function tailLines(text: string, count?: number): string;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * CI 失败日志的「锚点萃取」:把整段日志压成若干段带行号的失败上下文。
3
+ *
4
+ * 为什么需要它:
5
+ * 1. 矩阵构建(matrix)下一个 run 会有 N 个 job 的失败日志,动辄几万行;
6
+ * 「整段给模型」必然触顶默认输出上限,而截断发生在**萃取之前**——
7
+ * 结果就是 Agent 只能看到前 24KB,后面的失败根本不存在于上下文里;
8
+ * 2. 模型真正需要的不是全量日志,而是「每个失败的起点 + 之后几十行」;
9
+ * 3. 锚点全部取自各框架日志的首列特征(实测输出),不含猜测。
10
+ *
11
+ * 纯函数、零 I/O:日志文本进,结构化块出。
12
+ */
13
+ /**
14
+ * 锚点识别(行首特征)。
15
+ *
16
+ * 为什么分两条正则:
17
+ * 多数框架的失败标记在**行首**(pytest 的 `FAILED path::test`、node 的
18
+ * `AssertionError`、Go 的 `panic:`),但 TypeScript 编译器的真实输出是
19
+ * `src/file.ts(3,5): error TS2322: ...` —— 前面带 `文件(行,列):` 前缀,
20
+ * 行首锚定会一条都命中不了。所以 TS 单独用「行内出现」判定。
21
+ *
22
+ * `❌` 单列:本项目自己的 CI 测试输出就是 `❌ FAIL: xxx`,不加这一条
23
+ * 等于对自己仓库的失败日志完全失明。
24
+ */
25
+ const ANCHOR_LINE_RE = /^(?:FAIL|FAILED|ERROR|--- FAIL|AssertionError|Error:|panic:|Traceback \(most recent call last\)|npm ERR!|✖|❌)/;
26
+ const ANCHOR_INLINE_RE = /\berror TS\d+:/;
27
+ /**
28
+ * `gh run view --log-failed` 的每行前缀:`<job>\t<step>\t<ISO8601>Z <正文>`。
29
+ *
30
+ * 这是**实测**出来的(不是猜的):不打掉这个前缀,所有行首都变成 job 名,
31
+ * 上面两条正则一条也命中不了——锚点萃取会整体退化成「尾部 200 行回退」,
32
+ * 功能等于不存在。剥离只用于**匹配**,块正文仍保留原始行(job/step 是有用上下文)。
33
+ */
34
+ const GH_LOG_PREFIX_RE = /^[^\t]*\t[^\t]*\t\d{4}-\d{2}-\d{2}T[0-9:.]+Z\s?/;
35
+ /** 去掉 gh 的 `job\tstep\ttimestamp ` 前缀(没有前缀时原样返回) */
36
+ export function stripLogPrefix(line) {
37
+ const src = typeof line === 'string' ? line : '';
38
+ const m = GH_LOG_PREFIX_RE.exec(src);
39
+ return m ? src.slice(m[0].length) : src;
40
+ }
41
+ /**
42
+ * 从日志文本中萃取失败块。
43
+ *
44
+ * 合并规则:相邻锚点相距不超过 `radius` 行时并入同一块——同一个异常常常先打
45
+ * `FAIL` 再打 `AssertionError`,拆成两块等于把一次失败说两遍。
46
+ * 每块长度上限 `2*radius+1` 行,保证单块不会因为连续堆栈而无限膨胀。
47
+ */
48
+ export function extractFailureAnchors(text, options = {}) {
49
+ const radius = Number.isFinite(options.radius) && options.radius > 0 ? Math.floor(options.radius) : 15;
50
+ const maxBlocks = Number.isFinite(options.maxBlocks) && options.maxBlocks > 0 ? Math.floor(options.maxBlocks) : 8;
51
+ const src = typeof text === 'string' ? text : '';
52
+ if (!src)
53
+ return { blocks: [], totalBlocks: 0, droppedBlocks: 0 };
54
+ const lines = src.split(/\r?\n/);
55
+ const anchors = [];
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+ // 先剥 gh 的 job/step/timestamp 前缀,再按行首特征匹配(见 GH_LOG_PREFIX_RE 注释)
59
+ const body = stripLogPrefix(line).trim();
60
+ if (ANCHOR_LINE_RE.test(body) || ANCHOR_INLINE_RE.test(body))
61
+ anchors.push(i);
62
+ }
63
+ if (anchors.length === 0)
64
+ return { blocks: [], totalBlocks: 0, droppedBlocks: 0 };
65
+ // 分组:相邻锚点间距 > radius 才开新块
66
+ const groups = [];
67
+ for (const idx of anchors) {
68
+ const cur = groups[groups.length - 1];
69
+ if (!cur || idx - cur.last > radius)
70
+ groups.push({ start: idx, last: idx });
71
+ else
72
+ cur.last = idx;
73
+ }
74
+ const maxLines = 2 * radius + 1;
75
+ const all = groups.map((g) => {
76
+ const end = Math.min(lines.length, g.last + radius + 1);
77
+ const slice = lines.slice(g.start, end).slice(0, maxLines);
78
+ const anchorRaw = lines[g.start].trim();
79
+ return {
80
+ anchor: anchorRaw.length > 160 ? anchorRaw.slice(0, 160) + '…' : anchorRaw,
81
+ startLine: g.start + 1,
82
+ text: slice.join('\n'),
83
+ };
84
+ });
85
+ const blocks = all.slice(0, maxBlocks);
86
+ return { blocks, totalBlocks: all.length, droppedBlocks: all.length - blocks.length };
87
+ }
88
+ /** 把萃取结果渲染成 Markdown(人可读、模型可读,行号显式) */
89
+ export function renderFailureAnchors(result) {
90
+ if (!result || result.blocks.length === 0)
91
+ return '';
92
+ const parts = [];
93
+ parts.push(`**共命中 ${result.totalBlocks} 个失败块**,以下为前 ${result.blocks.length} 个:`);
94
+ result.blocks.forEach((b, i) => {
95
+ parts.push(`\n### 失败块 ${i + 1}(日志第 ${b.startLine} 行起)\n\`\`\`\n${b.text}\n\`\`\``);
96
+ });
97
+ if (result.droppedBlocks > 0) {
98
+ parts.push(`\n> ⚠️ 已省略 ${result.droppedBlocks} 个失败块(共 ${result.totalBlocks} 个)。需要时可用更小的范围重跑,或到 Actions 页面查看完整日志。`);
99
+ }
100
+ return parts.join('\n');
101
+ }
102
+ /**
103
+ * 无锚点时的回退:取尾部若干行。
104
+ *
105
+ * 为什么取尾部而不是头部:日志的失败结论在末尾(构建工具通常先刷编译输出、
106
+ * 最后才打印错误汇总),取头部基本只能拿到环境准备信息。
107
+ */
108
+ export function tailLines(text, count = 200) {
109
+ const src = typeof text === 'string' ? text : '';
110
+ if (!src)
111
+ return '';
112
+ const lines = src.split(/\r?\n/);
113
+ if (lines.length <= count)
114
+ return src;
115
+ return lines.slice(lines.length - count).join('\n');
116
+ }
@@ -55,3 +55,127 @@ export interface GitStatParsedResult {
55
55
  * 提取文件路径与 `X files changed, Y insertions(+), Z deletions(-)` 汇总。
56
56
  */
57
57
  export declare function parseGitStatText(statText: string): GitStatParsedResult | null;
58
+ /**
59
+ * 提取 `.git/config` 中所有 `[remote "..."]` 段内的 url。
60
+ *
61
+ * 为什么必须按段扫描而不是全文抓 `url = `:`[submodule "..."]` 段同样带 url
62
+ * (实测 `[submodule "lib"] url = https://github.com/x/y.git`)。全文正则会把
63
+ * 子模块的远程算进指纹,导致一次 `git submodule add` 就触发整个面板缓存的
64
+ * 无谓作废。子模块的 url 与本工作区关联哪个 GitHub 仓库毫无关系。
65
+ *
66
+ * 输出语义:保持文件中的出现顺序、已去重、不含空值。
67
+ * 排序交给调用方(指纹需要稳定顺序,但那是调用方的语义,纯函数不该替它决定)。
68
+ */
69
+ export declare function parseRemoteUrls(configText: string): string[];
70
+ /** 单个 remote 段的解析结果 */
71
+ export interface ParsedRemote {
72
+ name: string;
73
+ url: string;
74
+ }
75
+ /** `.git/config` + `HEAD` 的解析结果,是「这个工作区关联哪个仓库」的唯一事实源 */
76
+ export interface ParsedGitConfig {
77
+ /** 每个 remote 名对应的第一个 url(身份解析用;同名多 url 时取首个) */
78
+ remotes: ParsedRemote[];
79
+ /**
80
+ * 全部 remote url 行(指纹用)。
81
+ *
82
+ * 为什么与 remotes 分开:身份解析需要「remote 名 → url」的一对一映射,
83
+ * 而指纹需要「配置里出现过的所有 url」。同一个 remote 允许写多行 url
84
+ * (git 认首个,但配置确实是多行),若指纹也按名字去重,第二行 url 的
85
+ * 变化就检测不到——那会让「用户改了备用 url」这种情况静默漏检。
86
+ */
87
+ allUrls: string[];
88
+ /** 当前分支名(来自 HEAD 的 `ref: refs/heads/<name>`);detached HEAD 时为 null */
89
+ currentBranch: string | null;
90
+ /** `branch.<currentBranch>.remote` 的值;无配置或无当前分支时为 null */
91
+ branchRemote: string | null;
92
+ }
93
+ /**
94
+ * 解析 `.git/config` 与 `HEAD`,产出与 git 语义一致的远程视图。
95
+ *
96
+ * 为什么把 HEAD 一起解析:`branch.<name>.remote` 的 `<name>` 是**当前分支**,
97
+ * 不知道当前分支就无法按 git 的规则选出 remote,只能退化成「取第一个」——
98
+ * 那正是本次要修的错误。
99
+ *
100
+ * @param configText `.git/config` 全文
101
+ * @param headText HEAD 文件全文。worktree 场景必须传**局部 gitdir** 下的那个,
102
+ * 传共享目录的会拿到主工作树分支名(实测两者不同)。
103
+ */
104
+ export declare function parseGitConfig(configText: string, headText?: string | null): ParsedGitConfig;
105
+ /**
106
+ * 按 git 的规则选出「这个工作区关联的 remote」。
107
+ *
108
+ * 优先级与 git 一致:
109
+ * 1. `branch.<当前分支>.remote` —— 当前分支显式跟踪的 remote
110
+ * 2. `origin` —— git 的默认远程
111
+ * 3. 第一个 remote —— 兜底(保持既有行为)
112
+ *
113
+ * 特殊值 `.` 表示跟踪**本地分支**而非远程(实测可设置),跳过第 1 级——
114
+ * 拿 "." 去查 `[remote "."]` 必然查不到,且语义上它根本不是远程。
115
+ */
116
+ export declare function resolveRemoteTarget(cfg: ParsedGitConfig): ParsedRemote | null;
117
+ /**
118
+ * 从 remote url 中提取 GitHub 的 owner/repo。
119
+ *
120
+ * 覆盖形态(均实测于真实 clone / hand-written config):
121
+ * https://github.com/o/r.git
122
+ * git@github.com:o/r.git
123
+ * ssh://git@github.com/o/r.git
124
+ * https://user:token@github.com/o/r.git
125
+ * 带尾斜杠 / 不带 .git
126
+ *
127
+ * 非 github.com 的 host 一律返回 null(GHE 不在支持范围)。
128
+ * 为什么绝不"猜一个":错误身份比没有身份更有害——面板会显示一个
129
+ * 用户根本不认识的仓库,且所有后续判断都建立在错误前提上。
130
+ * 正则两端锚定,杜绝 `github.com.evil.com` 这类子串误匹配。
131
+ */
132
+ export declare function parseGithubUrl(url: string): {
133
+ owner: string;
134
+ repo: string;
135
+ } | null;
136
+ /**
137
+ * 从 remote url 中提取 host(小写)。
138
+ *
139
+ * 覆盖:https://host/o/r、git@host:o/r、ssh://git@host:2222/o/r、带 token 的 https。
140
+ * 无法解析返回 null。
141
+ *
142
+ * 为什么不直接复用 parseGithubUrl:本函数要处理**任意 host**(GHE / GitLab),
143
+ * 用于判断「这个 remote 是不是 GitHub」,而不是「这个 remote 属于哪个 GitHub 仓库」。
144
+ */
145
+ export declare function parseRemoteHost(url: string): string | null;
146
+ /**
147
+ * 判断工作区处于哪种远程布局。
148
+ *
149
+ * 为什么需要分类而不是顺序盲扫:S2 的兜底逻辑按 `.git/config` 的书写顺序
150
+ * 挑第一个能解析出 GitHub 身份的 remote,而用户执行 `git remote rename` /
151
+ * `git remote add` 的先后会改变这个顺序——同一个工作区在两次配置操作后
152
+ * 可能解析出不同仓库。分类后每种布局的处置规则固定,与顺序无关。
153
+ *
154
+ * 分类只看 host 与 git 选中的 remote,不看 URL 的仓库名——后者是身份层的事。
155
+ */
156
+ export declare function classifyRepoLayout(cfg: ParsedGitConfig): 'github-only' | 'github-mixed' | 'no-github' | 'no-remote';
157
+ /**
158
+ * 在混合布局中确定性地选出一个 GitHub remote。
159
+ *
160
+ * 规则(与书写顺序无关):
161
+ * 1. 名字为 `origin` 的优先
162
+ * 2. 否则按 remote 名字字典序最小的
163
+ *
164
+ * 为什么用字典序而不是「第一个出现的」:后者让同一个工作区的解析结果
165
+ * 依赖用户执行 git remote 命令的先后,属于不可复现的行为。
166
+ * 字典序是可复现、可断言的确定性规则。
167
+ */
168
+ export declare function pickGithubRemote(cfg: ParsedGitConfig): ParsedRemote | null;
169
+ /**
170
+ * 将整段 Git Unified Diff 纯文本按变动文件切分为字典映射。
171
+ *
172
+ * 为什么需要按文件切片:PR 可能会修改数十个文件,前端需要支持「点击某个文件即刻查看该文件改动」。
173
+ * 在内存做切片索引后,切换文件无需发起网络请求(0ms 响应),体验极致流畅。
174
+ *
175
+ * 键必须与 `gh pr view --json files` 返回的 `files[].path` **逐字一致**,
176
+ * 否则点击文件查不到 diff。因此需要处理 core.quotePath 的八进制转义。
177
+ *
178
+ * @param rawDiff gh pr diff 或 git diff 输出的原始文本
179
+ * @returns 映射字典:{ [filePath: string]: string (该文件的完整 diff 文本段) }
180
+ */
181
+ export declare function splitUnifiedDiff(rawDiff: string): Record<string, string>;