dsh-github-flow 0.2.0 → 0.3.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 (44) hide show
  1. package/README.en.md +68 -22
  2. package/README.md +69 -23
  3. package/docs/UPGRADE-NOTES.md +93 -0
  4. package/docs/architecture.md +76 -31
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2.png +0 -0
  7. package/docs/images/3.png +0 -0
  8. package/docs/images/4.png +0 -0
  9. package/docs/npm-publish-guide.md +27 -5
  10. package/lib/api/routes-actions.d.ts +18 -4
  11. package/lib/api/routes-actions.js +143 -19
  12. package/lib/api/routes-lifecycle.d.ts +1 -1
  13. package/lib/api/routes-lifecycle.js +53 -2
  14. package/lib/api/routes-overview.js +104 -1
  15. package/lib/client.js +991 -55
  16. package/lib/commands/gh.js +11 -2
  17. package/lib/executor.d.ts +118 -1
  18. package/lib/executor.js +479 -12
  19. package/lib/main.js +10 -0
  20. package/lib/renderers/markdown.d.ts +12 -1
  21. package/lib/renderers/markdown.js +150 -16
  22. package/lib/tools/help.js +144 -11
  23. package/lib/tools/image-utils.d.ts +79 -0
  24. package/lib/tools/image-utils.js +247 -0
  25. package/lib/tools/issue.d.ts +18 -4
  26. package/lib/tools/issue.js +161 -6
  27. package/lib/tools/pr.d.ts +12 -4
  28. package/lib/tools/pr.js +128 -7
  29. package/lib/tools/repo.d.ts +20 -4
  30. package/lib/tools/repo.js +196 -4
  31. package/lib/tools/run.d.ts +8 -0
  32. package/lib/tools/run.js +104 -3
  33. package/lib/tools/secret.d.ts +5 -0
  34. package/lib/tools/secret.js +108 -23
  35. package/lib/types.d.ts +85 -1
  36. package/lib/update-settings.d.ts +18 -0
  37. package/lib/update-settings.js +60 -0
  38. package/lib/updater.d.ts +32 -1
  39. package/lib/updater.js +117 -9
  40. package/lib/upgrade-manager.d.ts +70 -1
  41. package/lib/upgrade-manager.js +346 -46
  42. package/lib/version.d.ts +1 -1
  43. package/lib/version.js +1 -1
  44. package/package.json +1 -1
package/lib/updater.js CHANGED
@@ -1,4 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
1
5
  import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL, NPM_REGISTRY_MIRROR, NPM_REGISTRY_OFFICIAL, } from './version.js';
