dsh-github-flow 0.1.8 → 0.2.1

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 (69) hide show
  1. package/README.en.md +162 -58
  2. package/README.md +167 -60
  3. package/docs/UPGRADE-NOTES.md +93 -0
  4. package/docs/architecture.md +47 -17
  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 +26 -3
  10. package/lib/api/route-helpers.d.ts +47 -0
  11. package/lib/api/route-helpers.js +134 -0
  12. package/lib/api/routes-actions.d.ts +23 -0
  13. package/lib/api/routes-actions.js +239 -0
  14. package/lib/api/routes-lifecycle.d.ts +9 -0
  15. package/lib/api/routes-lifecycle.js +126 -0
  16. package/lib/api/routes-overview.d.ts +11 -0
  17. package/lib/api/routes-overview.js +227 -0
  18. package/lib/api/routes-proxy.d.ts +18 -0
  19. package/lib/api/routes-proxy.js +63 -0
  20. package/lib/api/routes.d.ts +16 -6
  21. package/lib/api/routes.js +14 -504
  22. package/lib/client.js +785 -90
  23. package/lib/commands/gh.js +11 -2
  24. package/lib/executor.d.ts +17 -0
  25. package/lib/executor.js +146 -8
  26. package/lib/main.d.ts +1 -0
  27. package/lib/main.js +49 -9
  28. package/lib/proxy.d.ts +87 -0
  29. package/lib/proxy.js +275 -0
  30. package/lib/renderers/markdown.d.ts +29 -0
  31. package/lib/renderers/markdown.js +520 -0
  32. package/lib/tools/api.d.ts +4 -0
  33. package/lib/tools/api.js +14 -4
  34. package/lib/tools/help.d.ts +31 -0
  35. package/lib/tools/help.js +224 -0
  36. package/lib/tools/image-utils.d.ts +79 -0
  37. package/lib/tools/image-utils.js +247 -0
  38. package/lib/tools/issue.d.ts +40 -4
  39. package/lib/tools/issue.js +167 -19
  40. package/lib/tools/pr.d.ts +29 -4
  41. package/lib/tools/pr.js +144 -18
  42. package/lib/tools/release.d.ts +89 -0
  43. package/lib/tools/release.js +219 -0
  44. package/lib/tools/repo.d.ts +24 -4
  45. package/lib/tools/repo.js +114 -14
  46. package/lib/tools/run.d.ts +22 -1
  47. package/lib/tools/run.js +74 -14
  48. package/lib/tools/secret.d.ts +60 -0
  49. package/lib/tools/secret.js +175 -0
  50. package/lib/tools/skill.d.ts +63 -0
  51. package/lib/tools/skill.js +146 -0
  52. package/lib/types.d.ts +24 -1
  53. package/lib/update-settings.d.ts +18 -0
  54. package/lib/update-settings.js +60 -0
  55. package/lib/updater.d.ts +40 -1
  56. package/lib/updater.js +126 -9
  57. package/lib/upgrade-manager.d.ts +74 -1
  58. package/lib/upgrade-manager.js +356 -46
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/package.json +3 -2
  62. package/lib/client/icons.d.ts +0 -13
  63. package/lib/client/icons.js +0 -41
  64. package/lib/client/index.d.ts +0 -3
  65. package/lib/client/index.js +0 -36
  66. package/lib/client/panel.d.ts +0 -1
  67. package/lib/client/panel.js +0 -102
  68. package/lib/client/style.d.ts +0 -1
  69. package/lib/client/style.js +0 -555
@@ -1,4 +1,4 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawn, execFile } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
@@ -14,7 +14,7 @@ import { PLUGIN_PACKAGE_NAME } from './version.js';
14
14
  * 3. 动态将当前 node 运行时所在目录 (path.dirname(process.execPath)) 及 Mac 常用包管理器路径注入 PATH 首端,
15
15
  * 彻底实现 macOS 与 Linux 环境下子进程对 npm / pnpm 命令的 100% 自愈发现。
16
16
  */
