dsh-github-flow 0.2.0 → 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.
- package/README.en.md +31 -12
- package/README.md +33 -14
- package/docs/UPGRADE-NOTES.md +93 -0
- package/docs/architecture.md +33 -18
- package/docs/images/1.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/npm-publish-guide.md +15 -4
- package/lib/api/routes-actions.d.ts +18 -4
- package/lib/api/routes-actions.js +143 -19
- package/lib/api/routes-lifecycle.d.ts +1 -1
- package/lib/api/routes-lifecycle.js +53 -2
- package/lib/client.js +411 -44
- package/lib/commands/gh.js +11 -2
- package/lib/executor.js +122 -12
- package/lib/main.js +9 -0
- package/lib/renderers/markdown.d.ts +6 -1
- package/lib/renderers/markdown.js +41 -14
- package/lib/tools/help.js +12 -0
- package/lib/tools/image-utils.d.ts +79 -0
- package/lib/tools/image-utils.js +247 -0
- package/lib/tools/issue.d.ts +9 -4
- package/lib/tools/issue.js +75 -4
- package/lib/tools/pr.d.ts +8 -4
- package/lib/tools/pr.js +80 -6
- package/lib/tools/repo.d.ts +12 -4
- package/lib/tools/repo.js +71 -3
- package/lib/types.d.ts +12 -1
- package/lib/update-settings.d.ts +18 -0
- package/lib/update-settings.js +60 -0
- package/lib/updater.d.ts +32 -1
- package/lib/updater.js +117 -9
- package/lib/upgrade-manager.d.ts +70 -1
- package/lib/upgrade-manager.js +346 -46
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/upgrade-manager.js
CHANGED
|
@@ -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.
|
|
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
|
*
|
|
@@ -70,9 +221,27 @@ export class UpgradeManager {
|
|
|
70
221
|
logBuffer = [];
|
|
71
222
|
/** 代理管理器(可选):注入后一键升级的 npm/pnpm 子进程走用户代理 */
|
|
72
223
|
proxyManager;
|
|
224
|
+
/** 更新源模式查询(可选):一键升级的 NPM_CONFIG_REGISTRY 跟随用户在设置页选择的源 */
|
|
225
|
+
getUpdateMode = null;
|
|
73
226
|
attachProxyManager(manager) {
|
|
74
227
|
this.proxyManager = manager;
|
|
75
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
|
+
}
|
|
76
245
|
/**
|
|
77
246
|
* 探测当前插件是否是以本地 Git 源码软链接方式运行
|
|
78
247
|
*/
|
|
@@ -123,6 +292,94 @@ export class UpgradeManager {
|
|
|
123
292
|
log: this.logBuffer.join('\n'),
|
|
124
293
|
};
|
|
125
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
|
+
}
|
|
126
383
|
/**
|
|
127
384
|
* 执行一键升级核心流程
|
|
128
385
|
*/
|
|
@@ -165,27 +422,88 @@ export class UpgradeManager {
|
|
|
165
422
|
startedAt,
|
|
166
423
|
environment: env.isDesktop ? 'desktop' : 'web',
|
|
167
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
|
+
}
|
|
168
471
|
return new Promise((resolve) => {
|
|
169
472
|
const isWin = process.platform === 'win32';
|
|
170
473
|
let command = '';
|
|
171
474
|
let executionCwd = process.cwd();
|
|
172
|
-
|
|
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 依赖毫无作用,只制造假成功。
|
|
173
498
|
if (env.isDesktop) {
|
|
174
|
-
|
|
499
|
+
command = `dsh plugin --profile desktop add ${pkgWithVer}`;
|
|
175
500
|
const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
176
|
-
|
|
177
|
-
if (fs.existsSync(fallbackDesktopCwd)) {
|
|
178
|
-
executionCwd = fallbackDesktopCwd;
|
|
179
|
-
command = `dsh plugin --profile desktop add ${pkgWithVer} || npm install ${pkgWithVer} --save`;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
command = `dsh plugin --profile desktop add ${pkgWithVer} || npm install -g ${pkgWithVer}`;
|
|
183
|
-
}
|
|
501
|
+
executionCwd = env.profileDir || path.join(dshHome, 'profiles', 'desktop');
|
|
184
502
|
}
|
|
185
503
|
else {
|
|
186
504
|
// 场景 B:Web 版 / 开发者终端 CLI 版环境
|
|
187
505
|
const profile = env.profile || 'default';
|
|
188
|
-
command = `
|
|
506
|
+
command = `dsh plugin --profile ${profile} add ${pkgWithVer}`;
|
|
189
507
|
}
|
|
190
508
|
// 跨平台 Shell 选型与参数调度:
|
|
191
509
|
// - Windows: 采用 cmd.exe (/d /s /c),完美原生支持 '||' 且冷启动仅 ~10ms,规避 powershell 5.1 语法报错;
|
|
@@ -208,7 +526,7 @@ export class UpgradeManager {
|
|
|
208
526
|
// 代理环境变量按 applyToPackageManager 开关合并:npm/pnpm 遵循 HTTPS_PROXY,
|
|
209
527
|
// 与 gh 链路同一管理器,保证「设置代理后升级也走代理」的体验一致性。
|
|
210
528
|
const childEnv = {
|
|
211
|
-
...buildCrossPlatformEnv(),
|
|
529
|
+
...buildCrossPlatformEnv(this.resolveRegistry()),
|
|
212
530
|
...(this.proxyManager?.getEnv('package-manager') || {}),
|
|
213
531
|
};
|
|
214
532
|
const child = spawn(shell, args, {
|
|
@@ -218,7 +536,7 @@ export class UpgradeManager {
|
|
|
218
536
|
});
|
|
219
537
|
const timeoutTimer = setTimeout(() => {
|
|
220
538
|
child.kill();
|
|
221
|
-
const timeoutMsg = '升级命令执行超时(60
|
|
539
|
+
const timeoutMsg = '升级命令执行超时(60 秒),可能是网络阻塞或包管理器锁占用。可稍后重试一键升级,或点击「📋 复制命令」手动执行。';
|
|
222
540
|
this.lastResult = {
|
|
223
541
|
state: 'failed',
|
|
224
542
|
targetVersion,
|
|
@@ -248,47 +566,29 @@ export class UpgradeManager {
|
|
|
248
566
|
});
|
|
249
567
|
child.on('close', (code) => {
|
|
250
568
|
clearTimeout(timeoutTimer);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.lastResult = {
|
|
257
|
-
state: 'success',
|
|
258
|
-
targetVersion,
|
|
259
|
-
message: successMsg,
|
|
260
|
-
completedAt,
|
|
261
|
-
environment: env.isDesktop ? 'desktop' : 'web',
|
|
262
|
-
};
|
|
263
|
-
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
|
+
? {
|
|
264
574
|
ok: true,
|
|
265
|
-
message:
|
|
575
|
+
message: this.lastResult.message || '升级成功',
|
|
266
576
|
needRestart: true,
|
|
267
577
|
environment: env.isDesktop ? 'desktop' : 'web',
|
|
268
578
|
log: this.logBuffer.join('\n'),
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
else {
|
|
272
|
-
const failedMsg = `升级退出码异常 (${code}),可能需要管理员权限或在宿主环境终端手动执行。`;
|
|
273
|
-
this.lastResult = {
|
|
274
|
-
state: 'failed',
|
|
275
|
-
targetVersion,
|
|
276
|
-
message: failedMsg,
|
|
277
|
-
completedAt,
|
|
278
|
-
environment: env.isDesktop ? 'desktop' : 'web',
|
|
279
|
-
};
|
|
280
|
-
safeSettle({
|
|
579
|
+
}
|
|
580
|
+
: {
|
|
281
581
|
ok: false,
|
|
282
|
-
message:
|
|
582
|
+
message: this.lastResult.message || `升级退出码异常 (${code})`,
|
|
283
583
|
needRestart: false,
|
|
284
584
|
environment: env.isDesktop ? 'desktop' : 'web',
|
|
285
585
|
log: this.logBuffer.join('\n'),
|
|
286
|
-
}
|
|
287
|
-
|
|
586
|
+
};
|
|
587
|
+
safeSettle(finalResult);
|
|
288
588
|
});
|
|
289
589
|
child.on('error', (err) => {
|
|
290
590
|
clearTimeout(timeoutTimer);
|
|
291
|
-
const errMsg =
|
|
591
|
+
const errMsg = `升级失败:无法启动安装子进程(${err.message})。可重试一键升级,或点击「📋 复制命令」手动执行。`;
|
|
292
592
|
this.lastResult = {
|
|
293
593
|
state: 'failed',
|
|
294
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
|
|
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
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-github-flow",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"icon": "icon.svg",
|
|
5
5
|
"description": "GitHub 工作流引擎:为 DeepSeek Harness 提供原生 GitHub CLI (gh) 集成——9 大 Agent 领域工具(PR 审查合并、Issue 任务流转、CI 失败诊断、发版交付、密钥管理、Agent Skills 生态)、/gh 人类快捷指令、Web UI 全景驾驶舱与一键跨平台自动升级;零凭证侵入复用宿主机 gh 登录态,内置 Token 防溢出截断、高危操作二次确认与密文安全输入通道。",
|
|
6
6
|
"type": "module",
|