@xiaoyuyu6420/dsh-backup 0.12.2 → 0.13.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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +21 -21
- package/lib/index.js +157 -2
- package/package.json +8 -8
package/lib/index.js
CHANGED
|
@@ -272,6 +272,16 @@ function detectHostTrain() {
|
|
|
272
272
|
* 吃的都是同一语法的字符串,直接写常量在两个列车上行为等价。 */
|
|
273
273
|
const NS = 'dsh-backup';
|
|
274
274
|
|
|
275
|
+
/** 本插件自身名称与版本(package.json):更新检查的"当前版本"基准。 */
|
|
276
|
+
const SELF_META = (() => {
|
|
277
|
+
try {
|
|
278
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
279
|
+
return { name: pkg.name, version: pkg.version };
|
|
280
|
+
} catch {
|
|
281
|
+
return { name: '@xiaoyuyu6420/dsh-backup', version: 'unknown' };
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
284
|
+
|
|
275
285
|
/**
|
|
276
286
|
* 设置默认值。SCHEMA 的 .default() 与 resolveBase 的初始值都引用这里,
|
|
277
287
|
* 避免同一字面量散落多处导致迁移逻辑漏改。
|
|
@@ -281,6 +291,7 @@ const DEFAULTS = Object.freeze({
|
|
|
281
291
|
exclude: [],
|
|
282
292
|
redact: [...SENSITIVE_DEFAULTS],
|
|
283
293
|
githubRepo: '',
|
|
294
|
+
updateCheck: false,
|
|
284
295
|
});
|
|
285
296
|
|
|
286
297
|
/**
|
|
@@ -294,9 +305,10 @@ const SCHEMA = z.object({
|
|
|
294
305
|
exclude: z.array(z.string()).default(DEFAULTS.exclude),
|
|
295
306
|
redact: z.union([z.array(z.string()), 'off']).default(DEFAULTS.redact),
|
|
296
307
|
githubRepo: z.string().default(DEFAULTS.githubRepo),
|
|
308
|
+
updateCheck: z.boolean().default(DEFAULTS.updateCheck),
|
|
297
309
|
});
|
|
298
310
|
|
|
299
|
-
const SETTINGS_FIELDS = ['destination', 'keep', 'exclude', 'redact', 'githubRepo'];
|
|
311
|
+
const SETTINGS_FIELDS = ['destination', 'keep', 'exclude', 'redact', 'githubRepo', 'updateCheck'];
|
|
300
312
|
|
|
301
313
|
/** 校验并归一化单个字段到 `out`;非法值忽略。 */
|
|
302
314
|
function normalizeField(out, field, value) {
|
|
@@ -313,6 +325,8 @@ function normalizeField(out, field, value) {
|
|
|
313
325
|
else if (Array.isArray(value)) out.redact = value.filter((v) => typeof v === 'string');
|
|
314
326
|
} else if (field === 'githubRepo') {
|
|
315
327
|
if (typeof value === 'string') out.githubRepo = value;
|
|
328
|
+
} else if (field === 'updateCheck') {
|
|
329
|
+
if (typeof value === 'boolean') out.updateCheck = value;
|
|
316
330
|
}
|
|
317
331
|
}
|
|
318
332
|
|
|
@@ -398,6 +412,8 @@ const PANEL_INVOCATIONS = Object.freeze([
|
|
|
398
412
|
panelDescriptor('doctorRepair', ['selector'], true),
|
|
399
413
|
panelDescriptor('migrateCheck', [], true),
|
|
400
414
|
panelDescriptor('setGithubToken', ['token']),
|
|
415
|
+
panelDescriptor('checkUpdate', [], true),
|
|
416
|
+
panelDescriptor('update', [], true),
|
|
401
417
|
]);
|
|
402
418
|
|
|
403
419
|
/**
|
|
@@ -481,6 +497,16 @@ class BackupPanelService extends TypertRemoteService {
|
|
|
481
497
|
setGithubToken(token) {
|
|
482
498
|
return this.ops.setGithubToken(token);
|
|
483
499
|
}
|
|
500
|
+
|
|
501
|
+
/** 检查 npm 是否有新版本(只读,不写任何文件)。 */
|
|
502
|
+
checkUpdate(signal) {
|
|
503
|
+
return this.ops.checkUpdate(signal);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** 一键更新:先留升级前快照,再走官方 dsh plugin update(完成后需重启生效)。 */
|
|
507
|
+
update(signal) {
|
|
508
|
+
return this.ops.update(signal);
|
|
509
|
+
}
|
|
484
510
|
}
|
|
485
511
|
|
|
486
512
|
export function apply(ctx, pluginConfig) {
|
|
@@ -652,6 +678,100 @@ export function apply(ctx, pluginConfig) {
|
|
|
652
678
|
// ('D:' + '/' + name 仍是合法路径,renameBeside 等不受影响)。
|
|
653
679
|
const parentTar = (p) => (p.endsWith('/') ? p : `${p}/`);
|
|
654
680
|
|
|
681
|
+
/** semver 比较(常见形态,含 prerelease):a > b 才返回 true;解析失败保守降为 false。 */
|
|
682
|
+
function semverGt(a, b) {
|
|
683
|
+
const parse = (s) => {
|
|
684
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(String(s));
|
|
685
|
+
if (!m) return null;
|
|
686
|
+
return { n: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
687
|
+
};
|
|
688
|
+
const A = parse(a);
|
|
689
|
+
const B = parse(b);
|
|
690
|
+
if (!A || !B) return false;
|
|
691
|
+
for (let i = 0; i < 3; i += 1) {
|
|
692
|
+
if (A.n[i] !== B.n[i]) return A.n[i] > B.n[i];
|
|
693
|
+
}
|
|
694
|
+
if (A.pre === B.pre) return false;
|
|
695
|
+
return A.pre === null; // 无 prerelease 视为 release:同元组下 release > prerelease
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 检查 npm 最新版(更新感知,A)。纯只读,失败静默——返回 error 文案交给
|
|
700
|
+
* 调用方,插件正常运行绝不因网络问题阻断。fetch 为 Node >=20 全局。
|
|
701
|
+
*/
|
|
702
|
+
async function checkUpdate(signal) {
|
|
703
|
+
const ctrl = new AbortController();
|
|
704
|
+
const timer = setTimeout(() => ctrl.abort(), 8000);
|
|
705
|
+
const onHostAbort = () => ctrl.abort();
|
|
706
|
+
signal?.addEventListener('abort', onHostAbort, { once: true });
|
|
707
|
+
try {
|
|
708
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(SELF_META.name)}`, {
|
|
709
|
+
signal: ctrl.signal,
|
|
710
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
711
|
+
});
|
|
712
|
+
if (!res.ok) return { error: `npm registry 返回 ${res.status}` };
|
|
713
|
+
const meta = await res.json();
|
|
714
|
+
const latest = meta?.['dist-tags']?.latest;
|
|
715
|
+
if (typeof latest !== 'string' || !latest) return { error: 'registry 响应缺少 dist-tags.latest' };
|
|
716
|
+
return { current: SELF_META.version, latest, update: semverGt(latest, SELF_META.version) };
|
|
717
|
+
} catch (err) {
|
|
718
|
+
if (err && err.name === 'AbortError') return { error: '检查超时(8 秒)' };
|
|
719
|
+
return { error: String(err && err.message ? err.message : err) };
|
|
720
|
+
} finally {
|
|
721
|
+
clearTimeout(timer);
|
|
722
|
+
signal?.removeEventListener('abort', onHostAbort);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 一键更新(B):先拍升级前快照(未过期的退路),再走官方 `dsh plugin
|
|
728
|
+
* --profile <p> update <pkg>`(pnpm 语义——profile 是 pnpm 项目,插件绝不
|
|
729
|
+
* 自己覆写依赖文件,否则 lockfile 失配)。完成后提示重启生效:宿主在启动
|
|
730
|
+
* 时结算 Loader 树,Node 半不可在运行中热替换。
|
|
731
|
+
*/
|
|
732
|
+
async function runUpdate(signal, profileArg) {
|
|
733
|
+
const profile = profileArg || 'web';
|
|
734
|
+
const chk = await checkUpdate(signal);
|
|
735
|
+
if (chk.error) {
|
|
736
|
+
return { ok: false, updated: false, summary: `检查更新失败:${chk.error}\n可手动执行: dsh plugin --profile ${profile} update ${SELF_META.name}` };
|
|
737
|
+
}
|
|
738
|
+
if (!chk.update) {
|
|
739
|
+
return { ok: true, updated: false, current: chk.current, latest: chk.latest, summary: `已是最新版本 ${chk.current}` };
|
|
740
|
+
}
|
|
741
|
+
// 更新前快照:与启动钩子的升级前快照同一语义(不阻断更新,失败如实提示)
|
|
742
|
+
let snap = null;
|
|
743
|
+
try {
|
|
744
|
+
const s = await doBackup(undefined, signal, { namePrefix: 'dsh-pre-upgrade-' });
|
|
745
|
+
prunePrefixedSnapshots('dsh-pre-upgrade-', 2, s.name).catch(() => {});
|
|
746
|
+
snap = s.name;
|
|
747
|
+
} catch { /* 快照失败不阻断更新 */ }
|
|
748
|
+
// 解析 dsh CLI:优先 PATH(resolveExecutable),兜底用当前宿主入口(与
|
|
749
|
+
// detectHostTrain 同款定位——`node <入口> plugin ...` 与运行中的宿主同源)
|
|
750
|
+
let argv = null;
|
|
751
|
+
try {
|
|
752
|
+
argv = [await ctx.subprocess.resolveExecutable('dsh'), 'plugin', '--profile', profile, 'update', SELF_META.name];
|
|
753
|
+
} catch {
|
|
754
|
+
try {
|
|
755
|
+
argv = [process.execPath, realpathSync(process.argv[1]), 'plugin', '--profile', profile, 'update', SELF_META.name];
|
|
756
|
+
} catch {
|
|
757
|
+
return { ok: false, updated: false, summary: `找不到 dsh 命令,请手动执行: dsh plugin --profile ${profile} update ${SELF_META.name}${snap ? `\n更新前快照已留: ${snap}` : ''}` };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
const { root } = paths();
|
|
762
|
+
await spawnRun(argv, root, signal);
|
|
763
|
+
} catch (err) {
|
|
764
|
+
return { ok: false, updated: false, summary: `更新失败:${friendlyReason(err)}${snap ? `\n已有升级前快照 ${snap},可 /backup restore ${snap} 回滚。` : ''}` };
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
ok: true,
|
|
768
|
+
updated: true,
|
|
769
|
+
current: chk.latest,
|
|
770
|
+
latest: chk.latest,
|
|
771
|
+
summary: `已更新到 ${chk.latest}。${snap ? `\n更新前已留升级前快照 ${snap}(想回退:/backup restore ${snap})。` : '\n更新前快照未生成(不影响本次更新)。'}\n请重启 dsh web 使新版本生效。`,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
655
775
|
function defaultKeep() {
|
|
656
776
|
const cfg = resolvedConfig();
|
|
657
777
|
const k = Number(cfg.keep);
|
|
@@ -2616,7 +2736,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
2616
2736
|
// ---------- /backup 命令 ----------
|
|
2617
2737
|
ctx.commands.register({
|
|
2618
2738
|
name: 'backup',
|
|
2619
|
-
description: '备份/恢复 DSH 数据;子命令: list | verify [前缀|all] | restore <前缀|latest> [--dry-run] [--sync-deps] | doctor [--repair <前缀|latest>] | migrate-check | auto [N小时|off] | github status|sync|pull|repo|token <token|off> | [--keep N]',
|
|
2739
|
+
description: '备份/恢复 DSH 数据;子命令: list | verify [前缀|all] | restore <前缀|latest> [--dry-run] [--sync-deps] | doctor [--repair <前缀|latest>] | migrate-check | auto [N小时|off] | github status|sync|pull|repo|token <token|off> | check-update | update [--profile <名>] | [--keep N]',
|
|
2620
2740
|
handler: async (invocation) => {
|
|
2621
2741
|
const input = invocation.rawInput.trim();
|
|
2622
2742
|
try {
|
|
@@ -2680,6 +2800,25 @@ export function apply(ctx, pluginConfig) {
|
|
|
2680
2800
|
return { kind: r.failCount || !r.env.hardlink || r.env.credentialsFlat ? 'error' : 'success', text: r.summary };
|
|
2681
2801
|
}
|
|
2682
2802
|
|
|
2803
|
+
if (head === 'check-update' || head === 'update') {
|
|
2804
|
+
const profileIdx = parts.indexOf('--profile');
|
|
2805
|
+
const profile = profileIdx >= 0 && parts[profileIdx + 1] && !parts[profileIdx + 1].startsWith('--')
|
|
2806
|
+
? parts[profileIdx + 1]
|
|
2807
|
+
: 'web';
|
|
2808
|
+
if (head === 'check-update') {
|
|
2809
|
+
const u = await checkUpdate(invocation.signal);
|
|
2810
|
+
if (u.error) return { kind: 'error', text: `检查更新失败:${u.error}` };
|
|
2811
|
+
return {
|
|
2812
|
+
kind: 'success',
|
|
2813
|
+
text: u.update
|
|
2814
|
+
? `发现新版本:${u.current} → ${u.latest}\n输入 /backup update 一键更新(更新前自动留升级前快照)`
|
|
2815
|
+
: `已是最新版本:${u.current}`,
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
const r = await runUpdate(invocation.signal, profile);
|
|
2819
|
+
return { kind: r.ok ? 'success' : 'error', text: r.summary };
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2683
2822
|
if (head === 'github') {
|
|
2684
2823
|
const arg = parts[1] || 'status';
|
|
2685
2824
|
if (arg === 'repo') {
|
|
@@ -3109,6 +3248,22 @@ export function apply(ctx, pluginConfig) {
|
|
|
3109
3248
|
await writeStoredGithubToken(raw);
|
|
3110
3249
|
return { ok: true, tokenSet: true, summary: 'GitHub token 已保存:存在本机备份目录(0600),不进归档、不进 GitHub 同步。' };
|
|
3111
3250
|
},
|
|
3251
|
+
checkUpdate: async (signal) => {
|
|
3252
|
+
const u = await checkUpdate(signal);
|
|
3253
|
+
if (u.error) return { ok: false, current: SELF_META.version, latest: null, update: false, error: u.error, summary: `检查更新失败:${u.error}` };
|
|
3254
|
+
return {
|
|
3255
|
+
ok: true,
|
|
3256
|
+
current: u.current,
|
|
3257
|
+
latest: u.latest,
|
|
3258
|
+
update: u.update,
|
|
3259
|
+
error: null,
|
|
3260
|
+
summary: u.update ? `发现新版本:${u.current} → ${u.latest}` : `已是最新版本:${u.current}`,
|
|
3261
|
+
};
|
|
3262
|
+
},
|
|
3263
|
+
update: async (signal) => {
|
|
3264
|
+
const r = await runUpdate(signal, 'web');
|
|
3265
|
+
return { ok: r.ok, updated: r.updated, current: r.current ?? null, latest: r.latest ?? null, summary: r.summary };
|
|
3266
|
+
},
|
|
3112
3267
|
};
|
|
3113
3268
|
|
|
3114
3269
|
// 仅在装配了 Typert registry 的 profile(Web)里挂载面板服务;其余 profile 安静跳过。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "一键备份与恢复 DeepSeek Harness(dsh)的全部用户数据(~/.dsh):定时自动备份、升级快照、会话日志体检修复、迁移预检、宿主起不来也能用的救援通道、凭据脱敏、GitHub 同步跨机迁移,附可视化面板。 One command to back up & restore all of ~/.dsh: /backup, scheduled auto-backup, upgrade snapshots, session doctor & repair, migrate precheck, out-of-process rescue console, credential redaction, GitHub sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.24.0",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
],
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
-
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
57
|
-
"@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
58
|
-
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
59
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
60
|
-
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
61
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
59
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
60
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
61
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
62
62
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": ">=20",
|
|
72
|
-
"dsh": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1"
|
|
72
|
+
"dsh": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1"
|
|
73
73
|
},
|
|
74
74
|
"license": "MIT",
|
|
75
75
|
"repository": {
|