@noob-stupid/dsh-plugin-console 0.5.22 → 0.5.23
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/lib/server/infra/exec.js +135 -7
- package/package.json +1 -1
package/lib/server/infra/exec.js
CHANGED
|
@@ -13,13 +13,81 @@ function gitBin() {
|
|
|
13
13
|
return process.platform === 'win32' ? 'git.exe' : 'git'
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** PATH 条目是不是桌面端运行时的 `<…>\runtime\pnpm\bin` / `<…>\runtime\bin`(两种分隔符都认)。 */
|
|
17
|
+
const RUNTIME_PNPM_BIN_RE = /[\\/]runtime[\\/]pnpm[\\/]bin[\\/]?$/u
|
|
18
|
+
const RUNTIME_BIN_RE = /[\\/]runtime[\\/]bin[\\/]?$/u
|
|
19
|
+
|
|
20
|
+
/** cmd.exe 的元字符:`^` 是 cmd 自己的转义符(**引号内照样生效** —— 本机实测 `"a^b"` 到程序手里变成 `ab`),
|
|
21
|
+
* `& | < > ( )` 在引号内**仍然是 cmd 的操作符**(实测 `"a&b"` 会在 `&` 处断开,把 `b` 当命令执行)。
|
|
22
|
+
* 所以段内这些字符必须逐个加 `^` 转义。 */
|
|
23
|
+
const CMD_META_RE = /[\^&|<>()]/gu
|
|
24
|
+
|
|
25
|
+
/** 把一串命令段拼成**一条 cmd.exe 命令字符串**(纯函数,导出给单测)。
|
|
26
|
+
*
|
|
27
|
+
* 2026-09-27 改错(桌面端实例"所有 pnpm 操作都跑不了"的两处根因之一):
|
|
28
|
+
* 旧代码是 `['corepack','pnpm',...args].map((a) => JSON.stringify(a)).join(' ')` —— JSON.stringify 加的是
|
|
29
|
+
* **真引号**,而这一整串又是作为一个 argv 元素交给 Node 的 spawn;Windows 上 Node/libuv 按 MSVCRT 规则
|
|
30
|
+
* 把参数里的 `"` 转义成 `\"`,cmd 实际收到 `\"corepack\" \"pnpm\"` → `'\"corepack\"' 不是内部或外部命令`。
|
|
31
|
+
* 本机对着一个只回显参数的 .cmd 实测三种形态:
|
|
32
|
+
* · `"a" "b"` + 默认转义 → 收到 `\"a\" \"b\"` → 失败(旧缺陷)
|
|
33
|
+
* · `"a" "b"` + windowsVerbatimArguments → `/s` 会把首尾两个引号一起剥掉 → 也失败
|
|
34
|
+
* · `""a" "b""` + windowsVerbatimArguments → 正确解析成两个参数 ✓(含空格 / `&` / `|` / `>` 的参数
|
|
35
|
+
* 在段内加 `^` 转义后也原样到达)
|
|
36
|
+
* 所以本函数产出的是"**内层**命令字符串"(形如 `"corepack" "pnpm" "add" "x"`,**永不含 `\"`**),
|
|
37
|
+
* 最外层那对引号由 cmdShellArgv 负责加。
|
|
38
|
+
* 已知边界(如实记,不假装覆盖):`%VAR%` 与开启延迟展开时的 `!VAR!` 是 cmd 自己的展开,
|
|
39
|
+
* **任何引号/`^` 都挡不住**;我们生成的 argv(包名、registry、布尔选项、绝对路径)不含这两个字符,
|
|
40
|
+
* 真出现时该通道会失败并如实报错(不会静默跑成别的命令)。 */
|
|
41
|
+
function cmdCommandLine(parts) {
|
|
42
|
+
return parts
|
|
43
|
+
.map((part) => `"${String(part).replace(CMD_META_RE, (ch) => `^${ch}`)}"`)
|
|
44
|
+
.join(' ')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** corepack 形态的命令字符串(纯函数,导出给单测断言"不含 `\"` 转义")。 */
|
|
48
|
+
function cmdCorepackCommand(args) {
|
|
49
|
+
return cmdCommandLine(['corepack', 'pnpm', ...args])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 交给 cmd.exe 的完整 argv(`/d /s /c` + **整条再包一层引号**的内层命令字符串)。
|
|
53
|
+
* 必须与 `verbatim: true`(windowsVerbatimArguments)配套:Node 不再做任何引号转义,
|
|
54
|
+
* cmd 的 `/s` 剥掉最外层那一对,剩下的正好是我们要的命令行。 */
|
|
55
|
+
function cmdShellArgv(commandLine) {
|
|
56
|
+
return ['/d', '/s', '/c', `"${commandLine}"`]
|
|
57
|
+
}
|
|
58
|
+
|
|
16
59
|
/** pnpm 执行方式定位(跨平台,纯函数便于单测 → 按优先级返回列表,逐个尝试)。
|
|
17
60
|
* 事故(2026-09-20,同一位用户,node v24 + Linux):只按 Windows 布局找 corepack.js
|
|
18
61
|
* (`<node bin>/node_modules/corepack/dist/corepack.js`),而 Linux 的 npm 全局布局在
|
|
19
62
|
* `<prefix>/lib/node_modules/corepack/...` → AI 赋能的 install-npm 生成
|
|
20
63
|
* `node /usr/local/bin/node_modules/corepack/dist/corepack.js pnpm add …` →
|
|
21
|
-
* `Error: Cannot find module …`(MODULE_NOT_FOUND)。这里把三种布局 + PATH 兜底都列出来。
|
|
22
|
-
|
|
64
|
+
* `Error: Cannot find module …`(MODULE_NOT_FOUND)。这里把三种布局 + PATH 兜底都列出来。
|
|
65
|
+
*
|
|
66
|
+
* 事故(2026-09-27,官方桌面端实例:**所有** pnpm 操作都跑不了)—— 本次两处修:
|
|
67
|
+
* ① 形态缺陷(先于本轮存在):cmd-corepack 分支原来把 argv 数组交给 cmd.exe,Node 在 Windows 上
|
|
68
|
+
* 按 MSVCRT 规则把段内引号转义成 `\"` → cmd 收到 `\"corepack\" \"pnpm\"` → 一条都跑不起来。
|
|
69
|
+
* 修法与实测证据见 cmdCommandLine / cmdShellArgv。
|
|
70
|
+
* ② 认不出桌面端自带运行时(加法):桌面端 host 自己就是用
|
|
71
|
+
* `command: process.execPath`(Electron 二进制)+ `args: ['--expose-internals', <resources>\runtime\pnpm\bin\pnpm.mjs]`
|
|
72
|
+
* + `env.ELECTRON_RUN_AS_NODE=1` 跑 pnpm 的(dsh-desktop-host 的 packageManager),
|
|
73
|
+
* 而我们的候选只有"node 旁边的三种 corepack.js 布局 + cmd corepack + PATH 上的 pnpm" ——
|
|
74
|
+
* 桌面端的 host 二进制旁边没有 corepack.js,PATH 里也只有 `runtime\bin`(没有 pnpm.cmd),
|
|
75
|
+
* 于是三条候选全落空。现在把桌面端运行时(resourcesPath ① / PATH+env 线索 ③ / PATH 扫描 ②)
|
|
76
|
+
* 作为**新候选插在 node-corepack 之后、cmd-corepack 之前**:既有候选与顺序一个都没动。
|
|
77
|
+
* 兜底语义保持不变:只有"执行方式本身不可用"(ENOENT / Cannot find module)才换下一个,
|
|
78
|
+
* 真正的失败立即抛出(见 runPnpmWithFallback)。
|
|
79
|
+
* 全部候选都走注入的 `exists`;`env/resourcesPath/delimiter/nodeBin` 四个注入点只为离线单测,
|
|
80
|
+
* 生产调用方一个字都不用改(默认值就是当前进程的事实)。 */
|
|
81
|
+
function resolvePnpmRunners({
|
|
82
|
+
platform = process.platform,
|
|
83
|
+
execPath = process.execPath,
|
|
84
|
+
comspec = process.env.ComSpec ?? 'cmd.exe',
|
|
85
|
+
exists = existsSync,
|
|
86
|
+
env = process.env,
|
|
87
|
+
resourcesPath = process.resourcesPath,
|
|
88
|
+
delimiter = platform === 'win32' ? ';' : ':',
|
|
89
|
+
nodeBin = 'node',
|
|
90
|
+
} = {}) {
|
|
23
91
|
const binDir = dirname(execPath)
|
|
24
92
|
const corepackCandidates = [
|
|
25
93
|
join(binDir, 'node_modules', 'corepack', 'dist', 'corepack.js'), // Windows 官方安装器 / nvm-windows
|
|
@@ -27,17 +95,63 @@ function resolvePnpmRunners({ platform = process.platform, execPath = process.ex
|
|
|
27
95
|
join(binDir, '..', 'libexec', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // brew / 自编译布局
|
|
28
96
|
]
|
|
29
97
|
const runners = []
|
|
98
|
+
const add = (runner) => {
|
|
99
|
+
if (runners.some((r) => r.note === runner.note)) return // 同一路径可能被多条线索命中(resourcesPath + PATH)
|
|
100
|
+
runners.push(runner)
|
|
101
|
+
}
|
|
30
102
|
for (const js of corepackCandidates) {
|
|
31
103
|
if (exists(js)) {
|
|
32
104
|
runners.push({ kind: 'node-corepack', note: `node ${js} pnpm`, run: (args) => ({ bin: execPath, argv: [js, 'pnpm', ...args] }) })
|
|
33
105
|
}
|
|
34
106
|
}
|
|
107
|
+
|
|
108
|
+
// ── 新候选 ①/③:桌面端(Electron)自带运行时 ──────────────────────────────────
|
|
109
|
+
// ① Electron 的 `process.resourcesPath`(桌面端 = `<安装目录>\resources`);
|
|
110
|
+
// ③ 环境线索:PATH 条目里的 `<…>\runtime\pnpm\bin` 与 `<…>\runtime\bin`(桌面端 host 就是把
|
|
111
|
+
// `resources\runtime\bin` 拼进 profile 的 PATH 的,其同级就是 `runtime\pnpm\bin\pnpm.mjs`)。
|
|
112
|
+
const desktopNode = typeof env?.DSH_DESKTOP_NODE_EXECUTABLE === 'string' && env.DSH_DESKTOP_NODE_EXECUTABLE !== ''
|
|
113
|
+
? env.DSH_DESKTOP_NODE_EXECUTABLE
|
|
114
|
+
: execPath
|
|
115
|
+
const runtimeMjs = []
|
|
116
|
+
if (typeof resourcesPath === 'string' && resourcesPath !== '') runtimeMjs.push(join(resourcesPath, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'))
|
|
117
|
+
const pathValue = typeof env?.PATH === 'string' && env.PATH !== '' ? env.PATH : (typeof env?.Path === 'string' ? env.Path : '')
|
|
118
|
+
const pathDirs = String(pathValue).split(delimiter).map((d) => d.trim()).filter((d) => d !== '')
|
|
119
|
+
for (const dir of pathDirs) {
|
|
120
|
+
if (RUNTIME_PNPM_BIN_RE.test(dir)) runtimeMjs.push(join(dir, 'pnpm.mjs'))
|
|
121
|
+
else if (RUNTIME_BIN_RE.test(dir)) runtimeMjs.push(join(dirname(dir), 'pnpm', 'bin', 'pnpm.mjs'))
|
|
122
|
+
}
|
|
123
|
+
for (const js of runtimeMjs) {
|
|
124
|
+
if (!exists(js)) continue
|
|
125
|
+
add({
|
|
126
|
+
kind: 'desktop-pnpm-mjs',
|
|
127
|
+
note: `${desktopNode} --expose-internals ${js} pnpm`,
|
|
128
|
+
// 照抄桌面端 host 的形态(command=execPath,args=['--expose-internals', <pnpm.mjs>])。
|
|
129
|
+
// ELECTRON_RUN_AS_NODE 必须显式带上:不带它,Electron 二进制会当 **GUI 应用**启动 ——
|
|
130
|
+
// 等于弹一个新窗口,而且退出码可能是 0("假成功",比失败更糟)。
|
|
131
|
+
run: (args) => ({ bin: desktopNode, argv: ['--expose-internals', js, ...args], env: { ELECTRON_RUN_AS_NODE: '1' } }),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── 新候选 ②:PATH 扫描(pnpm.cmd(win) / pnpm(posix) / <dir>\pnpm\bin\pnpm.mjs)──────
|
|
136
|
+
for (const dir of pathDirs) {
|
|
137
|
+
const mjs = join(dir, 'pnpm', 'bin', 'pnpm.mjs')
|
|
138
|
+
if (exists(mjs)) add({ kind: 'path-pnpm-mjs', note: `${nodeBin} ${mjs} pnpm`, run: (args) => ({ bin: nodeBin, argv: [mjs, ...args] }) })
|
|
139
|
+
if (platform === 'win32') {
|
|
140
|
+
// .cmd 批处理不能直接 execFile(本机实测 EINVAL)→ 唯一通路是 cmd.exe(修好的单字符串形态)
|
|
141
|
+
if (exists(join(dir, 'pnpm.cmd'))) {
|
|
142
|
+
add({ kind: 'cmd-pnpm', note: `cmd /c pnpm(PATH: ${dir})`, run: (args) => ({ bin: comspec, argv: cmdShellArgv(cmdCommandLine(['pnpm', ...args])), verbatim: true }) })
|
|
143
|
+
}
|
|
144
|
+
} else if (exists(join(dir, 'pnpm'))) {
|
|
145
|
+
add({ kind: 'path-pnpm', note: join(dir, 'pnpm'), run: (args) => ({ bin: join(dir, 'pnpm'), argv: args }) })
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
35
149
|
if (platform === 'win32') {
|
|
36
|
-
//
|
|
150
|
+
// 修好的形态:单个命令字符串(段内各自加引号 + 转义 cmd 元字符)+ 整条外包裹 + verbatim
|
|
37
151
|
runners.push({
|
|
38
152
|
kind: 'cmd-corepack',
|
|
39
153
|
note: 'cmd /c corepack pnpm',
|
|
40
|
-
run: (args) => ({ bin: comspec, argv:
|
|
154
|
+
run: (args) => ({ bin: comspec, argv: cmdShellArgv(cmdCorepackCommand(args)), verbatim: true }),
|
|
41
155
|
})
|
|
42
156
|
} else {
|
|
43
157
|
runners.push({ kind: 'corepack', note: 'corepack pnpm', run: (args) => ({ bin: 'corepack', argv: ['pnpm', ...args] }) })
|
|
@@ -73,10 +187,21 @@ async function runPnpmWithFallback(args, { execOpts = {}, runners = resolvePnpmR
|
|
|
73
187
|
return wrapped
|
|
74
188
|
}
|
|
75
189
|
for (const runner of runners) {
|
|
76
|
-
|
|
190
|
+
// 执行方式可以带自己的 spawn 附加项(2026-09-27 加法):
|
|
191
|
+
// · `verbatim`:cmd.exe 形态必须原样透传命令行(否则 Node 会把段内引号转义成 `\"`,见 cmdCommandLine);
|
|
192
|
+
// · `env`:桌面端形态必须带 ELECTRON_RUN_AS_NODE=1(否则 Electron 二进制会当 GUI 应用启动)。
|
|
193
|
+
// 两者都只在 runner 明确给出时生效,既有的 node-corepack / corepack / pnpm 候选一个字不变。
|
|
194
|
+
const { bin, argv, verbatim = false, env: runnerEnv = null } = runner.run(args)
|
|
195
|
+
const opts = verbatim || runnerEnv !== null
|
|
196
|
+
? {
|
|
197
|
+
...execOpts,
|
|
198
|
+
...(verbatim ? { windowsVerbatimArguments: true } : {}),
|
|
199
|
+
...(runnerEnv === null ? {} : { env: { ...(typeof execOpts.env === 'object' && execOpts.env !== null ? execOpts.env : process.env), ...runnerEnv } }),
|
|
200
|
+
}
|
|
201
|
+
: execOpts
|
|
77
202
|
try {
|
|
78
203
|
// eslint-disable-next-line no-await-in-loop
|
|
79
|
-
await exec(bin, argv,
|
|
204
|
+
await exec(bin, argv, opts)
|
|
80
205
|
return { runner }
|
|
81
206
|
} catch (error) {
|
|
82
207
|
lastError = withStderr(error)
|
|
@@ -300,6 +425,9 @@ function execFileWithKillTree(bin, argv, opts = {}, deps = {}) {
|
|
|
300
425
|
cwd: opts.cwd,
|
|
301
426
|
env: opts.env,
|
|
302
427
|
windowsHide: opts.windowsHide !== false,
|
|
428
|
+
// 2026-09-27(加法):cmd.exe 形态要求"命令行原样透传"(见 infra/exec.js#cmdCommandLine)。
|
|
429
|
+
// 只有 runner 显式标了 verbatim 才会是 true;其余调用点行为完全不变(默认 false)。
|
|
430
|
+
windowsVerbatimArguments: opts.windowsVerbatimArguments === true,
|
|
303
431
|
detached: platform !== 'win32', // POSIX:自成进程组,-pid 才杀得掉整棵树(platform 可注入,见单测)
|
|
304
432
|
})
|
|
305
433
|
} catch (error) { finish(error, null); return }
|
|
@@ -434,4 +562,4 @@ const GH_BIN_CANDIDATES = [
|
|
|
434
562
|
join(homedir(), 'scoop', 'shims', 'gh.exe'),
|
|
435
563
|
]
|
|
436
564
|
|
|
437
|
-
export { GH_BIN_CANDIDATES, gitEnv, gitBin, processAlive, execFileAsync, resolvePnpmRunners, runPnpmWithFallback, killProcessTree, posixDescendants, execFileWithKillTree, KILL_WAIT_MS, KILL_WAIT_POLL_MS, GIT_LOW_SPEED_ENV, pnpmEnvOverrides, buildPnpmEnv, pnpmFetchArgs, pnpmAddArgs, unknownPnpmOption, runPnpmAdd }
|
|
565
|
+
export { GH_BIN_CANDIDATES, gitEnv, gitBin, processAlive, execFileAsync, resolvePnpmRunners, runPnpmWithFallback, killProcessTree, posixDescendants, execFileWithKillTree, KILL_WAIT_MS, KILL_WAIT_POLL_MS, GIT_LOW_SPEED_ENV, pnpmEnvOverrides, buildPnpmEnv, pnpmFetchArgs, pnpmAddArgs, unknownPnpmOption, runPnpmAdd, cmdCommandLine, cmdCorepackCommand, cmdShellArgv }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|