@twinsearth/w2m-dsh-plugin 1.9.8 → 1.9.10
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/bin/w2m-self-update.mjs +107 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.9.10/RELEASE-1910.json +21 -0
- package/docs/acceptance/1.9.9/RELEASE-199.json +21 -0
- package/lib/client.js +65 -2
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/src/plugin/tools.mjs +1 -1
- package/src/plugin/update-install.mjs +21 -2
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
-
"version": "1.9.
|
|
8
|
+
"version": "1.9.10"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "w2m-fleet",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
|
|
15
|
-
"version": "1.9.
|
|
15
|
+
"version": "1.9.10",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.9.
|
|
29
|
+
"version": "1.9.10"
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w2m-fleet",
|
|
3
3
|
"description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.10",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.9.10] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
- 新增 `bin/w2m-self-update.mjs`:可被编队任务直接下发的自更新入口,读取 GitHub Release、校验 SHA256SUMS、调用已验证安装器,输出一行 JSON,不重启 DSH。
|
|
13
|
+
- 固化升级策略:优先下发自更新指令;失败才使用 Node 远程执行回退(macOS 用 mac-self-update,Windows 用 node 替换+备份)。
|
|
14
|
+
- 参数:--profile、--dry-run、--allow-prerelease;环境变量 W2M_PROFILE_DIR / W2M_UPDATE_REPO。
|
|
15
|
+
|
|
16
|
+
## [1.9.9] - 2026-10-28
|
|
17
|
+
|
|
18
|
+
- 对话主工作区新增全文搜索:会话头部(置顶按钮同排)🔍 图标,点击展开搜索框,Enter/Shift+Enter 或 ↑/↓ 在已渲染对话中查找并高亮(Electron/Chromium find)。
|
|
19
|
+
|
|
20
|
+
- 修复 Windows「立即更新」失败:dsh 以 dsh.cmd 安装时,child_process.spawn(shell:false) 会报 EINVAL。现在 Windows 下不再把 .cmd/.bat 当作可直接执行计划,自动改用运行时 pnpm;若 dsh 计划出现 EINVAL,也会回退 pnpm 重试一次。
|
|
21
|
+
- 新增回归测试:.cmd 计划被拒绝并走 pnpm;spawn 失败仍回滚。
|
|
22
|
+
|
|
10
23
|
## [1.9.8] - 2026-10-28
|
|
11
24
|
|
|
12
25
|
- 左栏 GPW 入口改为纯图标(🌐),去掉重复的文字条目。
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Remote-triggerable, verified self-update for the W2M plugin.
|
|
4
|
+
*
|
|
5
|
+
* Fleet policy (operator): a machine is upgraded by dispatching this script as a task
|
|
6
|
+
* (`node <profile>/node_modules/@twinsearth/w2m-dsh-plugin/bin/w2m-self-update.mjs`), and only
|
|
7
|
+
* when that fails is a Node remote-execution fallback used. This file is therefore headless:
|
|
8
|
+
* it checks the configured GitHub release, verifies the published sha256, installs through the
|
|
9
|
+
* same updater the plugin UI uses, and prints one JSON line. It never restarts DSH.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node bin/w2m-self-update.mjs [--profile <dir|name>] [--dry-run] [--allow-prerelease] [--json]
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0 installed or already current; 1 check/install failed; 2 usage error.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { createAutoUpdater, findProfileDir } from '../src/plugin/auto-update.mjs';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_REPO = 'TwinsEarth/dsh-windows2macos';
|
|
22
|
+
const PACKAGE_NAME = '@twinsearth/w2m-dsh-plugin';
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = { profile: null, dryRun: false, allowPrerelease: false };
|
|
26
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
+
const arg = argv[i];
|
|
28
|
+
if (arg === '--profile') { args.profile = argv[i + 1] ?? null; i += 1; continue; }
|
|
29
|
+
if (arg === '--dry-run') { args.dryRun = true; continue; }
|
|
30
|
+
if (arg === '--allow-prerelease') { args.allowPrerelease = true; continue; }
|
|
31
|
+
if (arg === '--help' || arg === '-h') { args.help = true; continue; }
|
|
32
|
+
if (arg === '--json') { args.json = true; continue; }
|
|
33
|
+
args.unknown = arg;
|
|
34
|
+
}
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readInstalledVersion(profileDir) {
|
|
39
|
+
try {
|
|
40
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'node_modules', ...PACKAGE_NAME.split('/'), 'package.json'), 'utf8'));
|
|
41
|
+
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveProfile(input) {
|
|
48
|
+
if (input !== null) {
|
|
49
|
+
if (existsSync(input)) return input;
|
|
50
|
+
// A bare profile name resolves under the DSH home directory.
|
|
51
|
+
const named = join(process.env.DSH_HOME ?? join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh'), 'profiles', input);
|
|
52
|
+
if (existsSync(named)) return named;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof process.env.W2M_PROFILE_DIR === 'string' && existsSync(process.env.W2M_PROFILE_DIR)) return process.env.W2M_PROFILE_DIR;
|
|
56
|
+
return findProfileDir() ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const args = parseArgs(process.argv.slice(2));
|
|
60
|
+
if (args.help === true) {
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
'Usage: w2m-self-update [--profile <dir|name>] [--dry-run] [--allow-prerelease]\n' +
|
|
63
|
+
'Checks GitHub releases, verifies sha256, installs the newer plugin build for the next start.\n',
|
|
64
|
+
);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
if (args.unknown !== undefined) {
|
|
68
|
+
process.stderr.write(`unknown argument: ${args.unknown}\n`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const profileDir = resolveProfile(args.profile);
|
|
73
|
+
if (profileDir === null) {
|
|
74
|
+
process.stdout.write(JSON.stringify({ ok: false, outcome: 'error', error: 'no DSH profile directory found; pass --profile <dir>' }) + '\n');
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const currentVersion = readInstalledVersion(profileDir);
|
|
79
|
+
const updater = createAutoUpdater({
|
|
80
|
+
profileDir,
|
|
81
|
+
currentVersion,
|
|
82
|
+
repo: process.env.W2M_UPDATE_REPO ?? DEFAULT_REPO,
|
|
83
|
+
dryRun: args.dryRun,
|
|
84
|
+
allowPrerelease: args.allowPrerelease,
|
|
85
|
+
enabled: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let result;
|
|
89
|
+
try {
|
|
90
|
+
result = await updater.check('manual');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
process.stdout.write(JSON.stringify({ ok: false, outcome: 'error', error: error instanceof Error ? error.message : String(error) }) + '\n');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const outcome = result?.outcome ?? 'unknown';
|
|
97
|
+
const ok = result?.ok === true && (outcome === 'installed' || outcome === 'up-to-date' || outcome === 'current' || outcome === 'skipped');
|
|
98
|
+
process.stdout.write(JSON.stringify({
|
|
99
|
+
ok,
|
|
100
|
+
outcome,
|
|
101
|
+
current: currentVersion,
|
|
102
|
+
latest: result?.tag ?? result?.to ?? null,
|
|
103
|
+
reason: result?.reason ?? null,
|
|
104
|
+
error: result?.error ?? null,
|
|
105
|
+
restart_required: outcome === 'installed',
|
|
106
|
+
}) + '\n');
|
|
107
|
+
process.exit(ok ? 0 : 1);
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.10",
|
|
3
|
+
"case": "RELEASE-1910",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-27T23:24:12.342Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-1910-1793143452343",
|
|
11
|
+
"requirement": "V1.9.10:可下发的自更新 CLI 与编队升级策略",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"cli": "bin/w2m-self-update.mjs --help exit 0",
|
|
16
|
+
"dry_run": "current 1.9.9, latest v1.9.9, skipped, exit 0",
|
|
17
|
+
"policy": "prefer dispatch; Node remote-execution fallback; operator token required",
|
|
18
|
+
"gates": "full suite + preflight expected GREEN before tag"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.9",
|
|
3
|
+
"case": "RELEASE-199",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-27T22:54:14.249Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-199-1793141654250",
|
|
11
|
+
"requirement": "V1.9.9:修复 Windows 立即更新 spawn EINVAL(dsh.cmd),回退运行时 pnpm",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"plan": "win32 .cmd/.bat shim never selected",
|
|
16
|
+
"fallback": "EINVAL on dsh plan retries once with runtime pnpm",
|
|
17
|
+
"tests": "update-install 28/28 green",
|
|
18
|
+
"gates": "full suite + preflight expected GREEN before tag"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -499,6 +499,12 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
499
499
|
'.w2mChat_emoji{padding:2px 4px;border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer}',
|
|
500
500
|
'.w2mFleetStatus{display:flex;gap:16px;align-items:center;padding:2px 12px;font-size:11px;opacity:.8;white-space:nowrap}',
|
|
501
501
|
'.w2mFleetStatusItem{white-space:nowrap}',
|
|
502
|
+
'.w2mSearchToggle{height:26px;width:26px;border:0;border-radius:6px;background:transparent;color:inherit;font-size:14px;cursor:pointer}',
|
|
503
|
+
'.w2mSearchToggle:hover{background:rgba(127,127,127,.14)}',
|
|
504
|
+
'.w2mSearchBox{display:flex;align-items:center;gap:4px;padding:2px 4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-background-secondary)}',
|
|
505
|
+
'.w2mSearchInput{height:24px;min-width:220px;border:0;background:transparent;color:inherit;font:inherit;outline:none}',
|
|
506
|
+
'.w2mSearchBtn{height:24px;min-width:24px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
|
|
507
|
+
'.w2mSearchBtn:hover{background:rgba(127,127,127,.14)}',
|
|
502
508
|
'.w2mGpw_admin{padding:24px 32px 48px;box-sizing:border-box;max-width:1400px;margin:0 auto;color:var(--dsw-alias-label-primary);font-size:13px}',
|
|
503
509
|
'.w2mGpw_adminHead{margin-bottom:16px}',
|
|
504
510
|
'.w2mGpw_adminTitle{margin:0 0 6px;font-size:22px;line-height:30px;font-weight:600}',
|
|
@@ -794,7 +800,6 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
794
800
|
},
|
|
795
801
|
}, [
|
|
796
802
|
h('span', { key: 'i', className: 'w2mGpw_sideIcon', 'aria-hidden': 'true' }, '🌐'),
|
|
797
|
-
h('span', { key: 'l', className: 'w2mGpw_sideLabel' }, 'GPW'),
|
|
798
803
|
]);
|
|
799
804
|
}
|
|
800
805
|
|
|
@@ -925,6 +930,52 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
925
930
|
]);
|
|
926
931
|
}
|
|
927
932
|
|
|
933
|
+
/**
|
|
934
|
+
* v1.9.9: in-conversation full-text search. The icon sits in the session header next to Pin; the
|
|
935
|
+
* search itself uses the browser's own find (Electron/Chromium), which highlights and scrolls the
|
|
936
|
+
* rendered conversation without reading private session data through a plugin API.
|
|
937
|
+
*/
|
|
938
|
+
function ConversationSearch() {
|
|
939
|
+
const [open, setOpen] = React.useState(false);
|
|
940
|
+
const [query, setQuery] = React.useState('');
|
|
941
|
+
const inputRef = React.useRef(null);
|
|
942
|
+
React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
|
|
943
|
+
const find = (backwards) => {
|
|
944
|
+
if (query === '') return;
|
|
945
|
+
try {
|
|
946
|
+
if (typeof window !== 'undefined' && typeof window.find === 'function') {
|
|
947
|
+
window.find(query, false, backwards === true, true);
|
|
948
|
+
}
|
|
949
|
+
} catch { /* find is best effort, never a crash */ }
|
|
950
|
+
};
|
|
951
|
+
if (!open) {
|
|
952
|
+
return h('button', {
|
|
953
|
+
type: 'button',
|
|
954
|
+
className: 'w2mSearchToggle',
|
|
955
|
+
title: '搜索本对话',
|
|
956
|
+
'aria-label': '搜索本对话',
|
|
957
|
+
onClick: () => setOpen(true),
|
|
958
|
+
}, '🔍');
|
|
959
|
+
}
|
|
960
|
+
return h('div', { className: 'w2mSearchBox' }, [
|
|
961
|
+
h('input', {
|
|
962
|
+
key: 'q',
|
|
963
|
+
ref: inputRef,
|
|
964
|
+
className: 'w2mSearchInput',
|
|
965
|
+
value: query,
|
|
966
|
+
placeholder: '搜索本对话全部内容',
|
|
967
|
+
onChange: (event) => setQuery(event.target.value),
|
|
968
|
+
onKeyDown: (event) => {
|
|
969
|
+
if (event.key === 'Enter') { event.preventDefault(); find(event.shiftKey); }
|
|
970
|
+
if (event.key === 'Escape') { setOpen(false); setQuery(''); }
|
|
971
|
+
},
|
|
972
|
+
}),
|
|
973
|
+
h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => find(true) }, '↑'),
|
|
974
|
+
h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => find(false) }, '↓'),
|
|
975
|
+
h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); } }, '×'),
|
|
976
|
+
]);
|
|
977
|
+
}
|
|
978
|
+
|
|
928
979
|
/** v1.9.4: the GPW tab in Settings, between Models and Plugins. */
|
|
929
980
|
function GpwSettingsPage({ t }) {
|
|
930
981
|
return h('div', { className: 'w2mGpw_settingsBody' }, h(GpwPanel, { t, sessionId: undefined, inline: true, onClose: () => {} }));
|
|
@@ -2060,7 +2111,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
2060
2111
|
name: 'sidebar.panellist',
|
|
2061
2112
|
id: 'w2m-gpw',
|
|
2062
2113
|
order: 4,
|
|
2063
|
-
label: () => '
|
|
2114
|
+
label: () => '',
|
|
2064
2115
|
inject: () => ({}),
|
|
2065
2116
|
}, GpwSidebarIcon));
|
|
2066
2117
|
} catch (error) {
|
|
@@ -2128,6 +2179,18 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
2128
2179
|
|
|
2129
2180
|
|
|
2130
2181
|
|
|
2182
|
+
// v1.9.9: in-conversation search, in the session header next to Pin.
|
|
2183
|
+
try {
|
|
2184
|
+
slots.inject('conversation.session.header.actions', () => slots.register({
|
|
2185
|
+
name: 'conversation.session.header.actions',
|
|
2186
|
+
id: 'w2m-conversation-search',
|
|
2187
|
+
order: 90,
|
|
2188
|
+
label: () => '搜索本对话',
|
|
2189
|
+
inject: () => ({}),
|
|
2190
|
+
}, ConversationSearch));
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
console.error('@twinsearth/w2m-dsh-plugin: the conversation search control was not mounted', error);
|
|
2193
|
+
}
|
|
2131
2194
|
// v1.9.6: fleet chat as a real conversation View tab (after Chat and Rules), rendering the
|
|
2132
2195
|
// full chat page in the main workspace.
|
|
2133
2196
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.10",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -205,7 +205,8 @@
|
|
|
205
205
|
"w2m-secondary-link": "bin/w2m-secondary-link.mjs",
|
|
206
206
|
"w2m-bootstrap": "bin/w2m-bootstrap.mjs",
|
|
207
207
|
"w2m-bootstrap-sign": "bin/w2m-bootstrap-sign.mjs",
|
|
208
|
-
"w2m-hidden-task": "bin/w2m-hidden-task.mjs"
|
|
208
|
+
"w2m-hidden-task": "bin/w2m-hidden-task.mjs",
|
|
209
|
+
"w2m-self-update": "bin/w2m-self-update.mjs"
|
|
209
210
|
},
|
|
210
211
|
"disclosure": {
|
|
211
212
|
"cloud": true,
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.10",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -283,7 +283,7 @@ const MAX_STAGES = 16;
|
|
|
283
283
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
284
284
|
* overrides it at runtime for tests and packagers.
|
|
285
285
|
*/
|
|
286
|
-
const PLUGIN_VERSION = '1.9.
|
|
286
|
+
const PLUGIN_VERSION = '1.9.10';
|
|
287
287
|
|
|
288
288
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
289
289
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -316,8 +316,12 @@ export function planInstall(input) {
|
|
|
316
316
|
const dshPath = input.dshPath ?? null;
|
|
317
317
|
const pnpmPath = input.pnpmPath ?? null;
|
|
318
318
|
const nodePath = input.nodePath ?? process.execPath;
|
|
319
|
+
// v1.9.9: a .cmd/.bat shim is a Windows shell script; spawn(shell:false) refuses it with EINVAL
|
|
320
|
+
// ("could not run dsh (EINVAL: spawn EINVAL)"), and no other platform can execute it either.
|
|
321
|
+
// Only a directly executable path may take the dsh plan; anything else uses the runtime pnpm.
|
|
322
|
+
const dshSpawnable = dshPath !== null && !/\.(cmd|bat)$/i.test(dshPath);
|
|
319
323
|
|
|
320
|
-
if (
|
|
324
|
+
if (dshSpawnable) {
|
|
321
325
|
return {
|
|
322
326
|
via: 'dsh',
|
|
323
327
|
// `dsh plugin --profile <name> add <tarball>` is the supported surface;
|
|
@@ -531,7 +535,7 @@ export async function installUpdate(options = {}) {
|
|
|
531
535
|
const packageName = manifest?.name ?? options.packageName ?? DEFAULT_PACKAGE_NAME;
|
|
532
536
|
const from = readInstalledVersion(absoluteProfile, packageName);
|
|
533
537
|
|
|
534
|
-
|
|
538
|
+
let plan = planInstall({
|
|
535
539
|
profileDir: absoluteProfile,
|
|
536
540
|
profileName,
|
|
537
541
|
stagedTarball,
|
|
@@ -639,6 +643,21 @@ export async function installUpdate(options = {}) {
|
|
|
639
643
|
};
|
|
640
644
|
}
|
|
641
645
|
|
|
646
|
+
// v1.9.9: a spawn-level failure on the dsh plan (EINVAL/ENOENT) is retried once with the runtime
|
|
647
|
+
// pnpm, because that is the installer the same machine already uses successfully.
|
|
648
|
+
if (result?.spawnError?.code === 'EINVAL' && plan.via === 'dsh' && pnpmPath) {
|
|
649
|
+
const fallback = planInstall({ profileDir: absoluteProfile, profileName, stagedTarball, dshPath: null, pnpmPath, nodePath });
|
|
650
|
+
if (fallback !== null) {
|
|
651
|
+
step('dsh could not be spawned; retrying with the runtime pnpm');
|
|
652
|
+
plan = fallback;
|
|
653
|
+
try {
|
|
654
|
+
result = await runner({ argv: plan.argv, cwd: plan.cwd, via: plan.via });
|
|
655
|
+
} catch (error) {
|
|
656
|
+
result = { code: null, stdout: '', stderr: '', spawnError: { code: 'RUNNER_THREW', message: String(error?.message ?? error) }, timedOut: false };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
642
661
|
const stderrTail = tailBytes(result?.stderr ?? '', stderrTailBytes);
|
|
643
662
|
const stdoutTail = tailBytes(result?.stdout ?? '', stderrTailBytes);
|
|
644
663
|
const spawnFailed = result?.spawnError ?? null;
|