6
+ /**
7
+ * 检测当前 DSH 运行 profile 名(升级命令的 --profile 必填项,官方 CLI 语义:
8
+ * `dsh plugin --profile <name> add <pkg>` 在 profile 目录内转发给 pnpm)。
9
+ * 探测优先级(从权威到兜底,不瞎猜):
10
+ * 1. DSH_PROFILE 环境变量——launcher 启动时固定注入(桌面版=desktop,
11
+ * `dsh --profile web` 启动的 Web 版=web),是运行进程对自身 profile 的事实源;
12
+ * 2. DSH_PROFILE_DIR 路径含 desktop 时判定 desktop;
13
+ * 3. 兜底探测 ~/.dsh/profiles 下实际存在的 profile 目录(web > default 顺序),
14
+ * 两者皆无才回落 'default'(dsh CLI 的默认 profile 名)。
15
+ */
16
+ export function detectProfileName() {
17
+ const profile = (process.env.DSH_PROFILE || '').trim();
18
+ if (profile)
19
+ return profile;
20
+ const dir = (process.env.DSH_PROFILE_DIR || '').trim().toLowerCase();
21
+ if (dir.includes('desktop'))
22
+ return 'desktop';
23
+ // 0.5 最可靠的事实源兜底:本插件模块必然安装在 <profile>/node_modules/ 下,
24
+ // 从自身 import.meta.url 逐级上溯找到 profiles/<name> 段即为当前 profile。
25
+ // 优先于目录枚举:即使机器上装了多个 profile 也能指认「正在运行的这一个」。
26
+ try {
27
+ let cur = path.dirname(fileURLToPath(import.meta.url));
28
+ for (let i = 0; i < 8; i++) {
29
+ const parent = path.dirname(cur);
30
+ if (path.basename(parent) === 'profiles' && parent !== cur) {
31
+ const name = path.basename(cur);
32
+ if (name && name !== 'node_modules')
33
+ return name;
34
+ }
35
+ cur = parent;
36
+ if (!cur || cur === path.dirname(cur))
37
+ break;
38
+ }
39
+ }
40
+ catch { /* 非 file:// 上下文(理论不发生)继续走兜底 */ }
41
+ // 环境变量缺失(自定义启动方式):按实际存在的 profile 目录兜底
42
+ try {
43
+ const profilesRoot = path.join(os.homedir(), '.dsh', 'profiles');
44
+ if (fs.existsSync(profilesRoot)) {
45
+ const names = fs.readdirSync(profilesRoot).filter((n) => {
46
+ try {
47
+ return fs.statSync(path.join(profilesRoot, n)).isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ });
53
+ if (names.includes('web'))
54
+ return 'web';
55
+ if (names.length > 0 && !names.includes('default'))
56
+ return names[0];
57
+ }
58
+ }
59
+ catch { /* 探测失败走最终兜底 */ }
60
+ return 'default';
61
+ }
2
62
  /**
3
63
  * 极简语义化版本大小比较函数 (latest > current 时返回 true)
4
64
  *
@@ -43,12 +103,20 @@ export class UpdateChecker {
43
103
  inFlightPromise = null;
44
104
  intervalMs;
45
105
  customRegistry;
106
+ /** 配置总开关(checkUpdate: false 时的自动探测熔断),见 check() 内注释 */
107
+ enabled;
108
+ /**
109
+ * 当前更新源模式。null = 未初始化(走 legacy 双源竞速,兼容 store 不存在
110
+ * 且无 Cordis registryUrl 的老安装);一旦 UI 保存过任何 mode 即为权威。
111
+ */
112
+ sourceMode = null;
46
113
  constructor(options = {}) {
47
114
  // 默认缓存 24 小时,杜绝高频击穿 npm
48
115
  this.intervalMs = typeof options.intervalMs === 'number' && options.intervalMs > 0
49
116
  ? options.intervalMs
50
117
  : 86_400_000;
51
118
  this.customRegistry = options.customRegistry;
119
+ this.enabled = options.enabled !== false;
52
120
  }
53
121
  /**
54
122
  * 应用代理到 npm 探测的 fetch 链路。
@@ -60,19 +128,39 @@ export class UpdateChecker {
60
128
  manager.applyFetchDispatcher();
61
129
  }
62
130
  /**
63
- * 同步获取当前已知的版本状态(0ms 纯内存读取,绝不发外网请求)
131
+ * 运行时切换更新源(UI 保存即调用,无需插件 reload)。
132
+ * 为什么必须失效缓存:24h 缓存里存的是旧源的结果,用户切源后若不清缓存,
133
+ * 首次 check() 仍返回旧源的 latestVersion——用户感知「切换无效」。
134
+ * 'off' 与 enabled 熔断独立并存:enabled 来自 Cordis 静态配置(缺省 true),
135
+ * off 来自 UI 持久化设置;任一为「关」都熔断自动探测。
136
+ */
137
+ applySettings(mode) {
138
+ this.sourceMode = mode;
139
+ this.cachedInfo = null;
140
+ // 切换到 off 时,途中旧源的请求任其自然完成(结果已无人消费,无害);
141
+ // 不 abort inFlightPromise:abort 会让并发等待方拿到异常,得不偿失。
142
+ }
143
+ /** 当前生效的更新源模式(供命令行/升级链路展示与对齐) */
144
+ getSourceMode() {
145
+ return this.sourceMode;
146
+ }
147
+ /**
148
+ * 同步获取当前已知的版本状态(0ms 纯内存读取,绝不发外网请求)。
149
+ * 兜底命令同样按当前 profile 生成:网络探测失败时复制命令也必须开箱可用。
64
150
  */
65
151
  getQuickStatus() {
66
152
  if (this.cachedInfo) {
67
153
  return this.cachedInfo;
68
154
  }
155
+ const profile = detectProfileName();
156
+ const primary = `dsh plugin --profile ${profile} add ${PLUGIN_PACKAGE_NAME}@latest`;
69
157
  return {
70
158
  currentVersion: CURRENT_PLUGIN_VERSION,
71
159
  hasUpdate: false,
72
- upgradeCommand: `pnpm dsh plugin update ${PLUGIN_PACKAGE_NAME}`,
160
+ upgradeCommand: primary,
73
161
  allCommands: {
74
- dshCli: `pnpm dsh plugin update ${PLUGIN_PACKAGE_NAME}`,
75
- profile: `dsh plugin --profile default add ${PLUGIN_PACKAGE_NAME}@latest`,
162
+ dshCli: primary,
163
+ profile: primary,
76
164
  pnpm: `pnpm add ${PLUGIN_PACKAGE_NAME}@latest`,
77
165
  npm: `npm install -g ${PLUGIN_PACKAGE_NAME}@latest`,
78
166
  },
@@ -85,6 +173,14 @@ export class UpdateChecker {
85
173
  * @param force 是否忽略 24h 缓存强制穿透探测
86
174
  */
87
175
  async check(force = false) {
176
+ // 自动探测熔断(两个独立开关,任一为「关」即熔断):
177
+ // 1. enabled=false:Cordis 静态配置 checkUpdate: false;
178
+ // 2. sourceMode='off':UI「更新设置」选择了不检查更新。
179
+ // 熔断语义统一为「关自动提示,不关能力」:force(/gh upgrade、设置页保存后的
180
+ // 即时验证)与一键升级路由不经过此门——用户显式动作必须真探。
181
+ if ((!this.enabled || this.sourceMode === 'off') && !force) {
182
+ return this.getQuickStatus();
183
+ }
88
184
  const now = Date.now();
89
185
  // 1. 命中有效缓存直接返回
90
186
  if (!force && this.cachedInfo && now - this.cachedInfo.checkedAt < this.intervalMs) {
@@ -104,9 +200,17 @@ export class UpdateChecker {
104
200
  */
105
201
  async performCheck(now) {
106
202
  const fallback = this.getQuickStatus();
107
- // 双通道策略:用户自定义源 > 国内淘宝镜像 (2.5s极速) > 官方 npmjs 源 (3.5s兜底)
203
+ // 端点选择:UI 保存的源模式为权威(单源严格不互备——静默回落另一源等于
204
+ // 背叛用户选择:镜像同步滞后正是选官方的理由,反之亦然);无 UI 设置时
205
+ // 走 legacy 行为(自定义源 > 镜像竞速官方兜底)保持老安装兼容。
108
206
  const endpoints = [];
109
- if (this.customRegistry) {
207
+ if (this.sourceMode === 'official') {
208
+ endpoints.push(`${NPM_REGISTRY_OFFICIAL}/${PLUGIN_PACKAGE_NAME}/latest`);
209
+ }
210
+ else if (this.sourceMode === 'mirror') {
211
+ endpoints.push(`${NPM_REGISTRY_MIRROR}/${PLUGIN_PACKAGE_NAME}/latest`);
212
+ }
213
+ else if (this.customRegistry) {
110
214
  endpoints.push(`${this.customRegistry.replace(/\/$/, '')}/${PLUGIN_PACKAGE_NAME}/latest`);
111
215
  }
112
216
  else {
@@ -150,14 +254,18 @@ export class UpdateChecker {
150
254
  }
151
255
  const hasUpdate = isNewerVersion(CURRENT_PLUGIN_VERSION, latestVersion);
152
256
  const targetPkg = `${PLUGIN_PACKAGE_NAME}@${latestVersion}`;
257
+ // 命令按真实 profile 生成(P1 修复):dsh CLI 的 --profile 为必填项,
258
+ // 桌面端用户复制 'pnpm dsh plugin add ...' 会直接报 required option 错误。
259
+ const profile = detectProfileName();
260
+ const primary = `dsh plugin --profile ${profile} add ${targetPkg}`;
153
261
  this.cachedInfo = {
154
262
  currentVersion: CURRENT_PLUGIN_VERSION,
155
263
  latestVersion,
156
264
  hasUpdate,
157
- upgradeCommand: `pnpm dsh plugin add ${targetPkg}`,
265
+ upgradeCommand: primary,
158
266
  allCommands: {
159
- dshCli: `pnpm dsh plugin add ${targetPkg}`,
160
- profile: `dsh plugin --profile default add ${targetPkg}`,
267
+ dshCli: primary,
268
+ profile: primary,
161
269
  pnpm: `pnpm add ${targetPkg}`,
162
270
  npm: `npm install -g ${targetPkg}`,
163
271
  },
@@ -10,7 +10,45 @@ import type { ProxyManager } from './proxy.js';
10
10
  * 3. 动态将当前 node 运行时所在目录 (path.dirname(process.execPath)) 及 Mac 常用包管理器路径注入 PATH 首端,
11
11
  * 彻底实现 macOS 与 Linux 环境下子进程对 npm / pnpm 命令的 100% 自愈发现。
12
12
  */
13
- export declare function buildCrossPlatformEnv(): NodeJS.ProcessEnv;
13
+ export declare function buildCrossPlatformEnv(registryOverride?: string): NodeJS.ProcessEnv;
14
+ /**
15
+ * 探测 dsh CLI 是否对当前进程可达(PATH 或已知安装位置)。
16
+ * 为什么必须预检:桌面端 GUI 启动时 dsh 常不在 PATH,旧实现用 '||' 兜底到
17
+ * `npm install -g`——装到全局对 profile 依赖毫无作用,表现为「一键升级无效果」
18
+ * 的静默失败。预检不可达时直接给出带完整命令的明确指引,宁可见失败不可假成功。
19
+ */
20
+ export declare function probeDshCliAvailable(env: NodeJS.ProcessEnv): Promise<{
21
+ ok: boolean;
22
+ hint?: string;
23
+ }>;
24
+ /**
25
+ * 定位桌面版自带的 pnpm 分发包(pnpm.mjs)。
26
+ * 官方事实(apps/desktop/README):Desktop 在 resources/runtime/primary-runtime/
27
+ * dependencies/pnpm 携带完整 pnpm 分发,「包操作不要求 PATH 上存在 pnpm」。
28
+ * dsh CLI 的 plugin 子命令本质就是「进 profile 目录转发 pnpm」——CLI 不可达时
29
+ * 我们可以自己定位 profile 目录直调这份 pnpm,完全绕开 dsh CLI 依赖。
30
+ * 候选链:a. DSH_BUNDLED_PNPM 显式覆盖;b. argv[0] 宿主二进制推导(桌面版固定布局)。
31
+ */
32
+ export declare function locateBundledPnpm(): string | null;
33
+ /**
34
+ * 读取 profile 中已实际安装的本插件版本(实证校验的事实源)。
35
+ * 读不到(目录不存在/无产物/JSON 损坏)返回 null——调用方据此决定是否放行:
36
+ * 无法证伪时不在成功文案里虚报版本号,但不阻断(避免文件系统受限环境下误杀真成功)。
37
+ */
38
+ export declare function readInstalledVersion(profileDir?: string): string | null;
39
+ /**
40
+ * 把插件包名补写进 profile 的 dsh.profile.bundles(幂等)。
41
+ * 为什么需要:pnpm add 只改 dependencies;dsh plugin add 还会追加 bundles 列表
42
+ * (官方语义:无 dsh.bundle 声明的包只作普通依赖不激活)。直调 pnpm 的降级路径
43
+ * 必须补齐这一步,插件才会真正进入 profile 组合。
44
+ * 安全性(本机实测):pnpm add 对 dsh.profile 自定义字段完整保真,因此补写放在
45
+ * pnpm 成功后执行,读写同一份 package.json 不会互相破坏;写入用「读-改-写全量
46
+ * JSON」保持 2 空格缩进 + 尾换行与 dsh CLI 产物一致。
47
+ */
48
+ export declare function ensureBundleListed(profileDir: string, pkgName: string): {
49
+ ok: boolean;
50
+ error?: string;
51
+ };
14
52
  /**
15
53
  * 自动升级执行引擎
16
54
  *
@@ -27,7 +65,18 @@ export declare class UpgradeManager {
27
65
  private logBuffer;
28
66
  /** 代理管理器(可选):注入后一键升级的 npm/pnpm 子进程走用户代理 */
29
67
  private proxyManager?;
68
+ /** 更新源模式查询(可选):一键升级的 NPM_CONFIG_REGISTRY 跟随用户在设置页选择的源 */
69
+ private getUpdateMode;
30
70
  attachProxyManager(manager: ProxyManager): void;
71
+ /** 注入更新源查询函数(而非静态值):用户切换源后无需重新装配即实时生效 */
72
+ attachUpdateModeQuery(query: () => 'official' | 'mirror' | 'off' | null): void;
73
+ /**
74
+ * 按当前更新源推导 NPM_CONFIG_REGISTRY。
75
+ * 为什么跟随源而非写死:用户选官方源的理由(镜像同步滞后)对安装同样成立——
76
+ * 选镜像源装的可能是旧版。off 模式下一键升级仍可用(关提示不关能力),
77
+ * 其 registry 落官方源(能力路径取最权威的源)。
78
+ */
79
+ private resolveRegistry;
31
80
  /**
32
81
  * 探测当前插件是否是以本地 Git 源码软链接方式运行
33
82
  */
@@ -51,6 +100,26 @@ export declare class UpgradeManager {
51
100
  * 获取当前升级状态
52
101
  */
53
102
  getStatus(): UpgradeStatusResult;
103
+ /**
104
+ * 级别 2 降级路径:直调桌面版自带 pnpm 安装包(execFile 数组参数隔离,无 shell)。
105
+ * 输出与退出码经回调上抛,由调用方决定 bundles 补写与状态机收尾。
106
+ */
107
+ private runPnpmDirect;
108
+ /**
109
+ * 升级结果统一收尾(状态机 + 互斥锁复位)。级别 1/级别 2 共用,
110
+ * 保证 lastResult、isUpgrading、返回文案的形态完全一致。
111
+ *
112
+ * 关键防线一:退出码 0 ≠ 安装成功。本函数在退出码为 0 时再做一次实证校验——
113
+ * 读 profile 的 node_modules/<pkg>/package.json 实际版本,与目标版本比对。
114
+ * 不匹配(网络劫持返回空产物、registry 异常静默跳过安装、包管理器缓存错乱
115
+ * 等场景)一律判失败并如实报「实际安装版本」,绝不把没装上说成装好了。
116
+ *
117
+ * 关键防线二:失败文案不内嵌完整命令。前端升级卡片常驻「📋 复制命令」按钮
118
+ * (复制的命令已按当前 profile 生成),文字里贴长命令既让用户手动描选困难、
119
+ * 也无法区分 Web/桌面环境谁敲得了终端。文案只引导按钮 + 按环境给可行出路:
120
+ * 桌面版未注册 CLI 的用户走官方「管理 dsh 命令」菜单,其余走复制命令。
121
+ */
122
+ private finishUpgrade;
54
123
  /**
55
124
  * 执行一键升级核心流程
56
125
  */