17
- export function buildCrossPlatformEnv() {
17
+ export function buildCrossPlatformEnv(registryOverride) {
18
18
  const currentPath = process.env.PATH || '';
19
19
  const extraPaths = [];
20
20
  const isMac = process.platform === 'darwin';
@@ -26,6 +26,50 @@ export function buildCrossPlatformEnv() {
26
26
  }
27
27
  catch { }
28
28
  }
29
+ // 1.5 DSH CLI 所在目录自愈(P2 修复):一键升级实际要执行 `dsh plugin ...`,
30
+ // 但桌面版由 GUI 启动时不继承终端 PATH,子进程常找不到 dsh(实测本机 dsh 在
31
+ // D:\dsh-zm\resources\runtime\cli\bin,node 在 D:\CodeApps\nodejs——两者分离,
32
+ // 且安装根与 ~/.dsh 无任何路径关系)。
33
+ // 探测链(从准确到模糊):
34
+ // a. DSH_CLI_BIN 显式环境变量(用户/宿主可预设);
35
+ // b. process.argv[0] 宿主二进制推导——桌面版 argv[0] 即
36
+ // <安装根>/DeepSeek Harness.exe(或 node 跑 CLI 时的可执行),
37
+ // 其 dirname 拼上 resources/runtime/cli/bin 是桌面版固定布局;
38
+ // c. 从 DSH_PROFILE_DIR 逐级上溯(覆盖 ~/.dsh 布局的安装形态);
39
+ // d. ~/.dsh/runtime/cli/bin 常见兜底。
40
+ const dshBinCandidates = [];
41
+ if (process.env.DSH_CLI_BIN)
42
+ dshBinCandidates.push(process.env.DSH_CLI_BIN);
43
+ try {
44
+ if (process.argv[0]) {
45
+ const hostDir = path.dirname(path.resolve(process.argv[0]));
46
+ dshBinCandidates.push(path.join(hostDir, 'resources', 'runtime', 'cli', 'bin'));
47
+ }
48
+ }
49
+ catch { }
50
+ try {
51
+ const profileDir = (process.env.DSH_PROFILE_DIR || '').trim();
52
+ if (profileDir) {
53
+ // profiles/<name> -> 上溯到 ~/.dsh,再拼 runtime/cli/bin
54
+ let cur = path.resolve(profileDir);
55
+ for (let i = 0; i < 3; i++) {
56
+ dshBinCandidates.push(path.join(cur, 'runtime', 'cli', 'bin'));
57
+ cur = path.dirname(cur);
58
+ }
59
+ }
60
+ const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
61
+ dshBinCandidates.push(path.join(dshHome, 'runtime', 'cli', 'bin'));
62
+ }
63
+ catch { }
64
+ for (const cand of dshBinCandidates) {
65
+ try {
66
+ if (fs.existsSync(cand)) {
67
+ extraPaths.push(cand);
68
+ break;
69
+ }
70
+ }
71
+ catch { }
72
+ }
29
73
  // 2. macOS 专有软件与包管理器安装路径
30
74
  if (isMac) {
31
75
  extraPaths.push('/opt/homebrew/bin'); // Apple Silicon Homebrew
@@ -47,11 +91,118 @@ export function buildCrossPlatformEnv() {
47
91
  return {
48
92
  ...process.env,
49
93
  PATH: uniquePaths.join(separator),
50
- NPM_CONFIG_REGISTRY: 'https://registry.npmmirror.com', // 优先国内源加速
94
+ NPM_CONFIG_REGISTRY: registryOverride ?? 'https://registry.npmjs.org',
51
95
  LANG: 'zh_CN.UTF-8',
52
96
  LC_ALL: 'zh_CN.UTF-8',
53
97
  };
54
98
  }
99
+ /**
100
+ * 探测 dsh CLI 是否对当前进程可达(PATH 或已知安装位置)。
101
+ * 为什么必须预检:桌面端 GUI 启动时 dsh 常不在 PATH,旧实现用 '||' 兜底到
102
+ * `npm install -g`——装到全局对 profile 依赖毫无作用,表现为「一键升级无效果」
103
+ * 的静默失败。预检不可达时直接给出带完整命令的明确指引,宁可见失败不可假成功。
104
+ */
105
+ export async function probeDshCliAvailable(env) {
106
+ const isWin = process.platform === 'win32';
107
+ const probeCmd = isWin ? 'where' : 'which';
108
+ return new Promise((resolve) => {
109
+ try {
110
+ execFile(probeCmd, ['dsh'], { env, timeout: 5_000 }, (err, stdout) => {
111
+ if (!err && String(stdout).trim()) {
112
+ resolve({ ok: true });
113
+ }
114
+ else {
115
+ resolve({ ok: false, hint: `未在 PATH 中发现 dsh CLI(${probeCmd} dsh 无输出)` });
116
+ }
117
+ });
118
+ }
119
+ catch (e) {
120
+ resolve({ ok: false, hint: `dsh CLI 探测失败: ${e?.message || e}` });
121
+ }
122
+ });
123
+ }
124
+ /**
125
+ * 定位桌面版自带的 pnpm 分发包(pnpm.mjs)。
126
+ * 官方事实(apps/desktop/README):Desktop 在 resources/runtime/primary-runtime/
127
+ * dependencies/pnpm 携带完整 pnpm 分发,「包操作不要求 PATH 上存在 pnpm」。
128
+ * dsh CLI 的 plugin 子命令本质就是「进 profile 目录转发 pnpm」——CLI 不可达时
129
+ * 我们可以自己定位 profile 目录直调这份 pnpm,完全绕开 dsh CLI 依赖。
130
+ * 候选链:a. DSH_BUNDLED_PNPM 显式覆盖;b. argv[0] 宿主二进制推导(桌面版固定布局)。
131
+ */
132
+ export function locateBundledPnpm() {
133
+ const candidates = [];
134
+ if (process.env.DSH_BUNDLED_PNPM)
135
+ candidates.push(process.env.DSH_BUNDLED_PNPM);
136
+ try {
137
+ if (process.argv[0]) {
138
+ const hostDir = path.dirname(path.resolve(process.argv[0]));
139
+ candidates.push(path.join(hostDir, 'resources', 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'));
140
+ }
141
+ }
142
+ catch { }
143
+ for (const c of candidates) {
144
+ try {
145
+ if (fs.existsSync(c))
146
+ return c;
147
+ }
148
+ catch { }
149
+ }
150
+ return null;
151
+ }
152
+ /**
153
+ * 读取 profile 中已实际安装的本插件版本(实证校验的事实源)。
154
+ * 读不到(目录不存在/无产物/JSON 损坏)返回 null——调用方据此决定是否放行:
155
+ * 无法证伪时不在成功文案里虚报版本号,但不阻断(避免文件系统受限环境下误杀真成功)。
156
+ */
157
+ export function readInstalledVersion(profileDir) {
158
+ try {
159
+ const dir = profileDir || defaultProfileDir();
160
+ const pkgPath = path.join(dir, 'node_modules', PLUGIN_PACKAGE_NAME, 'package.json');
161
+ if (!fs.existsSync(pkgPath))
162
+ return null;
163
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
164
+ return typeof pkg.version === 'string' ? pkg.version : null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ function defaultProfileDir() {
171
+ const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
172
+ return process.env.DSH_PROFILE_DIR || path.join(dshHome, 'profiles', process.env.DSH_PROFILE || 'default');
173
+ }
174
+ /**
175
+ * 把插件包名补写进 profile 的 dsh.profile.bundles(幂等)。
176
+ * 为什么需要:pnpm add 只改 dependencies;dsh plugin add 还会追加 bundles 列表
177
+ * (官方语义:无 dsh.bundle 声明的包只作普通依赖不激活)。直调 pnpm 的降级路径
178
+ * 必须补齐这一步,插件才会真正进入 profile 组合。
179
+ * 安全性(本机实测):pnpm add 对 dsh.profile 自定义字段完整保真,因此补写放在
180
+ * pnpm 成功后执行,读写同一份 package.json 不会互相破坏;写入用「读-改-写全量
181
+ * JSON」保持 2 空格缩进 + 尾换行与 dsh CLI 产物一致。
182
+ */
183
+ export function ensureBundleListed(profileDir, pkgName) {
184
+ try {
185
+ const pkgPath = path.join(profileDir, 'package.json');
186
+ if (!fs.existsSync(pkgPath))
187
+ return { ok: false, error: `profile package.json 不存在: ${pkgPath}` };
188
+ const raw = fs.readFileSync(pkgPath, 'utf8');
189
+ const pkg = JSON.parse(raw);
190
+ if (!pkg.dsh)
191
+ pkg.dsh = {};
192
+ if (!pkg.dsh.profile)
193
+ pkg.dsh.profile = {};
194
+ if (!Array.isArray(pkg.dsh.profile.bundles))
195
+ pkg.dsh.profile.bundles = [];
196
+ if (pkg.dsh.profile.bundles.includes(pkgName))
197
+ return { ok: true }; // 幂等:已在列表
198
+ pkg.dsh.profile.bundles.push(pkgName);
199
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
200
+ return { ok: true };
201
+ }
202
+ catch (e) {
203
+ return { ok: false, error: `bundles 补写失败: ${e?.message || e}` };
204
+ }
205
+ }
55
206
  /**
56
207
  * 自动升级执行引擎
57
208
  *
@@ -68,6 +219,29 @@ export class UpgradeManager {
68
219
  state: 'idle',
69
220
  };
70
221
  logBuffer = [];
222
+ /** 代理管理器(可选):注入后一键升级的 npm/pnpm 子进程走用户代理 */
223
+ proxyManager;
224
+ /** 更新源模式查询(可选):一键升级的 NPM_CONFIG_REGISTRY 跟随用户在设置页选择的源 */
225
+ getUpdateMode = null;
226
+ attachProxyManager(manager) {
227
+ this.proxyManager = manager;
228
+ }
229
+ /** 注入更新源查询函数(而非静态值):用户切换源后无需重新装配即实时生效 */
230
+ attachUpdateModeQuery(query) {
231
+ this.getUpdateMode = query;
232
+ }
233
+ /**
234
+ * 按当前更新源推导 NPM_CONFIG_REGISTRY。
235
+ * 为什么跟随源而非写死:用户选官方源的理由(镜像同步滞后)对安装同样成立——
236
+ * 选镜像源装的可能是旧版。off 模式下一键升级仍可用(关提示不关能力),
237
+ * 其 registry 落官方源(能力路径取最权威的源)。
238
+ */
239
+ resolveRegistry() {
240
+ const mode = this.getUpdateMode ? this.getUpdateMode() : null;
241
+ if (mode === 'mirror')
242
+ return 'https://registry.npmmirror.com';
243
+ return 'https://registry.npmjs.org';
244
+ }
71
245
  /**
72
246
  * 探测当前插件是否是以本地 Git 源码软链接方式运行
73
247
  */
@@ -118,6 +292,94 @@ export class UpgradeManager {
118
292
  log: this.logBuffer.join('\n'),
119
293
  };
120
294
  }
295
+ /**
296
+ * 级别 2 降级路径:直调桌面版自带 pnpm 安装包(execFile 数组参数隔离,无 shell)。
297
+ * 输出与退出码经回调上抛,由调用方决定 bundles 补写与状态机收尾。
298
+ */
299
+ runPnpmDirect(pnpmMjs, profileDir, pkgWithVer, env, done) {
300
+ const chunks = [];
301
+ try {
302
+ const child = spawn(process.execPath, [pnpmMjs, 'add', pkgWithVer], {
303
+ cwd: profileDir,
304
+ env,
305
+ windowsHide: true,
306
+ });
307
+ const collect = (b) => {
308
+ const t = b.toString();
309
+ chunks.push(t);
310
+ this.logBuffer.push(t);
311
+ if (this.logBuffer.length > 200)
312
+ this.logBuffer.shift();
313
+ };
314
+ child.stdout?.on('data', collect);
315
+ child.stderr?.on('data', collect);
316
+ const timer = setTimeout(() => child.kill(), 120_000);
317
+ child.on('close', (code) => {
318
+ clearTimeout(timer);
319
+ done({ ok: code === 0, log: chunks.join('') });
320
+ });
321
+ child.on('error', (err) => {
322
+ clearTimeout(timer);
323
+ done({ ok: false, log: [...chunks, String(err?.message || err)].join('') });
324
+ });
325
+ }
326
+ catch (e) {
327
+ done({ ok: false, log: `spawn pnpm 失败: ${e?.message || e}` });
328
+ }
329
+ }
330
+ /**
331
+ * 升级结果统一收尾(状态机 + 互斥锁复位)。级别 1/级别 2 共用,
332
+ * 保证 lastResult、isUpgrading、返回文案的形态完全一致。
333
+ *
334
+ * 关键防线一:退出码 0 ≠ 安装成功。本函数在退出码为 0 时再做一次实证校验——
335
+ * 读 profile 的 node_modules/<pkg>/package.json 实际版本,与目标版本比对。
336
+ * 不匹配(网络劫持返回空产物、registry 异常静默跳过安装、包管理器缓存错乱
337
+ * 等场景)一律判失败并如实报「实际安装版本」,绝不把没装上说成装好了。
338
+ *
339
+ * 关键防线二:失败文案不内嵌完整命令。前端升级卡片常驻「📋 复制命令」按钮
340
+ * (复制的命令已按当前 profile 生成),文字里贴长命令既让用户手动描选困难、
341
+ * 也无法区分 Web/桌面环境谁敲得了终端。文案只引导按钮 + 按环境给可行出路:
342
+ * 桌面版未注册 CLI 的用户走官方「管理 dsh 命令」菜单,其余走复制命令。
343
+ */
344
+ finishUpgrade(result, targetVersion, env, startedAt, profileDir) {
345
+ const completedAt = Date.now();
346
+ const wantVersion = targetVersion || 'latest';
347
+ const pkgWithVer = targetVersion ? `${PLUGIN_PACKAGE_NAME}@${targetVersion}` : `${PLUGIN_PACKAGE_NAME}@latest`;
348
+ // 出路文案按环境分流:桌面版保留官方菜单通道(未注册 CLI 的用户唯一可行路径)
349
+ const actionHint = env.isDesktop
350
+ ? '点击「📋 复制命令」后在系统终端运行;若终端无 dsh 命令,请先在 DSH 桌面端菜单 → 管理 dsh 命令… → 安装'
351
+ : '点击「📋 复制命令」后在部署 DSH 的机器终端运行';
352
+ if (result.ok) {
353
+ // 实证校验:产物版本才是唯一可信事实源
354
+ const installed = readInstalledVersion(profileDir);
355
+ if (installed && targetVersion && installed !== targetVersion) {
356
+ this.lastResult = {
357
+ state: 'failed',
358
+ targetVersion,
359
+ message: `升级失败:命令正常结束,但 profile 中实际安装的版本是 ${installed}(目标 ${targetVersion}),未完成目标版本安装。${actionHint},或稍后重试一键升级。`,
360
+ completedAt,
361
+ environment: env.isDesktop ? 'desktop' : 'web',
362
+ };
363
+ this.isUpgrading = false;
364
+ return;
365
+ }
366
+ const verSuffix = installed ? `(实证校验通过:${installed})` : '';
367
+ const successMsg = env.isDesktop
368
+ ? `升级成功!已安装 ${pkgWithVer} 到桌面版 Profile${verSuffix}。需重启 DSH 桌面客户端彻底生效后端工具,或刷新当前界面。`
369
+ : `升级成功!已安装 ${pkgWithVer}${verSuffix}。需重启 DSH 服务彻底生效后端工具,或刷新网页先体验新版界面。`;
370
+ this.lastResult = { state: 'success', targetVersion: wantVersion, message: successMsg, completedAt, environment: env.isDesktop ? 'desktop' : 'web' };
371
+ }
372
+ else {
373
+ this.lastResult = {
374
+ state: 'failed',
375
+ targetVersion,
376
+ message: `升级失败(安装命令返回非零退出码)。${targetVersion ? `目标版本 ${targetVersion} 未安装成功。` : ''}${actionHint},或稍后重试一键升级。`,
377
+ completedAt,
378
+ environment: env.isDesktop ? 'desktop' : 'web',
379
+ };
380
+ }
381
+ this.isUpgrading = false;
382
+ }
121
383
  /**
122
384
  * 执行一键升级核心流程
123
385
  */
@@ -160,27 +422,88 @@ export class UpgradeManager {
160
422
  startedAt,
161
423
  environment: env.isDesktop ? 'desktop' : 'web',
162
424
  };
425
+ // 两级执行策略(官方事实:桌面版的 dsh CLI 注册是可选操作,大量桌面用户
426
+ // 未注册命令;而 Desktop 自带完整 pnpm 分发包,「包操作不要求 PATH 上存在
427
+ // pnpm」,dsh plugin 本质是「进 profile 目录转发 pnpm + 补写 bundles 列表」):
428
+ // 级别 1:dsh CLI 可达(Web 版终端起 / 桌面版已注册命令)→ 走 CLI,行为
429
+ // 与官方完全一致(含 bundles 维护);
430
+ // 级别 2:CLI 不可达但能定位自带 pnpm 与 profile 目录 → 直调 pnpm add +
431
+ // 自行补写 bundles,完全绕开 CLI 依赖;
432
+ // 兜底:两者皆不可用 → 给官方「管理 dsh 命令」菜单指引(安装后重试即可)。
433
+ // 注意:任何提前失败分支必须复位 isUpgrading(safeSettle 不经过这些路径)。
434
+ // registry 按用户「更新设置」选择的源解析(每次升级执行时实时取,切源后无需重新装配)。
435
+ const probeEnv = { ...buildCrossPlatformEnv(this.resolveRegistry()), ...(this.proxyManager?.getEnv('package-manager') || {}) };
436
+ const dshProbe = await probeDshCliAvailable(probeEnv);
437
+ let usePnpmFallback = false;
438
+ if (!dshProbe.ok) {
439
+ const bundledPnpm = locateBundledPnpm();
440
+ const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
441
+ const profileDir = env.profileDir || path.join(dshHome, 'profiles', env.profile || 'default');
442
+ if (bundledPnpm && fs.existsSync(profileDir)) {
443
+ usePnpmFallback = true;
444
+ this.logBuffer.push(`[fallback] dsh CLI 不可达,降级为直调自带 pnpm: ${bundledPnpm}`);
445
+ }
446
+ else {
447
+ this.isUpgrading = false;
448
+ // 官方指引(apps/desktop/README「终端命令」):桌面版菜单提供「管理 dsh
449
+ // 命令…」的安装/修复入口,注册后即可在终端使用 dsh。指引一次安装永久解决。
450
+ // 官方指引(apps/desktop/README「终端命令」):桌面版菜单提供「管理 dsh
451
+ // 命令…」的安装/修复入口,注册后即可在终端使用 dsh。一次安装永久解决。
452
+ // 文案不内嵌命令(引导复制按钮),且此场景多为桌面版——Web 版通常自带 CLI。
453
+ const menuPath = 'DSH 桌面端菜单 → 管理 dsh 命令… → 安装';
454
+ const failMsg = `未能自动定位可用的安装通道(${dshProbe.hint || 'dsh CLI 不可用'},且未找到桌面版自带的 pnpm)。请通过官方入口注册命令:${menuPath}(注册后重试一键升级即可);或点击「📋 复制命令」获取手动升级命令。`;
455
+ this.lastResult = {
456
+ state: 'failed',
457
+ targetVersion,
458
+ message: failMsg,
459
+ completedAt: Date.now(),
460
+ environment: env.isDesktop ? 'desktop' : 'web',
461
+ };
462
+ return {
463
+ ok: false,
464
+ message: failMsg,
465
+ needRestart: false,
466
+ environment: env.isDesktop ? 'desktop' : 'web',
467
+ log: `Both dsh CLI and bundled pnpm unavailable. Official menu: ${menuPath}`,
468
+ };
469
+ }
470
+ }
163
471
  return new Promise((resolve) => {
164
472
  const isWin = process.platform === 'win32';
165
473
  let command = '';
166
474
  let executionCwd = process.cwd();
167
- // 根据环境类型分流升级指令(跨平台安全路径):
475
+ if (usePnpmFallback) {
476
+ // 级别 2:直调桌面版自带 pnpm(execFile 数组参数,不经 shell,无注入面)。
477
+ // 升级成功后由 close 回调补写 bundles 列表(见下方 code === 0 分支)。
478
+ const bundledPnpm = locateBundledPnpm();
479
+ const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
480
+ executionCwd = env.profileDir || path.join(dshHome, 'profiles', env.profile || 'default');
481
+ this.runPnpmDirect(bundledPnpm, executionCwd, pkgWithVer, probeEnv, (result) => {
482
+ if (result.ok) {
483
+ const listed = ensureBundleListed(executionCwd, PLUGIN_PACKAGE_NAME);
484
+ if (!listed.ok) {
485
+ this.logBuffer.push(`[warn] ${listed.error}(依赖已安装;重启后如未生效,请在菜单「管理 dsh 命令」注册 CLI 后重试)`);
486
+ }
487
+ else {
488
+ this.logBuffer.push(`[fallback] 已补写 bundles 列表: ${PLUGIN_PACKAGE_NAME}`);
489
+ }
490
+ }
491
+ this.finishUpgrade(result, targetVersion, env, startedAt, executionCwd);
492
+ });
493
+ return;
494
+ }
495
+ // 级别 1:dsh CLI(官方语义 `dsh plugin --profile <name> add <pkg>` 在 profile
496
+ // 目录内转发 pnpm,自动维护 dependencies 与 bundles)。失败不静默滑入
497
+ // npm install —— 装到全局对 profile 依赖毫无作用,只制造假成功。
168
498
  if (env.isDesktop) {
169
- // 场景 A:DSH 桌面版环境(macOS / Windows / Linux 桌面端通用)
499
+ command = `dsh plugin --profile desktop add ${pkgWithVer}`;
170
500
  const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
171
- const fallbackDesktopCwd = env.profileDir || path.join(dshHome, 'profiles', 'desktop');
172
- if (fs.existsSync(fallbackDesktopCwd)) {
173
- executionCwd = fallbackDesktopCwd;
174
- command = `dsh plugin --profile desktop add ${pkgWithVer} || npm install ${pkgWithVer} --save`;
175
- }
176
- else {
177
- command = `dsh plugin --profile desktop add ${pkgWithVer} || npm install -g ${pkgWithVer}`;
178
- }
501
+ executionCwd = env.profileDir || path.join(dshHome, 'profiles', 'desktop');
179
502
  }
180
503
  else {
181
504
  // 场景 B:Web 版 / 开发者终端 CLI 版环境
182
505
  const profile = env.profile || 'default';
183
- command = `pnpm dsh plugin add ${pkgWithVer} || dsh plugin --profile ${profile} add ${pkgWithVer} || npm install -g ${pkgWithVer}`;
506
+ command = `dsh plugin --profile ${profile} add ${pkgWithVer}`;
184
507
  }
185
508
  // 跨平台 Shell 选型与参数调度:
186
509
  // - Windows: 采用 cmd.exe (/d /s /c),完美原生支持 '||' 且冷启动仅 ~10ms,规避 powershell 5.1 语法报错;
@@ -200,7 +523,12 @@ export class UpgradeManager {
200
523
  resolve(result);
201
524
  };
202
525
  // 注入跨平台增强环境(尤其治愈 macOS GUI 启动丢失 PATH 的痛点)
203
- const childEnv = buildCrossPlatformEnv();
526
+ // 代理环境变量按 applyToPackageManager 开关合并:npm/pnpm 遵循 HTTPS_PROXY,
527
+ // 与 gh 链路同一管理器,保证「设置代理后升级也走代理」的体验一致性。
528
+ const childEnv = {
529
+ ...buildCrossPlatformEnv(this.resolveRegistry()),
530
+ ...(this.proxyManager?.getEnv('package-manager') || {}),
531
+ };
204
532
  const child = spawn(shell, args, {
205
533
  cwd: executionCwd,
206
534
  windowsHide: true,
@@ -208,7 +536,7 @@ export class UpgradeManager {
208
536
  });
209
537
  const timeoutTimer = setTimeout(() => {
210
538
  child.kill();
211
- const timeoutMsg = '升级命令执行超时(60 秒),可能是网络阻塞或包管理器锁占用,建议在终端手动执行升级。';
539
+ const timeoutMsg = '升级命令执行超时(60 秒),可能是网络阻塞或包管理器锁占用。可稍后重试一键升级,或点击「📋 复制命令」手动执行。';
212
540
  this.lastResult = {
213
541
  state: 'failed',
214
542
  targetVersion,
@@ -238,47 +566,29 @@ export class UpgradeManager {
238
566
  });
239
567
  child.on('close', (code) => {
240
568
  clearTimeout(timeoutTimer);
241
- const completedAt = Date.now();
242
- if (code === 0) {
243
- const successMsg = env.isDesktop
244
- ? `升级成功!已安装最新依赖 ${pkgWithVer} 到桌面版 Profile。需重启 DSH 桌面客户端彻底生效后端工具,或刷新当前界面。`
245
- : `升级成功!已安装最新依赖 ${pkgWithVer}。需重启 DSH 服务彻底生效后端工具,或刷新网页先体验新版界面。`;
246
- this.lastResult = {
247
- state: 'success',
248
- targetVersion,
249
- message: successMsg,
250
- completedAt,
251
- environment: env.isDesktop ? 'desktop' : 'web',
252
- };
253
- safeSettle({
569
+ // 级别 1 收尾也走实证校验(与级别 2 同一防线):退出码 0 后读产物版本,
570
+ // 与目标不符即判失败——杜绝 dsh 命令静默未安装却报成功的假象。
571
+ this.finishUpgrade({ ok: code === 0 }, targetVersion, env, startedAt, executionCwd);
572
+ const finalResult = this.lastResult.state === 'success'
573
+ ? {
254
574
  ok: true,
255
- message: successMsg,
575
+ message: this.lastResult.message || '升级成功',
256
576
  needRestart: true,
257
577
  environment: env.isDesktop ? 'desktop' : 'web',
258
578
  log: this.logBuffer.join('\n'),
259
- });
260
- }
261
- else {
262
- const failedMsg = `升级退出码异常 (${code}),可能需要管理员权限或在宿主环境终端手动执行。`;
263
- this.lastResult = {
264
- state: 'failed',
265
- targetVersion,
266
- message: failedMsg,
267
- completedAt,
268
- environment: env.isDesktop ? 'desktop' : 'web',
269
- };
270
- safeSettle({
579
+ }
580
+ : {
271
581
  ok: false,
272
- message: failedMsg,
582
+ message: this.lastResult.message || `升级退出码异常 (${code})`,
273
583
  needRestart: false,
274
584
  environment: env.isDesktop ? 'desktop' : 'web',
275
585
  log: this.logBuffer.join('\n'),
276
- });
277
- }
586
+ };
587
+ safeSettle(finalResult);
278
588
  });
279
589
  child.on('error', (err) => {
280
590
  clearTimeout(timeoutTimer);
281
- const errMsg = `无法启动安装子进程: ${err.message}`;
591
+ const errMsg = `升级失败:无法启动安装子进程(${err.message})。可重试一键升级,或点击「📋 复制命令」手动执行。`;
282
592
  this.lastResult = {
283
593
  state: 'failed',
284
594
  targetVersion,
package/lib/version.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * 2. 避免在 ESM 运行时动态读取 package.json 引入 fs 权限和路径解析边界问题;
7
7
  * 3. 方便在 CI 发布流水线或 build 脚本中自动化校准。
8
8
  */
9
- export declare const CURRENT_PLUGIN_VERSION = "0.1.8";
9
+ export declare const CURRENT_PLUGIN_VERSION = "0.2.1";
10
10
  export declare const PLUGIN_PACKAGE_NAME = "dsh-github-flow";
11
11
  export declare const NPM_PACKAGE_URL = "https://www.npmjs.com/package/dsh-github-flow";
12
12
  export declare const NPM_REGISTRY_MIRROR = "https://registry.npmmirror.com";
package/lib/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 2. 避免在 ESM 运行时动态读取 package.json 引入 fs 权限和路径解析边界问题;
7
7
  * 3. 方便在 CI 发布流水线或 build 脚本中自动化校准。
8
8
  */
9
- export const CURRENT_PLUGIN_VERSION = '0.1.8';
9
+ export const CURRENT_PLUGIN_VERSION = '0.2.1';
10
10
  export const PLUGIN_PACKAGE_NAME = 'dsh-github-flow';
11
11
  export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-github-flow';
12
12
  export const NPM_REGISTRY_MIRROR = 'https://registry.npmmirror.com';
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-github-flow",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "icon": "icon.svg",
5
- "description": "GitHub Flow: Native GitHub CLI (gh) integration for DeepSeek Harness — Agent Tools, Slash Commands, and Web UI Workspace",
5
+ "description": "GitHub 工作流引擎:为 DeepSeek Harness 提供原生 GitHub CLI (gh) 集成——9 大 Agent 领域工具(PR 审查合并、Issue 任务流转、CI 失败诊断、发版交付、密钥管理、Agent Skills 生态)、/gh 人类快捷指令、Web UI 全景驾驶舱与一键跨平台自动升级;零凭证侵入复用宿主机 gh 登录态,内置 Token 防溢出截断、高危操作二次确认与密文安全输入通道。",
6
6
  "type": "module",
7
7
  "main": "./lib/main.js",
8
8
  "repository": {
@@ -30,6 +30,7 @@
30
30
  ],
31
31
  "scripts": {
32
32
  "build": "tsc && node scripts/build.mjs",
33
+ "test": "node test/run-all.mjs",
33
34
  "prepare": "pnpm run build"
34
35
  },
35
36
  "dsh": {
@@ -1,13 +0,0 @@
1
- /**
2
- * GitHub 官方高精度的 Invertocat 矢量路径
3
- */
4
- export declare const GITHUB_OCTOCAT_PATH = "M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.72 6.56 11.52C4.78 11.32 2.92 10.63 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.32 9.47 11.52C9.76 11.77 10.01 12.25 10.01 13C10.01 14.08 10 14.95 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z";
5
- /**
6
- * 基础矢量 Logo
7
- */
8
- export declare function createOctocatSvg(size?: number, color?: string): any;
9
- /**
10
- * 现代暗黑科技感徽标 Artwork(用于卡片与主要入口)
11
- * 带有暗夜流光微渐变与精致白描
12
- */
13
- export declare function createOctocatBadge(size?: number): any;
@@ -1,41 +0,0 @@
1
- /**
2
- * GitHub 官方高精度的 Invertocat 矢量路径
3
- */
4
- export const GITHUB_OCTOCAT_PATH = "M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.72 6.56 11.52C4.78 11.32 2.92 10.63 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.32 9.47 11.52C9.76 11.77 10.01 12.25 10.01 13C10.01 14.08 10 14.95 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z";
5
- /**
6
- * 基础矢量 Logo
7
- */
8
- export function createOctocatSvg(size = 18, color = "currentColor") {
9
- return React.createElement("svg", {
10
- viewBox: "0 0 16 16",
11
- width: size,
12
- height: size,
13
- fill: color,
14
- style: { flexShrink: 0, display: "inline-block", verticalAlign: "middle" }
15
- }, React.createElement("path", {
16
- fillRule: "evenodd",
17
- clipRule: "evenodd",
18
- d: GITHUB_OCTOCAT_PATH
19
- }));
20
- }
21
- /**
22
- * 现代暗黑科技感徽标 Artwork(用于卡片与主要入口)
23
- * 带有暗夜流光微渐变与精致白描
24
- */
25
- export function createOctocatBadge(size = 36) {
26
- var iconSize = Math.round(size * 0.58);
27
- return React.createElement("div", {
28
- style: {
29
- display: "flex",
30
- alignItems: "center",
31
- justifyContent: "center",
32
- width: `${size}px`,
33
- height: `${size}px`,
34
- borderRadius: "10px",
35
- background: "linear-gradient(135deg, #2ea043 0%, #1f6feb 100%)", // GitHub 科技绿蓝微渐变
36
- boxShadow: "0 2px 8px rgba(31, 111, 235, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.2)",
37
- flexShrink: 0,
38
- position: "relative"
39
- }
40
- }, createOctocatSvg(iconSize, "#ffffff"));
41
- }
@@ -1,3 +0,0 @@
1
- export declare const name = "dsh-github-flow";
2
- export declare const inject: readonly ['slots'];
3
- export declare function apply(ctx: any): void;
@@ -1,36 +0,0 @@
1
- import { GITHUB_PANEL_CSS } from './style.js';
2
- import { createGithubPanelComponent } from './panel.js';
3
- export const name = 'dsh-github-flow';
4
- export const inject = ['slots'];
5
- export function apply(ctx) {
6
- // 1. 注入样式
7
- if (typeof styles !== 'undefined' && styles.insert) {
8
- styles.insert(GITHUB_PANEL_CSS);
9
- }
10
- else if (typeof document !== 'undefined') {
11
- const styleEl = document.createElement('style');
12
- styleEl.textContent = GITHUB_PANEL_CSS;
13
- document.head.appendChild(styleEl);
14
- }
15
- // 2. 侧边栏图标组件
16
- function GithubSidebarIcon() {
17
- return React.createElement('div', { className: 'dsh-github-icon', title: 'GitHub 工作台' }, React.createElement('svg', { viewBox: '0 0 16 16', width: 18, height: 18, fill: 'currentColor' }, React.createElement('path', {
18
- d: 'M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z',
19
- })));
20
- }
21
- // 3. 注册到 sidebar.panellist(排在 schedules 自动任务后,workspaces 工作区前)
22
- if (ctx.slots && typeof ctx.slots.register === 'function') {
23
- ctx.slots.register({
24
- slot: 'sidebar.panellist',
25
- id: 'github',
26
- order: 20,
27
- label: 'GitHub',
28
- }, GithubSidebarIcon);
29
- // 4. 注册中央主视图(main slot, key: 'github')
30
- const GithubWorkspacePanel = createGithubPanelComponent();
31
- ctx.slots.register({
32
- slot: 'main',
33
- key: 'github',
34
- }, GithubWorkspacePanel);
35
- }
36
- }
@@ -1 +0,0 @@
1
- export declare function createGithubPanelComponent(): () => any;