dsh-mobile-plugin 0.4.8 → 0.5.0
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/icon.png +0 -0
- package/lib/mobile/history-derive.js +86 -2
- package/lib/mobile/host-settings.js +103 -6
- package/lib/mobile/live-steps.js +356 -0
- package/lib/mobile/model-prices-table.cjs +23 -0
- package/lib/mobile/model-prices.cjs +88 -0
- package/lib/mobile/plugin-update.js +16 -1
- package/lib/mobile/session-cache.js +17 -1
- package/lib/mobile/session-step.js +226 -0
- package/lib/mobile/stats-collect.cjs +40 -0
- package/lib/mobile/stats-ledger.cjs +45 -0
- package/lib/mobile/step-index.js +108 -0
- package/lib/mobile/stream.js +29 -0
- package/lib/panel/index.html +11 -11
- package/lib/reconnect-scheduler.cjs +19 -6
- package/lib/relay/connection-check.js +115 -0
- package/lib/relay/firewall-probe.js +300 -0
- package/lib/relay/preview.js +147 -24
- package/lib/relay/tunnel.js +26 -1
- package/lib/routes/control-plane.js +47 -1
- package/lib/routes/mobile-api.js +3 -0
- package/lib/sidecar-facade-worker.cjs +44 -2
- package/lib/update-install.cjs +301 -43
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +5 -1
package/lib/update-install.cjs
CHANGED
|
@@ -7,16 +7,26 @@
|
|
|
7
7
|
* 另一个插件没装、太老或坏了就整个失效。
|
|
8
8
|
*
|
|
9
9
|
* 不自己实现包管理,只驱动它:profile 的 pnpm-workspace.yaml 已经写好 node-linker、peer 等
|
|
10
|
-
* 关键设置,这里只「cd 到 profile 跑一句 pnpm add
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* 关键设置,这里只「cd 到 profile 跑一句 pnpm add <包名>@<版本>」,不传布局类开关。必要的
|
|
11
|
+
* 开关有两层:auto-install-peers=false(peer 是运行时注入的,npm 上没有)、minimum-release-age=0
|
|
12
|
+
* (2026-10-26 用户要求一检测到新版就立刻装,不等「新发布先晾一会儿」;那道策略查的是整份
|
|
13
|
+
* lockfile,别的插件刚发也会连累我们),再加上 CI=1(无终端)。命令行带一份、环境变量再带一份:
|
|
14
|
+
* pnpm 12 的原生 CLI 会静默忽略这两个 --config 参数,环境变量那一份还认。
|
|
15
|
+
*
|
|
16
|
+
* 装完要**登记**:这个版本是被我们绕过晾置期装进去的,不写进 profile 的 minimumReleaseAgeExclude,
|
|
17
|
+
* 它就会留在 lockfile 里,之后 pnpm 校验整份 lockfile 时把这台机器上所有安装都拒掉
|
|
18
|
+
* (官方桌面端不替用户临时放行,只能干等 24 小时)。
|
|
14
19
|
*
|
|
15
20
|
* 包管理器优先用宿主发布的那个(profile 是宿主自带 pnpm 装的,PATH 上版本不同会
|
|
16
21
|
* ERR_PNPM_UNEXPECTED_STORE),找不到再退回 PATH。装之前先看有没有 agent 在跑;force 与 auto
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* 都跳过这道闸(2026-10-26 用户要求自动更新一检测到新版就立刻装,与面板上那颗「强制更新」
|
|
23
|
+
* 同一个口径),两者的区别只剩「没新版时装不装」。顺序上先答「有没有新版」,没东西可装时
|
|
24
|
+
* 不该拿「有对话在跑」挡在前面。
|
|
25
|
+
*
|
|
26
|
+
* 宿主环境对不上时先修一次再重试:node_modules 是旧版 pnpm 建的(两个 hoist 相关的
|
|
27
|
+
* ERR_PNPM_*_DIFF)就重建依赖树;记录的 store 和现在解析的不是同一个(ERR_PNPM_UNEXPECTED_STORE)
|
|
28
|
+
* 就把记录里的那个 store 写回 profile 的 storeDir。两种都只修一次,修不了就如实失败 —— 这类
|
|
29
|
+
* 现场不修的话,这台机器之后每一次更新都会撞同一堵墙。
|
|
20
30
|
*
|
|
21
31
|
* 只动 profile 的依赖树;找不到包管理器就如实失败;装完以盘上版本号为准,退出码 0 不算装上。
|
|
22
32
|
*/
|
|
@@ -34,6 +44,9 @@ const DEFAULT_NAME = 'dsh-mobile-plugin';
|
|
|
34
44
|
/** 一次安装的上限:pnpm 装一个包 + 拉依赖,给足但别无限等。 */
|
|
35
45
|
const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
36
46
|
|
|
47
|
+
/** 依赖树是旧版 pnpm 建的:这两种报错都只说明「要重建」,不是这一版有问题。 */
|
|
48
|
+
const HOIST_DIFF = /ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF|ERR_PNPM_VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF/;
|
|
49
|
+
|
|
37
50
|
/** 命令输出留多少字符(日志与错误信息里给个人看得懂的尾巴)。 */
|
|
38
51
|
const OUTPUT_TAIL = 4000;
|
|
39
52
|
|
|
@@ -88,6 +101,90 @@ function spawnEnv(dirs, env = process.env) {
|
|
|
88
101
|
return { ...env, PATH: [...dirs, ...existing].join(path.delimiter), CI: env.CI || '1' };
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
/**
|
|
105
|
+
* 每次调 pnpm 都带上的那两格策略开关,**环境变量这一份不是备份,是另一半**。
|
|
106
|
+
*
|
|
107
|
+
* pnpm 11 两种写法都认;pnpm 12 的原生 CLI 会静默忽略部分 `--config.*`(市场在 #600 上实测
|
|
108
|
+
* 过,一次装失败一次白等),但同一个键走 `PNPM_CONFIG_*` 它照读。命令行与环境变量各带一份,
|
|
109
|
+
* 换哪个大版本都不用重新猜。
|
|
110
|
+
*/
|
|
111
|
+
const POLICY_ENV = {
|
|
112
|
+
PNPM_CONFIG_AUTO_INSTALL_PEERS: 'false',
|
|
113
|
+
PNPM_CONFIG_MINIMUM_RELEASE_AGE: '0',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** 「配置里这个包用的是哪个注册表」缓存多久:查更新两分钟一轮,源不常换。 */
|
|
117
|
+
const REGISTRY_CACHE_MS = 5 * 60 * 1000;
|
|
118
|
+
|
|
119
|
+
/** profile 目录 → 上一次问到的注册表(问不到也记,省得每轮重跑一次包管理器)。 */
|
|
120
|
+
const registryCache = new Map();
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 从 `<pm> config get registry` 的输出里取地址。
|
|
124
|
+
*
|
|
125
|
+
* pnpm / npm 都只回一行地址;升级提示、警告可能在前面,所以从后往前找第一个 http(s) 行。
|
|
126
|
+
*/
|
|
127
|
+
function parseRegistryOutput(output) {
|
|
128
|
+
const lines = String(output || '').split('\n').map(line => line.trim()).filter(Boolean);
|
|
129
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
130
|
+
if (/^https?:\/\/\S+$/.test(lines[i])) return lines[i].replace(/\/+$/, '');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 这台机器上 pnpm / npm 真正会用的注册表。
|
|
137
|
+
*
|
|
138
|
+
* 问的是包管理器自己(`config get registry`),不是我们读文件猜:registry 可能来自 profile 的
|
|
139
|
+
* `.npmrc`、用户级 rc、pnpm-workspace.yaml 的 registries,还可以是环境变量改的 —— 只有它自己
|
|
140
|
+
* 的解析链能给出同一个答案。查更新与真正安装因此走同一个源,不会「查得到、装不上」,也是
|
|
141
|
+
* 镜像 / 私有源机器上自动更新还能自己动的前提。
|
|
142
|
+
*
|
|
143
|
+
* 拿不到(没有包管理器、命令超时、输出里没有地址)就回 null,调用方退回官方源:
|
|
144
|
+
* 这条链上多一次探测不该把「检查更新」变成不可用。`DSHM_NPM_REGISTRY` 显式给了值时不探,
|
|
145
|
+
* 那是给人排查 / 私有部署用的最高优先级。
|
|
146
|
+
*/
|
|
147
|
+
async function profileRegistry({
|
|
148
|
+
profileNodeModules,
|
|
149
|
+
packageManager = null,
|
|
150
|
+
env = process.env,
|
|
151
|
+
platform = process.platform,
|
|
152
|
+
dirs = null,
|
|
153
|
+
timeoutMs = 20000,
|
|
154
|
+
cache = registryCache,
|
|
155
|
+
now = Date.now,
|
|
156
|
+
} = {}) {
|
|
157
|
+
// `DSHM_NPM_REGISTRY` 显式给了值就不探:那是给人排查 / 私有部署用的最高优先级,
|
|
158
|
+
// 而且查更新那一半读的是进程环境,这里不能只看子进程那一份。
|
|
159
|
+
const override = String(env.DSHM_NPM_REGISTRY || process.env.DSHM_NPM_REGISTRY || '').trim();
|
|
160
|
+
if (!profileNodeModules || override !== '') return null;
|
|
161
|
+
const profileDir = path.dirname(profileNodeModules);
|
|
162
|
+
const hit = cache.get(profileDir);
|
|
163
|
+
if (hit && now() - hit.at < REGISTRY_CACHE_MS) return hit.registry;
|
|
164
|
+
const searchDirs = dirs ?? toolSearchDirs({ platform, env });
|
|
165
|
+
const childEnv = spawnEnv(searchDirs, env);
|
|
166
|
+
const hostInvocation = normalizeInvocation(packageManager);
|
|
167
|
+
const candidates = [];
|
|
168
|
+
if (hostInvocation) candidates.push({ command: hostInvocation.command, prefix: hostInvocation.args, extraEnv: hostInvocation.env });
|
|
169
|
+
const pnpm = resolveExecutable('pnpm', searchDirs, { platform, env });
|
|
170
|
+
if (pnpm) candidates.push({ command: pnpm, prefix: [], extraEnv: {} });
|
|
171
|
+
const npm = resolveExecutable('npm', searchDirs, { platform, env });
|
|
172
|
+
if (npm) candidates.push({ command: npm, prefix: [], extraEnv: {} });
|
|
173
|
+
let registry = null;
|
|
174
|
+
for (const candidate of candidates) {
|
|
175
|
+
const result = await run(candidate.command, [...candidate.prefix, 'config', 'get', 'registry'], {
|
|
176
|
+
cwd: profileDir,
|
|
177
|
+
env: { ...childEnv, ...candidate.extraEnv },
|
|
178
|
+
timeoutMs,
|
|
179
|
+
});
|
|
180
|
+
if (!result.ok) continue;
|
|
181
|
+
registry = parseRegistryOutput(result.output);
|
|
182
|
+
if (registry) break;
|
|
183
|
+
}
|
|
184
|
+
cache.set(profileDir, { at: now(), registry });
|
|
185
|
+
return registry;
|
|
186
|
+
}
|
|
187
|
+
|
|
91
188
|
/** profile 里装着的那份插件包的版本号(读不到返回 null)。 */
|
|
92
189
|
function installedVersion(profileNodeModules, name = DEFAULT_NAME) {
|
|
93
190
|
try {
|
|
@@ -155,7 +252,136 @@ function installAllowedFor(source) {
|
|
|
155
252
|
}
|
|
156
253
|
|
|
157
254
|
/**
|
|
158
|
-
*
|
|
255
|
+
* 把一条「这一版是我故意装的」写进 profile 的 `minimumReleaseAgeExclude`。
|
|
256
|
+
*
|
|
257
|
+
* 为什么非要写:我们是用一次性开关把 24 小时内的版本装进去的,那个开关只对那一条命令生效。
|
|
258
|
+
* 这一版随后留在 lockfile 里,pnpm 11 在**任何** add / remove 之前都会拿当前策略校验整份
|
|
259
|
+
* lockfile —— 官方桌面端不替用户临时放行,于是这台机器上所有插件安装都要干等到它满 24 小时,
|
|
260
|
+
* 报错还会点名我们这个包。写进去就是把「我信任这一版」这件事落在配置里,撤销的代价只有一行。
|
|
261
|
+
*
|
|
262
|
+
* 规则跟市场那边一致:**同名只读第一条**(pnpm 的 #883),所以必须并进已有的那一行,
|
|
263
|
+
* 不能另起一行;条目可以是 `name@v || name@v2`,也可能是带引号的。单行的 flow 写法
|
|
264
|
+
* (`minimumReleaseAgeExclude: [a, b]`)不碰 —— 改坏了比不写更贵,而 pnpm 自己写出来的是块形式。
|
|
265
|
+
*
|
|
266
|
+
* 失败一律返回 false,不抛:登记是善后,不是更新本身,它出问题不该把装好的版本说成没装上。
|
|
267
|
+
*/
|
|
268
|
+
function recordReleaseAgeExclusion(profileDir, name, version) {
|
|
269
|
+
const file = path.join(profileDir, 'pnpm-workspace.yaml');
|
|
270
|
+
let text;
|
|
271
|
+
try {
|
|
272
|
+
text = fs.readFileSync(file, 'utf8');
|
|
273
|
+
} catch {
|
|
274
|
+
// 没有 workspace 文件就什么都不做:替人发明一个会改掉 pnpm 认的 workspace 根。
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const lines = text.split('\n');
|
|
278
|
+
const at = lines.findIndex(line => /^minimumReleaseAgeExclude\s*:/.test(line));
|
|
279
|
+
if (at !== -1) {
|
|
280
|
+
const inline = lines[at].replace(/^minimumReleaseAgeExclude\s*:/, '').trim();
|
|
281
|
+
if (inline !== '' && !inline.startsWith('#')) return false;
|
|
282
|
+
let end = at;
|
|
283
|
+
for (let i = at + 1; i < lines.length; i += 1) {
|
|
284
|
+
const line = lines[i];
|
|
285
|
+
if (/^\s*$/.test(line) || /^\s*#/.test(line) || /^\s+-/.test(line)) {
|
|
286
|
+
end = i;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
for (let i = at + 1; i <= end; i += 1) {
|
|
292
|
+
const match = /^(\s*-\s*)(.*)$/.exec(lines[i]);
|
|
293
|
+
if (!match) continue;
|
|
294
|
+
const entry = parseExcludeEntry(match[2]);
|
|
295
|
+
if (entry === null || entry.name !== name) continue;
|
|
296
|
+
if (entry.versions.includes(version)) return true;
|
|
297
|
+
lines[i] = `${match[1]}${addVersionToEntry(match[2], version)}`;
|
|
298
|
+
fs.writeFileSync(file, lines.join('\n'));
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
let indent = ' ';
|
|
302
|
+
for (let i = at + 1; i <= end; i += 1) {
|
|
303
|
+
const match = /^(\s+)-/.exec(lines[i]);
|
|
304
|
+
if (match) {
|
|
305
|
+
indent = match[1];
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
lines.splice(end + 1, 0, `${indent}- ${excludeScalar(name, version)}`);
|
|
310
|
+
fs.writeFileSync(file, lines.join('\n'));
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
const tail = text.replace(/\s*$/, '');
|
|
314
|
+
fs.writeFileSync(file, `${tail}${tail ? '\n' : ''}minimumReleaseAgeExclude:\n - ${excludeScalar(name, version)}\n`);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 一条排除规则:包名 + 已列举的版本(拼在 ` || ` 后面)。看不懂就回 null,别猜。 */
|
|
319
|
+
function parseExcludeEntry(raw) {
|
|
320
|
+
let body = String(raw || '').trim();
|
|
321
|
+
if (body === '' || body.startsWith('#')) return null;
|
|
322
|
+
const quote = /^(['"])(.*)\1\s*$/.exec(body);
|
|
323
|
+
if (quote) {
|
|
324
|
+
body = quote[2];
|
|
325
|
+
} else {
|
|
326
|
+
const hash = body.search(/\s#/);
|
|
327
|
+
if (hash !== -1) body = body.slice(0, hash).trim();
|
|
328
|
+
}
|
|
329
|
+
const parts = body.split('||').map(part => part.trim()).filter(Boolean);
|
|
330
|
+
if (parts.length === 0) return null;
|
|
331
|
+
const at = parts[0].lastIndexOf('@');
|
|
332
|
+
if (at <= 0) return null;
|
|
333
|
+
return {
|
|
334
|
+
name: parts[0].slice(0, at),
|
|
335
|
+
versions: parts.map(part => part.slice(part.lastIndexOf('@') + 1)),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** 往一条规则后面追版本,引号与行尾注释原样保留。 */
|
|
340
|
+
function addVersionToEntry(raw, version) {
|
|
341
|
+
const body = String(raw).trim();
|
|
342
|
+
const quote = /^(['"])(.*)\1\s*$/.exec(body);
|
|
343
|
+
if (quote) return `${quote[1]}${quote[2]} || ${version}${quote[1]}`;
|
|
344
|
+
const hash = body.search(/\s#/);
|
|
345
|
+
if (hash === -1) return `${body} || ${version}`;
|
|
346
|
+
return `${body.slice(0, hash)} || ${version}${body.slice(hash)}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 写进 YAML 的一条新规则;`@scope/...` 这种以 `@` 开头的标量必须带引号。 */
|
|
350
|
+
function excludeScalar(name, version) {
|
|
351
|
+
const value = `${name}@${version}`;
|
|
352
|
+
return value.startsWith('@') ? `'${value}'` : value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* pnpm 说「记录的 store 和现在解析的不是同一个」时,把记录里的那个写回 profile。
|
|
357
|
+
*
|
|
358
|
+
* 这是 pnpm 报错里给的持久修法(`store-dir` 只在命令行那一次有效,改不改写
|
|
359
|
+
* `node_modules/.modules.yaml` 里的记录才是关键)。只加、不改:已经有 `storeDir` 就不动,
|
|
360
|
+
* 那种情况不是我们该替人翻的。
|
|
361
|
+
*/
|
|
362
|
+
function ensureProfileStoreDir(profileDir, storeDir) {
|
|
363
|
+
const file = path.join(profileDir, 'pnpm-workspace.yaml');
|
|
364
|
+
let text;
|
|
365
|
+
try {
|
|
366
|
+
text = fs.readFileSync(file, 'utf8');
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
if (/^storeDir\s*:/m.test(text)) return false;
|
|
371
|
+
const quoted = /[\s#:'"]/.test(storeDir) ? JSON.stringify(storeDir) : storeDir;
|
|
372
|
+
const tail = text.replace(/\s*$/, '');
|
|
373
|
+
fs.writeFileSync(file, `${tail}${tail ? '\n' : ''}storeDir: ${quoted}\n`);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** `child` 是不是 `parent` 以内(用来确认报错里的 node_modules 真是这个 profile 的)。 */
|
|
378
|
+
function isInsideDir(parent, child) {
|
|
379
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
380
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 宿主**发布出来的**包管理器调用方式(DSH 桌面版把它挂在 `profileContext.packageManager`
|
|
159
385
|
* 上,形状 `{command, args, env?}`)。
|
|
160
386
|
*
|
|
161
387
|
* 为什么非得用它、而不是 PATH 上那个 pnpm:桌面版由 GUI 拉起时 PATH 很干净,而且**版本
|
|
@@ -243,18 +469,15 @@ function run(command, args, { cwd, env, timeoutMs, spawnImpl = spawn }) {
|
|
|
243
469
|
* @param {object} options
|
|
244
470
|
* @param {string} options.profileNodeModules profile 的 `node_modules` 目录
|
|
245
471
|
* @param {string} options.version 要装的版本(注册表给的)
|
|
246
|
-
* @param {boolean} [options.force] true = 跳过「新发布先晾一会儿」的门槛
|
|
247
|
-
* @param {boolean} [options.auto] 自动更新那一轮:同样跳过发布晾置期(见文件头)
|
|
248
472
|
* @param {() => any} [options.beforeInstall] 真正跑包管理器**之前**调一次(外壳用它把正在跑的
|
|
249
473
|
* 那一代复制进回退目录)。包管理器一跑就把旧的 store 目录清掉了,晚一步没得复制。
|
|
250
|
-
* @returns {Promise<{installed, skipped, beforeVersion, installedVersion, source, command,
|
|
474
|
+
* @returns {Promise<{installed, skipped, beforeVersion, installedVersion, source, command,
|
|
475
|
+
* manager, healed, releaseAgeRecorded, output, failure}>}
|
|
251
476
|
*/
|
|
252
477
|
async function installLatest({
|
|
253
478
|
profileNodeModules,
|
|
254
479
|
version,
|
|
255
480
|
name = DEFAULT_NAME,
|
|
256
|
-
force = false,
|
|
257
|
-
auto = false,
|
|
258
481
|
timeoutMs = INSTALL_TIMEOUT_MS,
|
|
259
482
|
env = process.env,
|
|
260
483
|
platform = process.platform,
|
|
@@ -333,41 +556,55 @@ async function installLatest({
|
|
|
333
556
|
}
|
|
334
557
|
|
|
335
558
|
const spec = `${name}@${version}`;
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
const skipReleaseAge = force || auto;
|
|
559
|
+
// 一律带上:自动更新是「一检测到就装」,手动点的那一下同样是「现在就要」,没理由先让这道
|
|
560
|
+
// 策略挡一次、再靠失败重试绕过来。命令行与环境变量各带一份,pnpm 12 只认后者(见 POLICY_ENV)。
|
|
561
|
+
const policyArgs = ['--config.auto-install-peers=false', '--config.minimum-release-age=0'];
|
|
340
562
|
const args = picked.manager === 'pnpm'
|
|
341
|
-
? ['add', spec,
|
|
563
|
+
? ['add', spec, ...policyArgs]
|
|
342
564
|
: ['install', spec, '--no-audit', '--no-fund'];
|
|
343
|
-
|
|
565
|
+
const command = [picked.manager, ...args].join(' ');
|
|
344
566
|
onEvent({ phase: 'install-started', command, manager: picked.kind });
|
|
345
567
|
|
|
346
|
-
const
|
|
568
|
+
const runManager = (extraArgs, baseArgs = args) => run(picked.command, [...picked.prefix, ...baseArgs, ...extraArgs], {
|
|
347
569
|
cwd: profileDir,
|
|
348
|
-
env: { ...childEnv, ...picked.env },
|
|
570
|
+
env: { ...childEnv, ...picked.env, ...(picked.manager === 'pnpm' ? POLICY_ENV : {}) },
|
|
349
571
|
timeoutMs,
|
|
350
572
|
});
|
|
351
|
-
|
|
573
|
+
|
|
574
|
+
let result = await runManager([]);
|
|
352
575
|
let output = result.output;
|
|
353
576
|
/**
|
|
354
|
-
*
|
|
577
|
+
* 宿主环境对不上:不是这个包有问题,是这台机器的依赖树跟当前 pnpm 对不上 —— 不修的话,
|
|
578
|
+
* 之后每一次更新都会撞同一堵墙。修一次、重试一次;修不了就如实失败,下个 tick 再来。
|
|
355
579
|
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* 重试一次只会把同一条错误再说一遍,还多花一次等待。
|
|
580
|
+
* 两种现场各有各的修法,判据都在 pnpm 自己的报错里:
|
|
581
|
+
* ① 布局是两个大版本之前建的(PUBLIC_HOIST_PATTERN_DIFF / VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF)
|
|
582
|
+
* → 用现在这版 pnpm 重建依赖树;
|
|
583
|
+
* ② 记录的 store 与它现在解析的不是同一个(UNEXPECTED_STORE)→ 把**记录里的那个**写回
|
|
584
|
+
* profile 的 storeDir。只取报错点名、且确实存在于盘上的那个路径;记录本身就是旧盘 /
|
|
585
|
+
* 不存在的路径时不动 —— 那是另一种坏法,写回去只会换一种失败。
|
|
363
586
|
*/
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
587
|
+
let healed = null;
|
|
588
|
+
if (!result.ok && picked.manager === 'pnpm') {
|
|
589
|
+
if (HOIST_DIFF.test(output)) {
|
|
590
|
+
const rebuild = await runManager([], ['install', '--no-frozen-lockfile', ...policyArgs]);
|
|
591
|
+
if (rebuild.ok) {
|
|
592
|
+
healed = 'rebuild';
|
|
593
|
+
result = await runManager([]);
|
|
594
|
+
output = `${output}\n--- node_modules 是旧版 pnpm 建的,已重建依赖树并重试 ---\n${result.output}`;
|
|
595
|
+
} else {
|
|
596
|
+
output = `${output}\n--- 试着重建依赖树,但也没成:${rebuild.output} ---`;
|
|
597
|
+
}
|
|
598
|
+
} else if (/ERR_PNPM_UNEXPECTED_STORE/.test(output)) {
|
|
599
|
+
const linked = /currently linked from the store at "([^"]+)"/.exec(output)?.[1];
|
|
600
|
+
const modules = /The dependencies at "([^"]+)"/.exec(output)?.[1];
|
|
601
|
+
if (linked && modules && isInsideDir(profileDir, modules) && fs.existsSync(linked)
|
|
602
|
+
&& ensureProfileStoreDir(profileDir, linked)) {
|
|
603
|
+
healed = 'store';
|
|
604
|
+
result = await runManager([]);
|
|
605
|
+
output = `${output}\n--- pnpm 记录的 store 与本机解析的不一致,已把记录里的那个写回 storeDir 并重试 ---\n${result.output}`;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
371
608
|
}
|
|
372
609
|
const after = installedVersion(profileNodeModules, name);
|
|
373
610
|
if (!result.ok) {
|
|
@@ -379,7 +616,8 @@ async function installLatest({
|
|
|
379
616
|
source: declaredSource(profileDir, name),
|
|
380
617
|
command,
|
|
381
618
|
manager: picked.kind,
|
|
382
|
-
|
|
619
|
+
healed,
|
|
620
|
+
releaseAgeRecorded: false,
|
|
383
621
|
output,
|
|
384
622
|
// 超时与「真的失败了」分开说:前者可以再试,后者多半要看输出。
|
|
385
623
|
failure: {
|
|
@@ -390,6 +628,8 @@ async function installLatest({
|
|
|
390
628
|
}
|
|
391
629
|
// 退出码 0 不算数:**盘上版本真的变了**才算装上(市场那份代码吃过这个亏)。
|
|
392
630
|
const changed = after !== null && after !== beforeVersion;
|
|
631
|
+
// 这一版是我们绕开晾置期装进去的,登记进排除清单,免得它反过来把别的安装挡 24 小时。
|
|
632
|
+
const releaseAgeRecorded = changed ? recordReleaseAgeExclusion(profileDir, name, after) : false;
|
|
393
633
|
return {
|
|
394
634
|
installed: changed,
|
|
395
635
|
skipped: false,
|
|
@@ -398,7 +638,8 @@ async function installLatest({
|
|
|
398
638
|
source: declaredSource(profileDir, name),
|
|
399
639
|
command,
|
|
400
640
|
manager: picked.kind,
|
|
401
|
-
|
|
641
|
+
healed,
|
|
642
|
+
releaseAgeRecorded,
|
|
402
643
|
output,
|
|
403
644
|
failure: changed
|
|
404
645
|
? null
|
|
@@ -427,9 +668,12 @@ async function installLatest({
|
|
|
427
668
|
* 把它搬到插件自己这条路上来。判到有人跑就**不动盘**,如实报成「可再试」,下个 tick 再来。
|
|
428
669
|
*
|
|
429
670
|
* `force` 是**用户明说「我知道,装吧」**的那一挡(面板上那颗「强制更新」):它同时跳过
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
671
|
+
* 这道闸,而且不管有没有新版都装一遍。`auto`(宿主那一轮自动更新)跳过同一道闸,只是仍然
|
|
672
|
+
* 「没新版就不动盘」。发布晾置期**不再是一个「哪一档才跳过」的问题**:装的时候一律跳过,
|
|
673
|
+
* 装完把这一版登记进排除清单(见文件头),所以手动、强制、自动三条路的安装条件是一致的。
|
|
674
|
+
*
|
|
675
|
+
* 「查」用的是这台机器上 pnpm / npm 真正在用的那个注册表(`profileRegistry`),不是写死的
|
|
676
|
+
* 官方源:镜像是通的、官方源连不上的机器,也要能自己发现新版。
|
|
433
677
|
*/
|
|
434
678
|
async function installLatestIfNeeded(options = {}) {
|
|
435
679
|
const { profileNodeModules, force = false, auto = false, name = DEFAULT_NAME, agents = null } = options;
|
|
@@ -451,7 +695,17 @@ async function installLatestIfNeeded(options = {}) {
|
|
|
451
695
|
check: null,
|
|
452
696
|
};
|
|
453
697
|
}
|
|
454
|
-
|
|
698
|
+
// 查与装用同一个源:问一次这台机器上 pnpm / npm 真正在用的注册表(问不到才退回官方源)。
|
|
699
|
+
// 不这样做的话,镜像 / 私有源机器上会出现「检查更新说连不上、手动装却是通的」。
|
|
700
|
+
const registry = await profileRegistry({
|
|
701
|
+
profileNodeModules,
|
|
702
|
+
packageManager: options.packageManager ?? null,
|
|
703
|
+
env: options.env,
|
|
704
|
+
platform: options.platform,
|
|
705
|
+
dirs: options.dirs ?? null,
|
|
706
|
+
cache: options.registryCache,
|
|
707
|
+
});
|
|
708
|
+
const check = await checkForUpdate({ installedVersion: beforeVersion, source, base: registry ?? undefined });
|
|
455
709
|
// 「有没有新版」排在「有没有人在跑」**前面**:没东西可装的时候,拿一句「有对话在跑」挡住
|
|
456
710
|
// 真正的答案(已经是最新)是误导。
|
|
457
711
|
if (!force && !check.updateAvailable) {
|
|
@@ -504,6 +758,10 @@ module.exports = {
|
|
|
504
758
|
resolveExecutable,
|
|
505
759
|
normalizeInvocation,
|
|
506
760
|
runningAgentIds,
|
|
761
|
+
profileRegistry,
|
|
762
|
+
recordReleaseAgeExclusion,
|
|
763
|
+
ensureProfileStoreDir,
|
|
764
|
+
parseRegistryOutput,
|
|
507
765
|
DEFAULT_NAME,
|
|
508
766
|
INSTALL_TIMEOUT_MS,
|
|
509
767
|
OUTPUT_TAIL,
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (dsh 0.2.x, peers resolved by the host)",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"icon": "./icon.png",
|
|
6
7
|
"engines": {
|
|
7
8
|
"dsh": ">=0.2.0 <0.3.0"
|
|
8
9
|
},
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"exports": {
|
|
15
16
|
".": "./lib/index.js",
|
|
16
17
|
"./client": "./lib/client.js",
|
|
18
|
+
"./locale/*.json": "./locale/*.json",
|
|
17
19
|
"./package.json": "./package.json"
|
|
18
20
|
},
|
|
19
21
|
"repository": {
|
|
@@ -37,6 +39,8 @@
|
|
|
37
39
|
"lib/**/*.html",
|
|
38
40
|
"lib/**/package.json",
|
|
39
41
|
"dsh-mobile-credentials",
|
|
42
|
+
"locale/*.json",
|
|
43
|
+
"icon.png",
|
|
40
44
|
"README.md"
|
|
41
45
|
],
|
|
42
46
|
"publishConfig": {
|