dsh-github-flow 0.4.0 → 0.6.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 (82) hide show
  1. package/README.en.md +91 -12
  2. package/README.md +91 -12
  3. package/docs/UPGRADE-NOTES.md +9 -12
  4. package/docs/architecture.md +63 -29
  5. package/docs/images/1.png +0 -0
  6. package/lib/api/routes-actions.js +268 -5
  7. package/lib/api/routes-overview.js +147 -29
  8. package/lib/cache.d.ts +77 -4
  9. package/lib/cache.js +114 -7
  10. package/lib/client.d.ts +9 -4
  11. package/lib/client.js +5293 -942
  12. package/lib/commands/gh.d.ts +6 -1
  13. package/lib/commands/gh.js +144 -71
  14. package/lib/executor/error-classifier.d.ts +55 -0
  15. package/lib/executor/error-classifier.js +199 -0
  16. package/lib/executor/git-parser.d.ts +157 -1
  17. package/lib/executor/git-parser.js +513 -4
  18. package/lib/executor.d.ts +147 -4
  19. package/lib/executor.js +866 -77
  20. package/lib/main.js +28 -4
  21. package/lib/renderers/agent-task.d.ts +16 -0
  22. package/lib/renderers/agent-task.js +107 -0
  23. package/lib/renderers/codespace.d.ts +16 -0
  24. package/lib/renderers/codespace.js +103 -0
  25. package/lib/renderers/discussion.d.ts +16 -0
  26. package/lib/renderers/discussion.js +101 -0
  27. package/lib/renderers/markdown.d.ts +4 -0
  28. package/lib/renderers/markdown.js +4 -0
  29. package/lib/renderers/project.d.ts +20 -0
  30. package/lib/renderers/project.js +128 -0
  31. package/lib/renderers/skill.d.ts +30 -0
  32. package/lib/renderers/skill.js +180 -0
  33. package/lib/renderers/status.d.ts +5 -0
  34. package/lib/renderers/status.js +52 -0
  35. package/lib/telemetry.d.ts +70 -0
  36. package/lib/telemetry.js +161 -0
  37. package/lib/tools/agent-task.d.ts +54 -0
  38. package/lib/tools/agent-task.js +135 -0
  39. package/lib/tools/codespace.d.ts +51 -0
  40. package/lib/tools/codespace.js +154 -0
  41. package/lib/tools/discussion.d.ts +70 -0
  42. package/lib/tools/discussion.js +195 -0
  43. package/lib/tools/help.js +129 -14
  44. package/lib/tools/image-utils.d.ts +27 -0
  45. package/lib/tools/image-utils.js +73 -3
  46. package/lib/tools/project.d.ts +80 -0
  47. package/lib/tools/project.js +241 -0
  48. package/lib/tools/repo.js +13 -1
  49. package/lib/tools/skill.d.ts +24 -1
  50. package/lib/tools/skill.js +115 -5
  51. package/lib/tools/status.d.ts +3 -2
  52. package/lib/tools/status.js +9 -4
  53. package/lib/types.d.ts +336 -0
  54. package/lib/ui/commit-modal.js +144 -14
  55. package/lib/ui/help-modal.d.ts +21 -0
  56. package/lib/ui/help-modal.js +300 -0
  57. package/lib/ui/issue-modal.d.ts +14 -0
  58. package/lib/ui/issue-modal.js +366 -0
  59. package/lib/ui/markdown-lite.d.ts +66 -0
  60. package/lib/ui/markdown-lite.js +312 -0
  61. package/lib/ui/network-banner.d.ts +12 -0
  62. package/lib/ui/network-banner.js +35 -0
  63. package/lib/ui/pr-modal.d.ts +17 -0
  64. package/lib/ui/pr-modal.js +516 -0
  65. package/lib/ui/repo-badges.d.ts +43 -0
  66. package/lib/ui/repo-badges.js +82 -0
  67. package/lib/ui/repo-selector.d.ts +14 -0
  68. package/lib/ui/repo-selector.js +112 -0
  69. package/lib/ui/storage.d.ts +56 -0
  70. package/lib/ui/storage.js +163 -0
  71. package/lib/ui/styles.d.ts +1 -1
  72. package/lib/ui/styles.js +1219 -0
  73. package/lib/ui/sync-settings-modal.js +1 -1
  74. package/lib/ui/upgrade-card.js +39 -14
  75. package/lib/ui/utils.d.ts +35 -0
  76. package/lib/ui/utils.js +172 -0
  77. package/lib/ui/workspace-panel.js +198 -31
  78. package/lib/upgrade-manager.d.ts +5 -1
  79. package/lib/upgrade-manager.js +24 -1
  80. package/lib/version.d.ts +1 -1
  81. package/lib/version.js +1 -1
  82. package/package.json +2 -2
