dsh-selfupdater 0.4.17 → 0.4.19
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/index.js +15 -4
- package/lib/updater.mjs +34 -14
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - 升级动作通过锁文件防并发,双端校验。
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { fetchLatestVersion, installedSelfVersion, isNewer, runPluginUpdateTask } from './plugin-update-task.mjs';
|
|
@@ -286,11 +286,22 @@ export function apply(ctx) {
|
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
// 并发防护:锁文件已存在说明有升级在跑(或上次异常残留未清理)。
|
|
289
|
+
// 残留判定:锁内容损坏,或 startedAt 超过 15 分钟(升级脚本总看门狗
|
|
290
|
+
// 仅 8 分钟,超过必然已死)—— 自动清理放行,避免强杀后永久 409。
|
|
289
291
|
if (isBusy()) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
+
let stale = false;
|
|
293
|
+
try {
|
|
294
|
+
const prev = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
295
|
+
stale = Date.now() - Number(prev.startedAt ?? 0) > 15 * 60 * 1000;
|
|
296
|
+
} catch { stale = true; }
|
|
297
|
+
if (!stale) {
|
|
298
|
+
sendJson(res, 409, { error: '已有一次升级在进行中' });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try { rmSync(lockFile, { force: true }); } catch { /* 清不掉则下次再试 */ }
|
|
292
302
|
}
|
|
293
|
-
//
|
|
303
|
+
// 预置锁文件:作为并发防护;updater.mjs 见到 by=plugin 的新鲜锁会
|
|
304
|
+
// 视为合法握手接管(见 updater.mjs main 的锁文件握手注释)。
|
|
294
305
|
// 先确保 .dsh 目录存在(首次安装/手动清理后可能尚无该目录,教训来自插件写状态 ENOENT 问题)。
|
|
295
306
|
try {
|
|
296
307
|
mkdirSync(dshStateDir, { recursive: true });
|
package/lib/updater.mjs
CHANGED
|
@@ -280,7 +280,8 @@ async function fetchLatestRelease(pkg) {
|
|
|
280
280
|
const errors = [];
|
|
281
281
|
results.forEach((r, i) => {
|
|
282
282
|
if (r.status === 'fulfilled') {
|
|
283
|
-
|
|
283
|
+
// 记录胜出版本来自哪个 registry,供 staging 安装用 --registry 同源下载
|
|
284
|
+
if (best === null || isNewer(r.value.version, best.version)) best = { ...r.value, base: targets[i].base };
|
|
284
285
|
} else {
|
|
285
286
|
errors.push(`${targets[i].base}/${targets[i].tag}: ${r.reason.message}`);
|
|
286
287
|
}
|
|
@@ -289,29 +290,36 @@ async function fetchLatestRelease(pkg) {
|
|
|
289
290
|
return best;
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
/** 仅取最新版本号(主流程使用)。 */
|
|
293
|
-
async function fetchLatestVersion(pkg) {
|
|
294
|
-
return (await fetchLatestRelease(pkg)).version;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
293
|
/**
|
|
298
294
|
* 阶段一:下载。把新版安装进独立 staging 目录。
|
|
299
295
|
* 关键点:pnpm 默认用符号链接布局,搬走 node_modules 后链接会全部失效;
|
|
300
296
|
* 通过 node-linker=hoisted 强制实体文件布局,保证 staging/node_modules
|
|
301
297
|
* 可以整体 rename 到 APP_DIR。
|
|
298
|
+
* @param target - 目标版本号(精确锁定)
|
|
299
|
+
* @param registryBase - 版本查询胜出的 registry 源,安装与查询保持同源
|
|
302
300
|
*/
|
|
303
|
-
async function downloadIntoStaging(target) {
|
|
301
|
+
async function downloadIntoStaging(target, registryBase) {
|
|
304
302
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
305
303
|
mkdirSync(stagingDir, { recursive: true });
|
|
304
|
+
// 精确锁定版本:alpha 渠道发版快,用 ^ 范围可能解析到比 target 更新的
|
|
305
|
+
// 版本,撞上"staged 必须严格等于 target"的校验而误报失败。
|
|
306
306
|
writeFileSync(
|
|
307
307
|
join(stagingDir, 'package.json'),
|
|
308
|
-
JSON.stringify({ name: 'dsh-selfupdate-staging', private: true, dependencies: { [PKG]:
|
|
308
|
+
JSON.stringify({ name: 'dsh-selfupdate-staging', private: true, dependencies: { [PKG]: target } }, null, 2),
|
|
309
309
|
);
|
|
310
310
|
// 实体文件布局 + 关闭交互确认,保证无人值守可执行。
|
|
311
311
|
writeFileSync(join(stagingDir, '.npmrc'), 'node-linker=hoisted\n');
|
|
312
312
|
const pnpm = pnpmCommand();
|
|
313
313
|
setState('downloading', `正在下载 ${PKG}@${target} …`);
|
|
314
|
-
|
|
314
|
+
// pnpm 不认 npm 风格的 --omit/--no-audit/--no-fund(0.4.18 实测踩坑):
|
|
315
|
+
// 排除 dev 依赖用 --prod;--registry 让安装与版本查询走同一个源,
|
|
316
|
+
// 避免"官方源不通、镜像查到版本却装不下来"的割裂。
|
|
317
|
+
const result = await runCommand(pnpm.file, [
|
|
318
|
+
...pnpm.args,
|
|
319
|
+
'install',
|
|
320
|
+
'--prod',
|
|
321
|
+
...(registryBase ? ['--registry', registryBase] : []),
|
|
322
|
+
], {
|
|
315
323
|
cwd: stagingDir,
|
|
316
324
|
env: { ...process.env, CI: 'true', npm_config_node_linker: 'hoisted' },
|
|
317
325
|
});
|
|
@@ -403,14 +411,26 @@ async function main() {
|
|
|
403
411
|
mkdirSync(dshStateDir, { recursive: true });
|
|
404
412
|
const current = currentDshVersion();
|
|
405
413
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
414
|
+
// 锁文件握手:插件本体 POST /perform 会预写锁文件(by=plugin)作为并发
|
|
415
|
+
// 防护,本脚本见到它应视为合法交接直接接管 —— 旧实现"存在即拒绝"与
|
|
416
|
+
// 插件端的预写约定自相矛盾,导致升级必然秒败(0.4.17 实测踩坑)。
|
|
417
|
+
// 仅当锁是陈旧残留(超过 10 分钟)或来源不明时才判定为重复执行。
|
|
418
|
+
if (existsSync(lockFile)) {
|
|
419
|
+
let handover = false;
|
|
420
|
+
try {
|
|
421
|
+
const prev = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
422
|
+
handover = prev?.by === 'plugin' && Date.now() - Number(prev.startedAt ?? 0) < 10 * 60 * 1000;
|
|
423
|
+
} catch { handover = false; }
|
|
424
|
+
if (!handover) throw new Error('已有一次升级在进行中(锁文件存在)');
|
|
425
|
+
}
|
|
426
|
+
writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: Date.now(), by: 'updater' }));
|
|
409
427
|
|
|
410
428
|
status = { currentVersion: current, startedAt: new Date().toISOString(), trigger: 'manual' };
|
|
411
429
|
setState('downloading', '正在查询 npm 最新版本 …');
|
|
412
430
|
|
|
413
|
-
|
|
431
|
+
// fetchLatestRelease 除版本号外还带回胜出的 registry 源,供 staging 同源安装
|
|
432
|
+
const release = await fetchLatestRelease(PKG);
|
|
433
|
+
const target = release.version;
|
|
414
434
|
status.latestVersion = target;
|
|
415
435
|
if (!isNewer(target, current)) {
|
|
416
436
|
setState('idle', `当前已是最新版本(${current})`);
|
|
@@ -418,7 +438,7 @@ async function main() {
|
|
|
418
438
|
}
|
|
419
439
|
|
|
420
440
|
log(`发现新版本:${current} -> ${target}`);
|
|
421
|
-
await downloadIntoStaging(target);
|
|
441
|
+
await downloadIntoStaging(target, release.base);
|
|
422
442
|
swapNodeModules();
|
|
423
443
|
|
|
424
444
|
setState('restarting', '正在重启 DeepSeek Harness …');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfupdater",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"description": "Self-update plugin for DeepSeek Harness: DSH core upgrades via detached swap script; plugin self-update installs in-place without killing the host and prompts for restart. DSH 主程序与已装插件的一站式在线更新插件。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|