dsh-github-flow 0.1.7 → 0.1.8
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 +200 -67
- package/README.md +199 -65
- package/docs/architecture.md +55 -13
- package/docs/images/1.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/lib/api/routes.d.ts +3 -1
- package/lib/api/routes.js +95 -4
- package/lib/client.js +851 -40
- package/lib/commands/gh.d.ts +2 -1
- package/lib/commands/gh.js +40 -3
- package/lib/executor.js +12 -2
- package/lib/main.d.ts +11 -0
- package/lib/main.js +41 -3
- package/lib/types.d.ts +45 -0
- package/lib/updater.d.ts +41 -0
- package/lib/updater.js +160 -0
- package/lib/upgrade-manager.d.ts +54 -0
- package/lib/upgrade-manager.js +299 -0
- package/lib/version.d.ts +13 -0
- package/lib/version.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { PLUGIN_PACKAGE_NAME } from './version.js';
|
|
7
|
+
/**
|
|
8
|
+
* 构建跨平台环境自愈增强的 ProcessEnv (重点解决 macOS GUI 应用丢失 PATH 的痛点)
|
|
9
|
+
*
|
|
10
|
+
* 为什么必须这样做:
|
|
11
|
+
* 1. 在 macOS 系统上,若 DSH 桌面客户端由 Finder / Dock / Spotlight (GUI) 启动,进程默认只继承
|
|
12
|
+
* 极简系统 PATH (/usr/bin:/bin:/usr/sbin:/sbin),不会自动读取用户 shell 配置文件 (~/.zshrc, ~/.bash_profile);
|
|
13
|
+
* 2. 用户通过 Homebrew (/opt/homebrew/bin 或 /usr/local/bin) 或 nvm 安装的 node、npm、pnpm 默认在 GUI 下不可见;
|
|
14
|
+
* 3. 动态将当前 node 运行时所在目录 (path.dirname(process.execPath)) 及 Mac 常用包管理器路径注入 PATH 首端,
|
|
15
|
+
* 彻底实现 macOS 与 Linux 环境下子进程对 npm / pnpm 命令的 100% 自愈发现。
|
|
16
|
+
*/
|
|
17
|
+
export function buildCrossPlatformEnv() {
|
|
18
|
+
const currentPath = process.env.PATH || '';
|
|
19
|
+
const extraPaths = [];
|
|
20
|
+
const isMac = process.platform === 'darwin';
|
|
21
|
+
const isWin = process.platform === 'win32';
|
|
22
|
+
// 1. 当前正在运行的 node 自身二进制文件所在目录(最优先,必然有配套 npm)
|
|
23
|
+
if (process.execPath) {
|
|
24
|
+
try {
|
|
25
|
+
extraPaths.push(path.dirname(process.execPath));
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
// 2. macOS 专有软件与包管理器安装路径
|
|
30
|
+
if (isMac) {
|
|
31
|
+
extraPaths.push('/opt/homebrew/bin'); // Apple Silicon Homebrew
|
|
32
|
+
extraPaths.push('/opt/homebrew/sbin');
|
|
33
|
+
extraPaths.push('/usr/local/bin'); // Intel Mac Homebrew
|
|
34
|
+
extraPaths.push('/usr/local/sbin');
|
|
35
|
+
extraPaths.push(path.join(os.homedir(), '.local/bin'));
|
|
36
|
+
extraPaths.push(path.join(os.homedir(), 'Library/pnpm'));
|
|
37
|
+
extraPaths.push(path.join(os.homedir(), '.nvm/current/bin'));
|
|
38
|
+
}
|
|
39
|
+
// 3. Linux 专有常见路径
|
|
40
|
+
if (process.platform === 'linux') {
|
|
41
|
+
extraPaths.push('/usr/local/bin');
|
|
42
|
+
extraPaths.push('/home/linuxbrew/.linuxbrew/bin');
|
|
43
|
+
extraPaths.push(path.join(os.homedir(), '.local/bin'));
|
|
44
|
+
}
|
|
45
|
+
const separator = isWin ? ';' : ':';
|
|
46
|
+
const uniquePaths = Array.from(new Set([...extraPaths, ...currentPath.split(separator)])).filter(Boolean);
|
|
47
|
+
return {
|
|
48
|
+
...process.env,
|
|
49
|
+
PATH: uniquePaths.join(separator),
|
|
50
|
+
NPM_CONFIG_REGISTRY: 'https://registry.npmmirror.com', // 优先国内源加速
|
|
51
|
+
LANG: 'zh_CN.UTF-8',
|
|
52
|
+
LC_ALL: 'zh_CN.UTF-8',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 自动升级执行引擎
|
|
57
|
+
*
|
|
58
|
+
* 核心设计考量与约束条件:
|
|
59
|
+
* 1. 互斥保护 (Mutex):同一时刻仅允许一个升级任务执行,防止用户连击或并发请求击垮包管理器 lockfile;
|
|
60
|
+
* 2. 环境自省 (Dev-Link Safe):检测是否在本地源码工作区调试,若是则阻断自动覆盖,保护源码不被破坏;
|
|
61
|
+
* 3. 跨平台兼容 (macOS / Windows / Linux):自动适配 cmd.exe 与 sh/zsh,自愈补全 macOS PATH 变量;
|
|
62
|
+
* 4. 稳态超时:设置 60s 进程上限,杜绝子进程因网络悬挂死锁宿主 WebServer;
|
|
63
|
+
* 5. 重启指引:Node.js 运行时内存不可变,升级成功后明确要求重启 DSH 彻底加载最新后端能力。
|
|
64
|
+
*/
|
|
65
|
+
export class UpgradeManager {
|
|
66
|
+
isUpgrading = false;
|
|
67
|
+
lastResult = {
|
|
68
|
+
state: 'idle',
|
|
69
|
+
};
|
|
70
|
+
logBuffer = [];
|
|
71
|
+
/**
|
|
72
|
+
* 探测当前插件是否是以本地 Git 源码软链接方式运行
|
|
73
|
+
*/
|
|
74
|
+
isDevLinked() {
|
|
75
|
+
try {
|
|
76
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
77
|
+
const isInsideNodeModules = currentDir.includes('node_modules');
|
|
78
|
+
// 检查上级或同级是否存在 .git 目录
|
|
79
|
+
const gitDirParent = path.resolve(currentDir, '../.git');
|
|
80
|
+
const gitDirRoot = path.resolve(currentDir, '../../.git');
|
|
81
|
+
const hasGit = fs.existsSync(gitDirParent) || fs.existsSync(gitDirRoot);
|
|
82
|
+
return !isInsideNodeModules && hasGit;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 智能识别当前 DSH 运行环境 (桌面版 vs Web/CLI 版)
|
|
90
|
+
*
|
|
91
|
+
* 依据事实源:
|
|
92
|
+
* 1. 桌面版启动时会固定注入 DSH_PROFILE=desktop 与 DSH_PROFILE_DIR (~/.dsh/profiles/desktop);
|
|
93
|
+
* 2. Web版/CLI版通常 DSH_PROFILE 为 default 或自定义工作区 profile;
|
|
94
|
+
* 3. 跨平台支持:通过 os.homedir() 安全解析 ~/.dsh 目录。
|
|
95
|
+
*/
|
|
96
|
+
detectEnvironment() {
|
|
97
|
+
const profile = (process.env.DSH_PROFILE || '').trim();
|
|
98
|
+
const profileDir = (process.env.DSH_PROFILE_DIR || '').trim();
|
|
99
|
+
const isDesktop = profile === 'desktop' || profileDir.toLowerCase().includes('desktop');
|
|
100
|
+
const isDevLinked = this.isDevLinked();
|
|
101
|
+
const platform = process.platform;
|
|
102
|
+
return {
|
|
103
|
+
isDesktop,
|
|
104
|
+
profile: profile || 'default',
|
|
105
|
+
profileDir: profileDir || undefined,
|
|
106
|
+
isDevLinked,
|
|
107
|
+
platform,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 获取当前升级状态
|
|
112
|
+
*/
|
|
113
|
+
getStatus() {
|
|
114
|
+
const env = this.detectEnvironment();
|
|
115
|
+
return {
|
|
116
|
+
...this.lastResult,
|
|
117
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
118
|
+
log: this.logBuffer.join('\n'),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 执行一键升级核心流程
|
|
123
|
+
*/
|
|
124
|
+
async executeUpgrade(targetVersion) {
|
|
125
|
+
const env = this.detectEnvironment();
|
|
126
|
+
if (this.isUpgrading) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
message: '升级任务已在进行中,请勿重复点击。',
|
|
130
|
+
needRestart: false,
|
|
131
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
132
|
+
log: this.logBuffer.join('\n'),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (env.isDevLinked) {
|
|
136
|
+
const devMsg = '检测到当前插件正在以本地 Git 源码软链接模式 (link:) 运行,请在源码目录执行 git pull 更新,已自动阻止 npm 覆盖以保护工作区代码。';
|
|
137
|
+
this.lastResult = {
|
|
138
|
+
state: 'failed',
|
|
139
|
+
targetVersion,
|
|
140
|
+
message: devMsg,
|
|
141
|
+
completedAt: Date.now(),
|
|
142
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
message: devMsg,
|
|
147
|
+
needRestart: false,
|
|
148
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
149
|
+
log: 'Operation blocked by dev-linked protection.',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
this.isUpgrading = true;
|
|
153
|
+
this.logBuffer = [];
|
|
154
|
+
const startedAt = Date.now();
|
|
155
|
+
const pkgWithVer = targetVersion ? `${PLUGIN_PACKAGE_NAME}@${targetVersion}` : `${PLUGIN_PACKAGE_NAME}@latest`;
|
|
156
|
+
this.lastResult = {
|
|
157
|
+
state: 'upgrading',
|
|
158
|
+
targetVersion,
|
|
159
|
+
message: `正在执行命令拉取 ${pkgWithVer}...`,
|
|
160
|
+
startedAt,
|
|
161
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
162
|
+
};
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const isWin = process.platform === 'win32';
|
|
165
|
+
let command = '';
|
|
166
|
+
let executionCwd = process.cwd();
|
|
167
|
+
// 根据环境类型分流升级指令(跨平台安全路径):
|
|
168
|
+
if (env.isDesktop) {
|
|
169
|
+
// 场景 A:DSH 桌面版环境(macOS / Windows / Linux 桌面端通用)
|
|
170
|
+
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
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// 场景 B:Web 版 / 开发者终端 CLI 版环境
|
|
182
|
+
const profile = env.profile || 'default';
|
|
183
|
+
command = `pnpm dsh plugin add ${pkgWithVer} || dsh plugin --profile ${profile} add ${pkgWithVer} || npm install -g ${pkgWithVer}`;
|
|
184
|
+
}
|
|
185
|
+
// 跨平台 Shell 选型与参数调度:
|
|
186
|
+
// - Windows: 采用 cmd.exe (/d /s /c),完美原生支持 '||' 且冷启动仅 ~10ms,规避 powershell 5.1 语法报错;
|
|
187
|
+
// - macOS / Linux: 优先采用用户的 SHELL (/bin/zsh 或 /bin/bash),兜底 /bin/sh,参数为 -c。
|
|
188
|
+
const shell = isWin
|
|
189
|
+
? (process.env.ComSpec || 'cmd.exe')
|
|
190
|
+
: (process.env.SHELL || '/bin/sh');
|
|
191
|
+
const args = isWin
|
|
192
|
+
? ['/d', '/s', '/c', command]
|
|
193
|
+
: ['-c', command];
|
|
194
|
+
let settled = false;
|
|
195
|
+
const safeSettle = (result) => {
|
|
196
|
+
if (settled)
|
|
197
|
+
return;
|
|
198
|
+
settled = true;
|
|
199
|
+
this.isUpgrading = false;
|
|
200
|
+
resolve(result);
|
|
201
|
+
};
|
|
202
|
+
// 注入跨平台增强环境(尤其治愈 macOS GUI 启动丢失 PATH 的痛点)
|
|
203
|
+
const childEnv = buildCrossPlatformEnv();
|
|
204
|
+
const child = spawn(shell, args, {
|
|
205
|
+
cwd: executionCwd,
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
env: childEnv,
|
|
208
|
+
});
|
|
209
|
+
const timeoutTimer = setTimeout(() => {
|
|
210
|
+
child.kill();
|
|
211
|
+
const timeoutMsg = '升级命令执行超时(60 秒),可能是网络阻塞或包管理器锁占用,建议在终端手动执行升级。';
|
|
212
|
+
this.lastResult = {
|
|
213
|
+
state: 'failed',
|
|
214
|
+
targetVersion,
|
|
215
|
+
message: timeoutMsg,
|
|
216
|
+
completedAt: Date.now(),
|
|
217
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
218
|
+
};
|
|
219
|
+
safeSettle({
|
|
220
|
+
ok: false,
|
|
221
|
+
message: timeoutMsg,
|
|
222
|
+
needRestart: false,
|
|
223
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
224
|
+
log: this.logBuffer.join('\n'),
|
|
225
|
+
});
|
|
226
|
+
}, 60_000);
|
|
227
|
+
child.stdout?.on('data', (chunk) => {
|
|
228
|
+
const text = chunk.toString();
|
|
229
|
+
this.logBuffer.push(text);
|
|
230
|
+
if (this.logBuffer.length > 200)
|
|
231
|
+
this.logBuffer.shift();
|
|
232
|
+
});
|
|
233
|
+
child.stderr?.on('data', (chunk) => {
|
|
234
|
+
const text = chunk.toString();
|
|
235
|
+
this.logBuffer.push(text);
|
|
236
|
+
if (this.logBuffer.length > 200)
|
|
237
|
+
this.logBuffer.shift();
|
|
238
|
+
});
|
|
239
|
+
child.on('close', (code) => {
|
|
240
|
+
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({
|
|
254
|
+
ok: true,
|
|
255
|
+
message: successMsg,
|
|
256
|
+
needRestart: true,
|
|
257
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
258
|
+
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({
|
|
271
|
+
ok: false,
|
|
272
|
+
message: failedMsg,
|
|
273
|
+
needRestart: false,
|
|
274
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
275
|
+
log: this.logBuffer.join('\n'),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
child.on('error', (err) => {
|
|
280
|
+
clearTimeout(timeoutTimer);
|
|
281
|
+
const errMsg = `无法启动安装子进程: ${err.message}`;
|
|
282
|
+
this.lastResult = {
|
|
283
|
+
state: 'failed',
|
|
284
|
+
targetVersion,
|
|
285
|
+
message: errMsg,
|
|
286
|
+
completedAt: Date.now(),
|
|
287
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
288
|
+
};
|
|
289
|
+
safeSettle({
|
|
290
|
+
ok: false,
|
|
291
|
+
message: errMsg,
|
|
292
|
+
needRestart: false,
|
|
293
|
+
environment: env.isDesktop ? 'desktop' : 'web',
|
|
294
|
+
log: this.logBuffer.join('\n'),
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
package/lib/version.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件版本与元数据模块
|
|
3
|
+
*
|
|
4
|
+
* 为什么独立为此文件:
|
|
5
|
+
* 1. 运行时需要精确获知自身当前安装版本与包标识,避免硬编码分散在各业务模块;
|
|
6
|
+
* 2. 避免在 ESM 运行时动态读取 package.json 引入 fs 权限和路径解析边界问题;
|
|
7
|
+
* 3. 方便在 CI 发布流水线或 build 脚本中自动化校准。
|
|
8
|
+
*/
|
|
9
|
+
export declare const CURRENT_PLUGIN_VERSION = "0.1.8";
|
|
10
|
+
export declare const PLUGIN_PACKAGE_NAME = "dsh-github-flow";
|
|
11
|
+
export declare const NPM_PACKAGE_URL = "https://www.npmjs.com/package/dsh-github-flow";
|
|
12
|
+
export declare const NPM_REGISTRY_MIRROR = "https://registry.npmmirror.com";
|
|
13
|
+
export declare const NPM_REGISTRY_OFFICIAL = "https://registry.npmjs.org";
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件版本与元数据模块
|
|
3
|
+
*
|
|
4
|
+
* 为什么独立为此文件:
|
|
5
|
+
* 1. 运行时需要精确获知自身当前安装版本与包标识,避免硬编码分散在各业务模块;
|
|
6
|
+
* 2. 避免在 ESM 运行时动态读取 package.json 引入 fs 权限和路径解析边界问题;
|
|
7
|
+
* 3. 方便在 CI 发布流水线或 build 脚本中自动化校准。
|
|
8
|
+
*/
|
|
9
|
+
export const CURRENT_PLUGIN_VERSION = '0.1.8';
|
|
10
|
+
export const PLUGIN_PACKAGE_NAME = 'dsh-github-flow';
|
|
11
|
+
export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-github-flow';
|
|
12
|
+
export const NPM_REGISTRY_MIRROR = 'https://registry.npmmirror.com';
|
|
13
|
+
export const NPM_REGISTRY_OFFICIAL = 'https://registry.npmjs.org';
|
package/package.json
CHANGED