package/lib/cache.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * 有上限的 TTL 缓存。
3
3
  *
4
- * 为什么独立成模块:缓存语义(过期、LRU 腾退、容量上限)是缺陷高发区,但嵌在路由文件里
5
- * 就无法单测——import 一个已执行过的路由模块拿不到干净实例。独立出来才能覆盖边界。
4
+ * 为什么独立成模块:缓存语义(过期、LRU 腾退、容量上限、身份失效)是缺陷高发区,
5
+ * 但嵌在路由文件里就无法单测——import 一个已执行过的路由模块拿不到干净实例。
6
+ * 独立出来才能覆盖边界。
6
7
  */
7
8
  export declare class TtlCache<T> {
8
9
  private ttlMs;
@@ -10,6 +11,16 @@ export declare class TtlCache<T> {
10
11
  private store;
11
12
  /** 容灾备份底座:记录最后一次成功的有效快照,不受 TTL 过期或主动 refresh 清空的影响 */
12
13
  private fallbackStore;
14
+ /**
15
+ * 每个 key 对应的 remote 配置指纹。
16
+ *
17
+ * 为什么独立于 store 而不是塞进 value:store 的 value 是业务数据 T,
18
+ * 混入簿记字段会污染 T 的类型、序列化与消费方断言。指纹是缓存层自己的
19
+ * 记账信息,不参与业务读取。
20
+ *
21
+ * 缺失(Map 中无该 key)= 调用方未提供指纹,此时永不作废(保守)。
22
+ */
23
+ private fingerprintStore;
13
24
  /**
14
25
  * @param ttlMs 命中有效期;由面向用户的 cacheTtlMs 配置驱动
15
26
  * @param maxEntries 容量上限。键通常是工作区路径,一次会话就可能引入若干键,
@@ -17,9 +28,71 @@ export declare class TtlCache<T> {
17
28
  */
18
29
  constructor(ttlMs: number, maxEntries?: number);
19
30
  get(key: string, now: number): T | undefined;
20
- set(key: string, data: T, now: number): void;
31
+ /**
32
+ * @param fingerprint 该 key 对应的 remote 配置指纹(可选)。
33
+ *
34
+ * 只在传入**非空字符串**时更新指纹;传入 undefined / null 一律保留旧值。
35
+ *
36
+ * 为什么 null 不能清除旧指纹:null 的真实含义是「这一次没读到」(config 被
37
+ * 占用、瞬时 IO 失败、路径暂不可达),不是「这个仓库没有 remote」。若把 null
38
+ * 当作清除,一次瞬时读取失败就会永久抹掉失效检测能力——之后 remote 真的变了
39
+ * 也永远发现不了,缓存里冻结的身份再也出不来。保留旧值时的行为是
40
+ * matchesFingerprint 返回 true(保守不作废),等下一次读到时再正确判定,
41
+ * 严格优于「永久失明」。
42
+ */
43
+ set(key: string, data: T, now: number, fingerprint?: string | null): void;
44
+ /** 三张表必须同步腾退,否则指纹会随 key 数量慢性增长 */
45
+ private evict;
21
46
  /** 获取最后一次有效成功的容灾底座数据(专供网络故障、超时时的 Fail-Safe 保全) */
22
47
  getFallback(key: string): T | undefined;
23
48
  get size(): number;
24
- clear(): void;
49
+ /**
50
+ * 清空缓存。
51
+ *
52
+ * @param options.keepFallback 是否保留 fallbackStore 容灾底座,默认 true。
53
+ * 默认保留是为了「网络抖动时宁可给旧数据也别白屏」;但用户显式点击
54
+ * 「重试刷新」时要的语义恰恰相反——宁可空白也要重来,此时必须传 false,
55
+ * 否则刷新动作会被容灾底座架空(这是真实踩到过的死循环:配置文件已变、
56
+ * 旧快照永久冻结,刷新按钮点了等于没点)。
57
+ */
58
+ clear(options?: {
59
+ keepFallback?: boolean;
60
+ }): void;
61
+ /**
62
+ * remote 配置指纹是否仍与当前事实一致。
63
+ *
64
+ * 为什么用指纹取代「身份字符串比对」:身份比对把解析器的正确性变成了缓存
65
+ * 有效性的前置条件——解析一偏(如双 remote 时取错 remote 段),比对永远
66
+ * 失配,缓存永久不命中,每次请求都重拉全部 gh 子进程。
67
+ * 指纹只回答「配置变了没有」,不依赖任何语义理解,因此对该类偏差免疫。
68
+ *
69
+ * 判定口径刻意保守:任一侧取不到指纹(离线、非 Git、无 remote)即视为一致。
70
+ */
71
+ matchesFingerprint(key: string, currentFingerprint: string | null | undefined): boolean;
72
+ /**
73
+ * 仅在指纹失配时作废该 key(含容灾底座)。返回是否真的作废。
74
+ * 与 matchesFingerprint 分开:判定与处置是两件事,调用点语义更清晰。
75
+ */
76
+ invalidateIfFingerprintChanged(key: string, currentFingerprint: string | null | undefined): boolean;
77
+ /**
78
+ * 身份校验:缓存里的「仓库身份」是否仍与当前事实一致。
79
+ *
80
+ * @deprecated 自 S1 起由 matchesFingerprint 取代,**当前生产代码与测试均无调用点**。
81
+ * 保留的唯一目的是 S1 的回滚路径:若指纹方案出问题,把 routes-overview
82
+ * 的两行调用切回身份比对即可,无需恢复已删除的代码。
83
+ * 计划在 S2(离线解析对齐 git 规则)完成后连同 getRepoIdentityFromLocal 一并删除。
84
+ *
85
+ * 为什么必须取代它:该方法依赖 `.git/config` 的解析结果,而解析器在多 remote
86
+ * 布局下会取错段(实测),导致身份恒失配 → 缓存永久失效。
87
+ *
88
+ * @param key 工作区路径
89
+ * @param currentNameWithOwner 当前从本地 .git/config 解析出的 owner/repo
90
+ */
91
+ matchesIdentity(key: string, currentNameWithOwner: string | null | undefined): boolean;
92
+ /**
93
+ * 仅在身份失配时作废该键。返回是否真的作废。
94
+ *
95
+ * @deprecated 同 matchesIdentity,改用 invalidateIfFingerprintChanged。
96
+ */
97
+ invalidateIfIdentityChanged(key: string, currentNameWithOwner: string | null | undefined): boolean;
25
98
  }
package/lib/cache.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * 有上限的 TTL 缓存。
3
3
  *
4
- * 为什么独立成模块:缓存语义(过期、LRU 腾退、容量上限)是缺陷高发区,但嵌在路由文件里
5
- * 就无法单测——import 一个已执行过的路由模块拿不到干净实例。独立出来才能覆盖边界。
4
+ * 为什么独立成模块:缓存语义(过期、LRU 腾退、容量上限、身份失效)是缺陷高发区,
5
+ * 但嵌在路由文件里就无法单测——import 一个已执行过的路由模块拿不到干净实例。
6
+ * 独立出来才能覆盖边界。
6
7
  */
7
8
  export class TtlCache {
8
9
  ttlMs;
@@ -10,6 +11,16 @@ export class TtlCache {
10
11
  store = new Map();
11
12
  /** 容灾备份底座:记录最后一次成功的有效快照,不受 TTL 过期或主动 refresh 清空的影响 */
12
13
  fallbackStore = new Map();
14
+ /**
15
+ * 每个 key 对应的 remote 配置指纹。
16
+ *
17
+ * 为什么独立于 store 而不是塞进 value:store 的 value 是业务数据 T,
18
+ * 混入簿记字段会污染 T 的类型、序列化与消费方断言。指纹是缓存层自己的
19
+ * 记账信息,不参与业务读取。
20
+ *
21
+ * 缺失(Map 中无该 key)= 调用方未提供指纹,此时永不作废(保守)。
22
+ */
23
+ fingerprintStore = new Map();
13
24
  /**
14
25
  * @param ttlMs 命中有效期;由面向用户的 cacheTtlMs 配置驱动
15
26
  * @param maxEntries 容量上限。键通常是工作区路径,一次会话就可能引入若干键,
@@ -32,22 +43,43 @@ export class TtlCache {
32
43
  this.store.set(key, hit);
33
44
  return hit.data;
34
45
  }
35
- set(key, data, now) {
46
+ /**
47
+ * @param fingerprint 该 key 对应的 remote 配置指纹(可选)。
48
+ *
49
+ * 只在传入**非空字符串**时更新指纹;传入 undefined / null 一律保留旧值。
50
+ *
51
+ * 为什么 null 不能清除旧指纹:null 的真实含义是「这一次没读到」(config 被
52
+ * 占用、瞬时 IO 失败、路径暂不可达),不是「这个仓库没有 remote」。若把 null
53
+ * 当作清除,一次瞬时读取失败就会永久抹掉失效检测能力——之后 remote 真的变了
54
+ * 也永远发现不了,缓存里冻结的身份再也出不来。保留旧值时的行为是
55
+ * matchesFingerprint 返回 true(保守不作废),等下一次读到时再正确判定,
56
+ * 严格优于「永久失明」。
57
+ */
58
+ set(key, data, now, fingerprint) {
36
59
  this.store.set(key, { data, time: now });
37
60
  this.fallbackStore.set(key, data);
61
+ if (typeof fingerprint === 'string' && fingerprint) {
62
+ this.fingerprintStore.set(key, fingerprint);
63
+ }
38
64
  while (this.store.size > this.maxEntries) {
39
65
  const oldest = this.store.keys().next();
40
66
  if (oldest.done)
41
67
  break;
42
- this.store.delete(oldest.value);
68
+ this.evict(oldest.value);
43
69
  }
44
70
  while (this.fallbackStore.size > this.maxEntries) {
45
71
  const oldest = this.fallbackStore.keys().next();
46
72
  if (oldest.done)
47
73
  break;
48
- this.fallbackStore.delete(oldest.value);
74
+ this.evict(oldest.value);
49
75
  }
50
76
  }
77
+ /** 三张表必须同步腾退,否则指纹会随 key 数量慢性增长 */
78
+ evict(key) {
79
+ this.store.delete(key);
80
+ this.fallbackStore.delete(key);
81
+ this.fingerprintStore.delete(key);
82
+ }
51
83
  /** 获取最后一次有效成功的容灾底座数据(专供网络故障、超时时的 Fail-Safe 保全) */
52
84
  getFallback(key) {
53
85
  return this.fallbackStore.get(key);
@@ -55,8 +87,83 @@ export class TtlCache {
55
87
  get size() {
56
88
  return this.store.size;
57
89
  }
58
- clear() {
59
- // 清空活动缓存以迫使后续请求重新发起网络拉取,但保留 fallbackStore 用于网络故障容灾
90
+ /**
91
+ * 清空缓存。
92
+ *
93
+ * @param options.keepFallback 是否保留 fallbackStore 容灾底座,默认 true。
94
+ * 默认保留是为了「网络抖动时宁可给旧数据也别白屏」;但用户显式点击
95
+ * 「重试刷新」时要的语义恰恰相反——宁可空白也要重来,此时必须传 false,
96
+ * 否则刷新动作会被容灾底座架空(这是真实踩到过的死循环:配置文件已变、
97
+ * 旧快照永久冻结,刷新按钮点了等于没点)。
98
+ */
99
+ clear(options = {}) {
60
100
  this.store.clear();
101
+ if (options.keepFallback === false) {
102
+ this.fallbackStore.clear();
103
+ this.fingerprintStore.clear();
104
+ }
105
+ }
106
+ /**
107
+ * remote 配置指纹是否仍与当前事实一致。
108
+ *
109
+ * 为什么用指纹取代「身份字符串比对」:身份比对把解析器的正确性变成了缓存
110
+ * 有效性的前置条件——解析一偏(如双 remote 时取错 remote 段),比对永远
111
+ * 失配,缓存永久不命中,每次请求都重拉全部 gh 子进程。
112
+ * 指纹只回答「配置变了没有」,不依赖任何语义理解,因此对该类偏差免疫。
113
+ *
114
+ * 判定口径刻意保守:任一侧取不到指纹(离线、非 Git、无 remote)即视为一致。
115
+ */
116
+ matchesFingerprint(key, currentFingerprint) {
117
+ const stored = this.fingerprintStore.get(key);
118
+ if (!stored || !currentFingerprint)
119
+ return true;
120
+ return stored === currentFingerprint;
121
+ }
122
+ /**
123
+ * 仅在指纹失配时作废该 key(含容灾底座)。返回是否真的作废。
124
+ * 与 matchesFingerprint 分开:判定与处置是两件事,调用点语义更清晰。
125
+ */
126
+ invalidateIfFingerprintChanged(key, currentFingerprint) {
127
+ if (this.matchesFingerprint(key, currentFingerprint))
128
+ return false;
129
+ this.evict(key);
130
+ return true;
131
+ }
132
+ /**
133
+ * 身份校验:缓存里的「仓库身份」是否仍与当前事实一致。
134
+ *
135
+ * @deprecated 自 S1 起由 matchesFingerprint 取代,**当前生产代码与测试均无调用点**。
136
+ * 保留的唯一目的是 S1 的回滚路径:若指纹方案出问题,把 routes-overview
137
+ * 的两行调用切回身份比对即可,无需恢复已删除的代码。
138
+ * 计划在 S2(离线解析对齐 git 规则)完成后连同 getRepoIdentityFromLocal 一并删除。
139
+ *
140
+ * 为什么必须取代它:该方法依赖 `.git/config` 的解析结果,而解析器在多 remote
141
+ * 布局下会取错段(实测),导致身份恒失配 → 缓存永久失效。
142
+ *
143
+ * @param key 工作区路径
144
+ * @param currentNameWithOwner 当前从本地 .git/config 解析出的 owner/repo
145
+ */
146
+ matchesIdentity(key, currentNameWithOwner) {
147
+ if (!currentNameWithOwner)
148
+ return true;
149
+ const cached = this.fallbackStore.get(key) ?? this.store.get(key)?.data;
150
+ if (!cached)
151
+ return true;
152
+ const cachedRepo = cached?.repo;
153
+ const cachedName = typeof cachedRepo?.nameWithOwner === 'string' ? cachedRepo.nameWithOwner : '';
154
+ if (!cachedName)
155
+ return true;
156
+ return cachedName.toLowerCase() === currentNameWithOwner.toLowerCase();
157
+ }
158
+ /**
159
+ * 仅在身份失配时作废该键。返回是否真的作废。
160
+ *
161
+ * @deprecated 同 matchesIdentity,改用 invalidateIfFingerprintChanged。
162
+ */
163
+ invalidateIfIdentityChanged(key, currentNameWithOwner) {
164
+ if (this.matchesIdentity(key, currentNameWithOwner))
165
+ return false;
166
+ this.evict(key);
167
+ return true;
61
168
  }
62
169
  }
package/lib/client.d.ts CHANGED
@@ -8,16 +8,21 @@ declare global {
8
8
  };
9
9
  }
10
10
  }
11
- import { formatRelativeTime, rollupCiStatus, reviewDecisionBadge, eventLabel } from './ui/utils.js';
11
+ import { formatRelativeTime, rollupCiStatus, reviewDecisionBadge, eventLabel, parseGitStatText } from './ui/utils.js';
12
12
  import { GITHUB_PANEL_CSS, injectStyles } from './ui/styles.js';
13
13
  import { renderOctocat, renderGitIcon, renderOctocatBadge, renderPluginBadge } from './ui/icons.js';
14
- import { getLocalSyncSettings, saveLocalSyncSettings, getStoredGlobalOverview, saveStoredGlobalOverview, clearStoredGlobalOverview, getCacheStorageMeta, getRightbarStorageKey, getStoredRightbarSnapshot, saveStoredRightbarData } from './ui/storage.js';
14
+ import { getLocalSyncSettings, saveLocalSyncSettings, getStoredGlobalOverview, saveStoredGlobalOverview, clearStoredGlobalOverview, getCacheStorageMeta, getRightbarStorageKey, getStoredRightbarSnapshot, saveStoredRightbarData, getStoredHeaderCardsVisible, saveStoredHeaderCardsVisible, HEADER_CARDS_STORAGE_KEY, getStoredRightbarStateFilter, saveStoredRightbarStateFilter, PR_STATE_FILTERS, ISSUE_STATE_FILTERS } from './ui/storage.js';
15
15
  import { BranchSelectorDropdown } from './ui/branch-selector.js';
16
+ import { RepoSelectorDropdown } from './ui/repo-selector.js';
16
17
  import { AuthGuideCard } from './ui/auth-guide-card.js';
17
18
  import { UpgradeNotificationCard } from './ui/upgrade-card.js';
18
- import { NetworkAlertBanner } from './ui/network-banner.js';
19
+ import { NetworkAlertBanner, RepoNoticeBar } from './ui/network-banner.js';
20
+ import { resolveRepoBadges } from './ui/repo-badges.js';
19
21
  import { SyncStatusBadge } from './ui/sync-status-badge.js';
20
22
  import { CommitDetailModal } from './ui/commit-modal.js';
23
+ import { PrDetailModal } from './ui/pr-modal.js';
24
+ import { IssueDetailModal } from './ui/issue-modal.js';
21
25
  import { GlobalSyncSettingsModal } from './ui/sync-settings-modal.js';
26
+ import { GithubHelpModal } from './ui/help-modal.js';
22
27
  import { GithubWorkspacePanel } from './ui/workspace-panel.js';
23
- export { formatRelativeTime, rollupCiStatus, reviewDecisionBadge, eventLabel, GITHUB_PANEL_CSS, injectStyles, renderOctocat, renderGitIcon, renderOctocatBadge, renderPluginBadge, getLocalSyncSettings, saveLocalSyncSettings, getStoredGlobalOverview, saveStoredGlobalOverview, clearStoredGlobalOverview, getCacheStorageMeta, getRightbarStorageKey, getStoredRightbarSnapshot, saveStoredRightbarData, BranchSelectorDropdown, AuthGuideCard, UpgradeNotificationCard, NetworkAlertBanner, SyncStatusBadge, CommitDetailModal, GlobalSyncSettingsModal, GithubWorkspacePanel, };
28
+ export { formatRelativeTime, rollupCiStatus, reviewDecisionBadge, eventLabel, parseGitStatText, GITHUB_PANEL_CSS, injectStyles, renderOctocat, renderGitIcon, renderOctocatBadge, renderPluginBadge, getLocalSyncSettings, saveLocalSyncSettings, getStoredGlobalOverview, saveStoredGlobalOverview, clearStoredGlobalOverview, getCacheStorageMeta, getRightbarStorageKey, getStoredRightbarSnapshot, saveStoredRightbarData, getStoredHeaderCardsVisible, saveStoredHeaderCardsVisible, HEADER_CARDS_STORAGE_KEY, getStoredRightbarStateFilter, saveStoredRightbarStateFilter, PR_STATE_FILTERS, ISSUE_STATE_FILTERS, BranchSelectorDropdown, RepoSelectorDropdown, AuthGuideCard, UpgradeNotificationCard, NetworkAlertBanner, RepoNoticeBar, resolveRepoBadges, SyncStatusBadge, CommitDetailModal, PrDetailModal, IssueDetailModal, GlobalSyncSettingsModal, GithubHelpModal, GithubWorkspacePanel, };