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
|
@@ -1,4 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动作类路由:缓存强制刷新 + 右侧栏快捷操作(create_pr/create_issue/open_browser/open_terminal)。
|
|
3
|
+
* 共同特征:POST 触发、读少写多、无缓存语义,与概览类路由的演进方向不同。
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
1
6
|
import { isSafeExternalUrl, sendJson } from './route-helpers.js';
|
|
7
|
+
/**
|
|
8
|
+
* 构造「在新终端窗口运行命令」的平台命令(execFile 数组形态,不经 shell)。
|
|
9
|
+
*
|
|
10
|
+
* Issue #2 修复要点:
|
|
11
|
+
* - Windows:cmd.exe(显式 .exe,规避 GUI 父进程 PATH 解析差异)+ start 的窗口标题
|
|
12
|
+
* 占位 "DSH GitHub"——start 的首个带引号参数会被解析为标题而非程序,占位后
|
|
13
|
+
* powershell.exe 才能被稳定识别为目标程序;
|
|
14
|
+
* - macOS:osascript 驱动 Terminal.app,命令经引号转义拼入 AppleScript;
|
|
15
|
+
* - Linux:发行版终端二进制与参数形态各异(gnome-terminal 用 `--`、konsole 用
|
|
16
|
+
* `-e`、xfce4-terminal 用 `-x`),单一硬编码在主流桌面大概率不存在——由
|
|
17
|
+
* detectLinuxTerminal 按探测结果返回对应形态。
|
|
18
|
+
*/
|
|
19
|
+
export function buildTerminalCommand(command) {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
return {
|
|
22
|
+
bin: 'cmd.exe',
|
|
23
|
+
// -NoProfile 是 Windows 兼容基线:powershell.exe 是全量自带的 5.1,其默认执行
|
|
24
|
+
// 策略常为 Restricted——加载用户 profile(如 conda init 写入的)会就地报
|
|
25
|
+
// UnauthorizedAccess 红错(实测:winget 安装仍正常,但启动噪音吓人)。
|
|
26
|
+
// 白名单命令(gh/winget/brew/apt)均不依赖用户 profile 环境;pwsh 7 是个人
|
|
27
|
+
// 安装不作依赖,-NoProfile 在 5.1/7 语义一致,一并消除该类报错并加速启动。
|
|
28
|
+
args: ['/c', 'start', 'DSH GitHub', 'powershell.exe', '-NoExit', '-NoProfile', '-Command', command],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (process.platform === 'darwin') {
|
|
32
|
+
// AppleScript 字符串内的双引号以 \" 转义;白名单命令虽无引号,仍统一转义防未来扩展踩坑
|
|
33
|
+
const escaped = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
34
|
+
return {
|
|
35
|
+
bin: 'osascript',
|
|
36
|
+
args: [
|
|
37
|
+
'-e', `tell application "Terminal" to do script "${escaped}"`,
|
|
38
|
+
'-e', 'tell application "Terminal" to activate',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (process.platform === 'linux') {
|
|
43
|
+
const terminal = detectLinuxTerminal();
|
|
44
|
+
if (!terminal)
|
|
45
|
+
return null;
|
|
46
|
+
// 各终端「执行命令并保持窗口」的参数形态不同:
|
|
47
|
+
// - gnome-terminal -- <sh> -c:新版 -e 已弃用,-- 后直接跟命令行
|
|
48
|
+
// - konsole/xfce4-terminal/xterm:-e/-x 执行,sh -c + exec 保交互语义
|
|
49
|
+
// (命令结束后 exec sh 落回交互 shell,窗口不闪退)
|
|
50
|
+
const shArgs = ['sh', '-c', `${command}; exec sh`];
|
|
51
|
+
switch (terminal) {
|
|
52
|
+
case 'gnome-terminal': return { bin: 'gnome-terminal', args: ['--', ...shArgs] };
|
|
53
|
+
case 'konsole': return { bin: 'konsole', args: ['-e', ...shArgs] };
|
|
54
|
+
case 'xfce4-terminal': return { bin: 'xfce4-terminal', args: ['-x', ...shArgs] };
|
|
55
|
+
case 'x-terminal-emulator': return { bin: 'x-terminal-emulator', args: ['-e', ...shArgs] };
|
|
56
|
+
case 'xterm': return { bin: 'xterm', args: ['-e', ...shArgs] };
|
|
57
|
+
default: return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** Linux 终端探测结果缓存(进程级):which 探测有子进程成本,会话内只做一次 */
|
|
63
|
+
let cachedLinuxTerminal;
|
|
64
|
+
/**
|
|
65
|
+
* 探测当前 Linux 桌面可用的终端模拟器(按桌面占有率排序)。
|
|
66
|
+
* 返回 null 表示全部不可用——调用方据此走失败分支而非硬编码赌一个。
|
|
67
|
+
*/
|
|
68
|
+
function detectLinuxTerminal() {
|
|
69
|
+
if (cachedLinuxTerminal !== undefined)
|
|
70
|
+
return cachedLinuxTerminal;
|
|
71
|
+
const candidates = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'x-terminal-emulator', 'xterm'];
|
|
72
|
+
try {
|
|
73
|
+
const { execFileSync } = require('node:child_process');
|
|
74
|
+
for (const name of candidates) {
|
|
75
|
+
try {
|
|
76
|
+
execFileSync('which', [name], { stdio: 'ignore', timeout: 2_000 });
|
|
77
|
+
cachedLinuxTerminal = name;
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
catch { /* 该候选不存在,试下一个 */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* which 本身不可用(极端环境),视为无可用终端 */ }
|
|
84
|
+
cachedLinuxTerminal = null;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** 测试辅助:重置 Linux 终端探测缓存(便于单测注入不同探测环境) */
|
|
88
|
+
export function resetLinuxTerminalCache() {
|
|
89
|
+
cachedLinuxTerminal = undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 执行终端唤起并等待真实结果(Issue #2 核心修复:不再先斩后奏报假成功)。
|
|
93
|
+
* start/osascript 场景下命令随宿主立即退出,close 即代表唤起动作用的成败;
|
|
94
|
+
* err 与 stderr 原样上抛给调用方决定 HTTP 语义。
|
|
95
|
+
*/
|
|
96
|
+
function launchTerminalAndWait(command) {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const built = buildTerminalCommand(command);
|
|
99
|
+
if (!built) {
|
|
100
|
+
resolve({ ok: false, error: '未找到可用的系统终端(Linux 下请先安装 gnome-terminal / konsole 等任一终端模拟器)' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// stdio 必须忽略:start 唤起的交互式窗口会继承 execFile 的 stdout/stderr 管道句柄,
|
|
104
|
+
// -NoExit 窗口存活期间管道永不关闭——回调要等满 10s 超时甚至不触发(实测复现:
|
|
105
|
+
// 唤起成功的窗口让 HTTP 响应挂起 10s+)。交互终端本就不该捕获输出;stdio ignore
|
|
106
|
+
// 后等待语义回归 cmd.exe 自身退出(start 创建窗口后立即返回,亚秒级)。
|
|
107
|
+
// 改用原生 spawn:execFile 的类型与实现都强制 stdout 管道捕获,而交互终端唤起
|
|
108
|
+
// 场景需要的恰恰是「不捕获」——spawn 的 stdio:'ignore' 是官方支持的干净形态。
|
|
109
|
+
const child = spawn(built.bin, built.args, {
|
|
110
|
+
stdio: 'ignore',
|
|
111
|
+
detached: false,
|
|
112
|
+
});
|
|
113
|
+
// 10s 兜底超时:正常路径 cmd.exe start 毫秒级返回;超时视为唤起故障并如实回传
|
|
114
|
+
let timedOut = false;
|
|
115
|
+
const killTimer = setTimeout(() => { timedOut = true; child.kill(); }, 10_000);
|
|
116
|
+
child.on('error', (err) => {
|
|
117
|
+
clearTimeout(killTimer);
|
|
118
|
+
resolve({ ok: false, error: err.message });
|
|
119
|
+
});
|
|
120
|
+
child.on('close', (code) => {
|
|
121
|
+
clearTimeout(killTimer);
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
resolve({ ok: true });
|
|
124
|
+
}
|
|
125
|
+
else if (timedOut) {
|
|
126
|
+
// kill 触发的 close 是 code=null——如实归因为超时而非"退出码 null"
|
|
127
|
+
resolve({ ok: false, error: '终端启动命令 10 秒未返回(可能被安全软件拦截)' });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
resolve({ ok: false, error: `终端启动命令退出码 ${code}` });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
2
135
|
export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
|
|
3
136
|
const { bindRoute } = deps;
|
|
4
137
|
// 1. 强制刷新缓存
|
|
@@ -80,26 +213,17 @@ export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
|
|
|
80
213
|
if (!allowedCommands.includes(command)) {
|
|
81
214
|
throw new Error(`不支持唤起该指令: ${command}`);
|
|
82
215
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
args = ['-e', `tell application "Terminal" to do script "${command}"`, '-e', 'tell application "Terminal" to activate'];
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
bin = 'x-terminal-emulator';
|
|
96
|
-
args = ['-e', command];
|
|
216
|
+
// Issue #2 修复:等待真实唤起结果再回传——旧实现 execFile 异步回调吞错、
|
|
217
|
+
// 路由先行返回 ok:true,导致终端弹不弹都提示成功。
|
|
218
|
+
const launch = await launchTerminalAndWait(command);
|
|
219
|
+
if (!launch.ok) {
|
|
220
|
+
sendJson(res, 500, {
|
|
221
|
+
ok: false,
|
|
222
|
+
message: `唤起终端失败:${launch.error}。请手动打开系统终端执行:${command}`,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
97
225
|
}
|
|
98
|
-
|
|
99
|
-
if (err)
|
|
100
|
-
console.warn('[dsh-github-flow] 唤起终端失败:', err?.message || err);
|
|
101
|
-
});
|
|
102
|
-
sendJson(res, 200, { ok: true, message: '已请求唤起系统终端' });
|
|
226
|
+
sendJson(res, 200, { ok: true, message: '已唤起系统终端' });
|
|
103
227
|
break;
|
|
104
228
|
}
|
|
105
229
|
default:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadUpdateSettings, saveUpdateSettings, isValidUpdateMode, describeUpdateMode } from '../update-settings.js';
|
|
2
|
+
import { sendJson, readJsonBody } from './route-helpers.js';
|
|
2
3
|
export function registerLifecycleRoutes(deps, executor, checker, upgrader) {
|
|
3
4
|
const { bindRoute } = deps;
|
|
4
5
|
// 1. 闲时静默检查更新端点(前端在 gh 业务数据完全拉取就绪后,由 idle 延时静默触发)
|
|
@@ -31,8 +32,12 @@ export function registerLifecycleRoutes(deps, executor, checker, upgrader) {
|
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
try {
|
|
35
|
+
// 一键升级是用户显式动作,不受 checkUpdate 配置熔断:check() 在禁用时
|
|
36
|
+
// 会返回 latestVersion 缺失的兜底态,此时回退 'latest' 让 pnpm 自行解析
|
|
37
|
+
// 最新版(与 UpgradeManager 自身的 targetVersion 可选语义一致)。
|
|
34
38
|
const info = await checker.check(false);
|
|
35
|
-
const
|
|
39
|
+
const target = info.latestVersion || 'latest';
|
|
40
|
+
const result = await upgrader.executeUpgrade(target);
|
|
36
41
|
sendJson(res, result.ok ? 200 : 500, result);
|
|
37
42
|
}
|
|
38
43
|
catch (err) {
|
|
@@ -52,6 +57,52 @@ export function registerLifecycleRoutes(deps, executor, checker, upgrader) {
|
|
|
52
57
|
sendJson(res, 200, { ok: true, data: upgrader.getStatus() });
|
|
53
58
|
},
|
|
54
59
|
});
|
|
60
|
+
// 5. 更新源设置:GET 读取(store 权威,缺省 official)/ POST 保存并立即用新源验证
|
|
61
|
+
bindRoute({
|
|
62
|
+
kind: 'exact',
|
|
63
|
+
path: '/api/github/update-settings',
|
|
64
|
+
handler: async (req, res) => {
|
|
65
|
+
if (req.method === 'GET') {
|
|
66
|
+
const mode = loadUpdateSettings();
|
|
67
|
+
sendJson(res, 200, { ok: true, data: { mode, description: describeUpdateMode(mode) } });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (req.method !== 'POST') {
|
|
71
|
+
sendJson(res, 405, { ok: false, message: '仅支持 GET / POST 请求' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const body = await readJsonBody(req);
|
|
76
|
+
const mode = body?.mode;
|
|
77
|
+
if (!isValidUpdateMode(mode)) {
|
|
78
|
+
sendJson(res, 400, { ok: false, message: `非法更新源模式: ${String(mode)}(允许 official / mirror / off)` });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 写盘失败不阻断内存态生效(save 内部已 warn),返回携带 error 提示重启后需重设
|
|
82
|
+
const saved = saveUpdateSettings(mode);
|
|
83
|
+
checker.applySettings(mode);
|
|
84
|
+
// 非 off 模式:立即用新源真探一次,把结果带回前端展示(缓存已被 applySettings 失效)
|
|
85
|
+
let probe = null;
|
|
86
|
+
if (mode !== 'off') {
|
|
87
|
+
const info = await checker.check(true);
|
|
88
|
+
probe = { latestVersion: info.latestVersion, hasUpdate: info.hasUpdate, checkedAt: info.checkedAt };
|
|
89
|
+
}
|
|
90
|
+
sendJson(res, 200, {
|
|
91
|
+
ok: true,
|
|
92
|
+
data: {
|
|
93
|
+
mode,
|
|
94
|
+
description: describeUpdateMode(mode),
|
|
95
|
+
persisted: saved.ok,
|
|
96
|
+
persistError: saved.error,
|
|
97
|
+
probe,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
sendJson(res, 500, { ok: false, message: err.message || '保存更新源设置失败' });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
55
106
|
// 4. 快速认证状态轻量探针 (耗时 < 500ms,专供登录自愈检测与快速恢复)
|
|
56
107
|
bindRoute({
|
|
57
108
|
kind: 'exact',
|