@xiaoyuyu6420/dsh-backup 0.12.1 → 0.12.2
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 +1 -0
- package/README.md +1 -0
- package/lib/client.js +1 -1
- package/lib/index.js +21 -5
- package/package.json +1 -1
- package/rescue/rescue.mjs +13 -3
package/README.en.md
CHANGED
|
@@ -137,6 +137,7 @@ Tried it? Tell us what broke, what's missing, what you liked — it directly sha
|
|
|
137
137
|
<details>
|
|
138
138
|
<summary>Recent releases</summary>
|
|
139
139
|
|
|
140
|
+
- **0.12.2** — Fix a Windows data-safety hazard (#88, thanks @Liuyeyuyangy0 for the outstanding report): when `DSH_HOME` sits directly under a drive root (e.g. `D:\dsh_data`), the string-slice parent computation degrades to the bare drive letter `D:`, and `tar -C D:` fails on both bsdtar and GNU tar — worse, tar creates the `-f` target before writing, leaving a 29-byte empty shell that the panel lists as a normal backup (no `.sha256`/`.meta.json`; silently zero usable backups, and the restore / doctor repair / rescue channels are hit by the same bug). All 7 tar `-C` call sites (backup, typed & full restore, doctor point-repair, rescue snapshot/restore/repair) now receive a separator-completed parent; `paths()` normalizes trailing slashes off `DSH_HOME` like `HOME` (a trailing slash used to empty the tar positional and produce 0-byte archives); failed tar runs now delete the empty shell before rethrowing, so failures are visible. Smoke gains scenario 28 (four path-layout assertions + a trailing-slash real-backup e2e). Archive format, settings and RPC unchanged.
|
|
140
141
|
- **0.12.1** — Fix client bundle leaking top-level `var module` / `var exports` as globals (#85, thanks @geyonder): the CJS shim now lives inside the `__ModuleLoader__.load` factory body instead of creating `window.module`, which made Monaco's AMD loader (dsh-vscode-mode and friends) misdetect a Node environment and break sibling plugins on the same page. Client smoke suite gains a classic-script global-leak regression assertion (`vm.runInContext`). Client-half only; node-side behavior and archive format unchanged.
|
|
141
142
|
- **0.12.0** — Migration safety net, round one, aimed at the community-wide "old sessions won't open after upgrade" wave (#6151/#6297/#6355): 1) `/backup migrate-check` statically scans all session logs (v0/v1/v2/v3) before you upgrade and predicts which sessions will refuse to open and under which rule (descriptor v2, permission/preset origin, event types outside the frozen lists, custom source kinds, seq drift, filename/generation mismatch — rules baked from host 0.1.5-rc.2, with the coverage boundary stated in every report), plus a filesystem hardlink probe (the exFAT publish-failure scenario from #6358); 2) a credential sentinel — the legacy flat-layout `.credentials.yaml` gets atomically replaced by the host with an irreversible versioned form, so on detection it is preserved into the vault before the host touches it. 3) configure the GitHub sync token right in the Settings panel (or `/backup github token <token>`) — stored locally as `github.token` (0600) in the backup directory, never archived or synced, taking precedence over environment variables; re-enter after cross-machine restore. Also: `engines.dsh` declaration (read by plugin-market compatibility cards).
|
|
142
143
|
- **0.11.3** — Follow the dsh `0.1.5` train: peerDependencies now include `^0.1.5-rc.1` (0.11.2 raised peer warnings on 0.1.5 hosts). Verified against host `0.1.5-rc.2`: real-host e2e 32/32; all six node-side peer packages are **byte-identical** between rc.1 and rc.2 (tarball diff) and the client train hasn't moved — zero adaptation surface this time; a cross-train in-place upgrade e2e (rc.1 host + 0.11.2 → rc.2 host + 0.11.3) passed 14/14 with settings and archives intact. Also fixed a latent bug in the upgrade e2e itself: the expected new version is now derived from the tarball instead of hardcoded.
|
package/README.md
CHANGED
|
@@ -137,6 +137,7 @@ dsh plugin --profile web add github:xiaoyuyu6420/dsh-backup
|
|
|
137
137
|
<details>
|
|
138
138
|
<summary>最近的版本</summary>
|
|
139
139
|
|
|
140
|
+
- **0.12.2** —— 修复 Windows 数据安全隐患(#88,感谢 @Liuyeyuyangy0 出色的报告):`DSH_HOME` 位于盘符根目录(如 `D:\dsh_data`)时,求父目录的字符串截取退化成裸盘符 `D:`,`tar -C D:` 在 bsdtar/GNU tar 下必然失败——而 tar 失败前已创建归档文件,留下 29 字节空壳被面板当正常备份列出(无 `.sha256`/`.meta.json`,静默零可用备份;恢复、doctor 修复、rescue 救援通道同样中招)。备份/恢复/修复/救援共 7 处 tar `-C` 统一改传补过分隔符的父目录;`paths()` 对 `DSH_HOME` 补上与 HOME 一致的尾斜杠归一化(尾斜杠曾让 tar 位置参数变空串、产出 0 字节坏包);tar 失败路径删除空壳归档再抛错,失败不再静默。smoke 新增场景 28(四种路径布局断言 + 尾斜杠真实备份 e2e)。归档格式、设置项、RPC 均不变。
|
|
140
141
|
- **0.12.1** —— 修复 client bundle 顶层 `var module` / `var exports` 泄漏为全局变量(#85,感谢 @geyonder 报告):CJS shim 移入 `__ModuleLoader__.load` factory 体内,不再创建 `window.module`——此前会把 Monaco 的 AMD loader(dsh-vscode-mode 等)误判成 Node 环境并破坏同页插件。冒烟测试新增经典脚本全局泄漏回归断言(`vm.runInContext`)。纯 client 半区修复,node 侧行为与归档格式不变。
|
|
141
142
|
- **0.12.0** —— 迁移安全网第一弹,对准官方社区集中爆发的「升级后老会话打不开」(#6151/#6297/#6355):① `/backup migrate-check` 迁移预检——升级前静态扫描全部代会话日志(v0/v1/v2/v3),预测哪些会话升完会打不开、挂在哪条规则(descriptor v2、permission/preset origin、冻结清单外事件类型、自定义来源 kind、seq 漂移、文件名代际不一致……规则烤入自宿主 0.1.5-rc.2,摘要如实标注覆盖边界),并探测文件系统硬链接支持(exFAT 上宿主发布会失败的 #6358 场景);② 凭据哨兵——旧扁平布局的 `.credentials.yaml` 会被宿主原子替换成 version:1(不可逆),检测到即先自动存底 vault 再让宿主动它。③ GitHub token 面板直配——设置面板(或 `/backup github token <token>`)直接粘贴保存,存本机备份目录 `github.token`(0600,不进归档、不进 GitHub 同步),优先于环境变量,跨机恢复后重填。另:新增 `engines.dsh` 声明(插件市场的兼容卡片读取它)。
|
|
142
143
|
- **0.11.3** —— 跟进 dsh `0.1.5` 列车:peerDependencies 追加 `^0.1.5-rc.1`(0.11.2 在 0.1.5 宿主上会报 peer 警告)。兼容实测:宿主 `0.1.5-rc.2` 真机 e2e 32/32;六个 node 侧 peer 包 rc.1↔rc.2 **逐字节相同**(tarball diff),client 包列车未动——本次升列车零适配面;跨列车原地升级 e2e(rc.1 宿主 + 0.11.2 → rc.2 宿主 + 0.11.3)14/14,设置与归档无损。顺带修了升级 e2e 脚本自身的一个坑:新版期望版本从写死改为从 tarball 动态推导,换版本对不再改脚本。
|
package/lib/client.js
CHANGED
|
@@ -450,5 +450,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
450
450
|
color: var(--dsw-alias-state-error-primary, #e53e3e);
|
|
451
451
|
line-height: 1.5;
|
|
452
452
|
}
|
|
453
|
-
`;var gp={name:"@xiaoyuyu6420/dsh-backup",version:"0.12.
|
|
453
|
+
`;var gp={name:"@xiaoyuyu6420/dsh-backup",version:"0.12.2",description:"\u4E00\u952E\u5907\u4EFD\u4E0E\u6062\u590D DeepSeek Harness\uFF08dsh\uFF09\u7684\u5168\u90E8\u7528\u6237\u6570\u636E\uFF08~/.dsh\uFF09\uFF1A\u5B9A\u65F6\u81EA\u52A8\u5907\u4EFD\u3001\u5347\u7EA7\u5FEB\u7167\u3001\u4F1A\u8BDD\u65E5\u5FD7\u4F53\u68C0\u4FEE\u590D\u3001\u8FC1\u79FB\u9884\u68C0\u3001\u5BBF\u4E3B\u8D77\u4E0D\u6765\u4E5F\u80FD\u7528\u7684\u6551\u63F4\u901A\u9053\u3001\u51ED\u636E\u8131\u654F\u3001GitHub \u540C\u6B65\u8DE8\u673A\u8FC1\u79FB\uFF0C\u9644\u53EF\u89C6\u5316\u9762\u677F\u3002 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.",type:"module",packageManager:"pnpm@11.24.0",main:"lib/index.js",exports:{".":"./lib/index.js","./client":"./lib/client.js","./cordis.patch.yml":"./cordis.patch.yml","./package.json":"./package.json"},files:["lib","rescue","cordis.patch.yml","README.md","README.en.md","LICENSE"],bin:{"dsh-rescue":"./rescue/rescue.mjs"},dsh:{bundle:{patch:"./cordis.patch.yml"},client:{platform:"web",inject:["@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-settings"]}},keywords:["dsh","dsh-plugin","deepseek-harness","backup","\u5907\u4EFD","restore","\u6062\u590D","schedule","\u81EA\u52A8\u5907\u4EFD","cross-platform","rescue","doctor","upgrade-snapshot","\u6570\u636E\u5B89\u5168"],peerDependencies:{"@deepseek-ai/cordis":"^4.0.1","@deepseek-ai/dsh-commands":"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1","@deepseek-ai/dsh-fs":"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1","@deepseek-ai/dsh-settings":"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1","@deepseek-ai/dsh-subprocess":"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1","@deepseek-ai/dsh-tools":"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1","@deepseek-ai/dsh-typert-protocol":"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1","@deepseek-ai/schemastery":"^3.18.1"},devDependencies:{esbuild:"^0.25.0",react:"^19.0.0","react-dom":"^19.0.0",zod:"^4.4.3"},engines:{node:">=20",dsh:"^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1"},license:"MIT",repository:{type:"git",url:"git+https://github.com/xiaoyuyu6420/dsh-backup.git"},homepage:"https://github.com/xiaoyuyu6420/dsh-backup#readme",bugs:{url:"https://github.com/xiaoyuyu6420/dsh-backup/issues"},publishConfig:{access:"public"}};var Rn="settings.backupPanel",Ug=gp.name,Dg=["slots","locale","remote"],Ng=y.object({destination:y.string(),dshHome:y.string(),keepDefault:y.number().int(),autoHours:y.number().int(),lastAuto:y.string().nullable(),backups:y.array(y.object({name:y.string(),size:y.number().int().nullable()})),typedBackups:y.array(y.object({name:y.string(),size:y.number().int().nullable(),types:y.array(y.string())})).optional()}),Og=y.object({ok:y.boolean(),summary:y.string(),path:y.string(),sha:y.string(),stale:y.number().int(),keep:y.number().int(),types:y.array(y.string()).optional(),hasCredentials:y.boolean().optional()}),Zg=y.object({ok:y.boolean(),summary:y.string(),results:y.array(y.object({name:y.string(),ok:y.boolean(),note:y.string()}))}),Tg=y.object({ok:y.boolean(),dryRun:y.boolean(),summary:y.string(),archive:y.string().nullable().optional(),files:y.number().int().nullable().optional(),aside:y.string().nullable().optional(),snapshotPath:y.string().nullable().optional(),sample:y.array(y.string()).optional(),preflight:y.array(y.string()).optional(),targetExists:y.boolean().nullable().optional(),merge:y.boolean().optional(),types:y.array(y.string()).optional(),willOverwrite:y.array(y.string()).optional(),restored:y.number().int().optional(),kept:y.array(y.string()).optional()}),Eg=y.object({ok:y.boolean(),hours:y.number().int(),summary:y.string()}),Ag=y.object({repoRaw:y.string().nullable(),repo:y.string().nullable(),tokenSet:y.boolean(),syncDir:y.string(),lastPush:y.string().nullable(),lastError:y.string().nullable()}),Lg=y.object({ok:y.boolean(),summary:y.string(),pushed:y.boolean(),tooBig:y.array(y.string())}),Cg=y.object({ok:y.boolean(),summary:y.string(),pulled:y.array(y.string()),corrupt:y.array(y.string()),total:y.number().int()}),Rg=y.object({ok:y.boolean(),summary:y.string()}),Fg=y.object({ok:y.boolean(),repo:y.string().nullable(),summary:y.string()}),Jg={name:"keep",wire:"keep",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#keep",schema:y.number().int().positive().optional()},acceptsUndefined:!0},Ys={name:"selector",wire:"selector",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#selector",schema:y.string().optional()},acceptsUndefined:!0},Vg={name:"dryRun",wire:"dryRun",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#dryRun",schema:y.boolean().optional()},acceptsUndefined:!0},Mg={name:"syncDeps",wire:"syncDeps",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#syncDeps",schema:y.boolean().optional()},acceptsUndefined:!0},Wg={name:"hours",wire:"hours",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#hours",schema:y.number().int().min(0).max(720)},acceptsUndefined:!0},Kg={name:"repo",wire:"repo",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#repo",schema:y.string().optional()},acceptsUndefined:!0},vp={name:"types",wire:"types",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#types",schema:y.array(y.string()).optional()},acceptsUndefined:!0};function me(t,r,i,o){return Object.freeze({id:`dsh-backup#backupPanel/${t}`,service:"backupPanel",namespace:"backupPanel",method:t,invocation:Object.freeze({kind:"direct"}),parameters:Object.freeze(r.map(e=>Object.freeze({...e,codec:Object.freeze(e.codec)}))),...o?{cancellation:Object.freeze({parameter:"signal"})}:{},result:Object.freeze({mode:"strict",typeSymbol:`dsh-backup/types#${t}Result`,schema:i})})}var hp=Object.freeze({package:"dsh-backup",descriptors:Object.freeze([me("status",[],Ng,!1),me("backup",[Jg,vp],Og,!0),me("verify",[Ys],Zg,!0),me("restore",[Ys,Vg,vp,Mg],Tg,!0),me("setAuto",[Wg],Eg,!1),me("githubStatus",[],Ag,!1),me("githubSyncNow",[],Lg,!0),me("githubPull",[],Cg,!0),me("removeEntry",[Ys],Rg,!0),me("setGithubRepo",[Kg],Fg,!1)])});function fe(t){if(!t.ok){let r=t.error,i=r&&r.message?`${r.code}: ${r.message}`:"backupPanel \u8C03\u7528\u5931\u8D25";throw new Error(`\u64CD\u4F5C\u6CA1\u6709\u5B8C\u6210\uFF1A\u9762\u677F\u6682\u65F6\u8FDE\u4E0D\u4E0A\u540E\u53F0\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u82E5\u53CD\u590D\u51FA\u73B0\u8BF7\u91CD\u542F dsh web\uFF08\u6280\u672F\u7EC6\u8282: ${i}\uFF09`)}return t.value}function Bg(t){t.effect(()=>t.locale.register(Rn,{zh:pp,en:mp}),"dsh-backup: dictionaries"),t.effect(()=>fp(),"dsh-backup: stylesheet"),t.effect(()=>{let r=null,i=!0,o=!1;return(async()=>{try{r=await t.remote.$mount(hp)}catch(e){console.error("dsh-backup: backupPanel mount failed:",e)}i=!1,o&&r?.()})(),()=>{o=!0,i||r?.()}},"dsh-backup: remote contribution"),t.inject(["remote.backupPanel"],r=>{let i=r.locale.bind(Rn),o=()=>r.remote.backupPanel,e={status:async()=>fe(await o().status()),backup:async(n,a)=>fe(await o().backup(n,a)),verify:async n=>fe(await o().verify(n)),restore:async(n,a,c,u)=>fe(await o().restore(n,a,c,u)),setAuto:async n=>fe(await o().setAuto(n)),githubStatus:async()=>fe(await o().githubStatus()),githubSyncNow:async()=>fe(await o().githubSyncNow()),githubPull:async()=>fe(await o().githubPull()),removeEntry:async n=>fe(await o().removeEntry(n)),setGithubRepo:async n=>fe(await o().setGithubRepo(n))};r.slots.inject("settings.plugins.tab",()=>r.slots.register({name:"settings.plugins.tab",id:"backup",order:35,label:()=>i("tab"),locale:Rn,inject:()=>({panel:e})},dp))})}
|
|
454
454
|
return module.exports; } });
|
package/lib/index.js
CHANGED
|
@@ -633,7 +633,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
633
633
|
const toFwd = (p) => (p.includes('\\') ? p.split('\\').join('/') : p);
|
|
634
634
|
const homeRaw = env?.get('HOME')?.value || env?.get('USERPROFILE')?.value;
|
|
635
635
|
const home = homeRaw ? toFwd(homeRaw.replace(/\/+$/, '')) : undefined;
|
|
636
|
-
|
|
636
|
+
// DSH_HOME 与 HOME 一样去尾斜杠(#88):'D:/dsh_data/' 会让 base 变空串、
|
|
637
|
+
// parent 退化为源目录本身,tar 位置参数成空串,产出 0 字节坏归档。
|
|
638
|
+
const dshHomeRaw = env?.get('DSH_HOME')?.value || (home ? `${home}/.dsh` : undefined);
|
|
639
|
+
const dshHome = dshHomeRaw ? toFwd(dshHomeRaw).replace(/\/+$/, '') || '/' : undefined;
|
|
637
640
|
if (!home || !dshHome) throw new Error('无法解析 HOME/USERPROFILE 或 DSH_HOME(launchEnvironment 缺失)');
|
|
638
641
|
const cfg = resolvedConfig();
|
|
639
642
|
const raw = typeof cfg.destination === 'string' && cfg.destination.trim()
|
|
@@ -643,6 +646,12 @@ export function apply(ctx, pluginConfig) {
|
|
|
643
646
|
return { home, dshHome: toFwd(dshHome), root };
|
|
644
647
|
}
|
|
645
648
|
|
|
649
|
+
// #88:parent 由字符串截取求得,DSH_HOME 直接挂在盘符根时退化为 'D:'——
|
|
650
|
+
// bsdtar/GNU tar 都把裸盘符解析为"该盘符当前目录",-C D: 必然失败并留下
|
|
651
|
+
// 空壳归档。tar 的 -C 一律传补过分隔符的值;fs 侧继续用原 parent
|
|
652
|
+
// ('D:' + '/' + name 仍是合法路径,renameBeside 等不受影响)。
|
|
653
|
+
const parentTar = (p) => (p.endsWith('/') ? p : `${p}/`);
|
|
654
|
+
|
|
646
655
|
function defaultKeep() {
|
|
647
656
|
const cfg = resolvedConfig();
|
|
648
657
|
const k = Number(cfg.keep);
|
|
@@ -1008,7 +1017,14 @@ export function apply(ctx, pluginConfig) {
|
|
|
1008
1017
|
const redacted = skipVault ? await existingSensitive(dshHome) : await refreshVault(dshHome, home, signal);
|
|
1009
1018
|
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
1010
1019
|
const redactFlags = isTyped ? [] : redactExcludeFlags(base);
|
|
1011
|
-
|
|
1020
|
+
try {
|
|
1021
|
+
await spawnRun([tar, '--exclude=*node_modules*', '--exclude=.system', ...quarantineFlags, ...redactFlags, ...extraExcludes(), '-czf', name, '-C', parentTar(parent), ...(isTyped ? typePaths : [base])], root, signal);
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
// tar 失败前已创建 -f 目标——空壳归档留在备份目录会被面板当正常备份
|
|
1024
|
+
// 列出来(#88:29 字节静默空包)。删掉空壳再抛,让失败可见。
|
|
1025
|
+
await rm(out, { force: true }).catch(() => {});
|
|
1026
|
+
throw err;
|
|
1027
|
+
}
|
|
1012
1028
|
|
|
1013
1029
|
const shaText = await sha256File(out, home, signal);
|
|
1014
1030
|
await writeOwned(`${out}.sha256`, `${shaText} ${out}\n`);
|
|
@@ -1683,7 +1699,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
1683
1699
|
}
|
|
1684
1700
|
try {
|
|
1685
1701
|
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
1686
|
-
await spawnRun([tar, '-xzf', picked.name, '-C', parent, ...fileTargets], root, signal);
|
|
1702
|
+
await spawnRun([tar, '-xzf', picked.name, '-C', parentTar(parent), ...fileTargets], root, signal);
|
|
1687
1703
|
} catch (err) {
|
|
1688
1704
|
// 对齐 #28 / doctorRepair 回滚:从留档还原每个被覆盖项;连还原都失败
|
|
1689
1705
|
// 时把留档路径交代清楚,绝不静默停在半截状态。
|
|
@@ -1751,7 +1767,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
1751
1767
|
}
|
|
1752
1768
|
try {
|
|
1753
1769
|
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
1754
|
-
await spawnRun([tar, '-xzf', picked.name, '-C', parent], root, signal);
|
|
1770
|
+
await spawnRun([tar, '-xzf', picked.name, '-C', parentTar(parent)], root, signal);
|
|
1755
1771
|
} catch (err) {
|
|
1756
1772
|
// 挪旁之后解压失败会让 .dsh 处于空位——这是恢复流程里用户最容易慌
|
|
1757
1773
|
// 的时刻。尽力自动回滚:清掉半截解压产物、把原数据改回原名;连回滚
|
|
@@ -2420,7 +2436,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
2420
2436
|
// 交代清楚,绝不静默停在最糟的中间态。
|
|
2421
2437
|
try {
|
|
2422
2438
|
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
2423
|
-
await spawnRun([tar, '-xzf', picked.name, '-C', parent, ...targets.map((t) => t.entry)], root, signal);
|
|
2439
|
+
await spawnRun([tar, '-xzf', picked.name, '-C', parentTar(parent), ...targets.map((t) => t.entry)], root, signal);
|
|
2424
2440
|
} catch (err) {
|
|
2425
2441
|
const restored = new Set();
|
|
2426
2442
|
for (let i = 0; i < targets.length; i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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",
|
package/rescue/rescue.mjs
CHANGED
|
@@ -84,6 +84,10 @@ function run(argv, cwd) {
|
|
|
84
84
|
return r.stdout.toString('utf8');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// #88:同 lib/index.js——parent 由字符串截取求得,DSH_HOME 挂盘符根时退化
|
|
88
|
+
// 为 'D:',裸盘符作 tar -C 会失败。-C 一律传补过分隔符的值。
|
|
89
|
+
const parentTar = (p) => (p.endsWith('/') ? p : `${p}/`);
|
|
90
|
+
|
|
87
91
|
async function sha256File(absPath) {
|
|
88
92
|
const info = await fs.stat(absPath);
|
|
89
93
|
if (info.size > HASH_MAX_BYTES) throw new Error(`文件 ${Math.floor(info.size / 1048576)}MB 超过哈希上限`);
|
|
@@ -420,7 +424,7 @@ async function doctorRepair(root, dshHome, selector) {
|
|
|
420
424
|
kept.push(keep);
|
|
421
425
|
}
|
|
422
426
|
try {
|
|
423
|
-
run(['tar', '-xzf', picked.name, '-C', parent, ...targets.map((t) => t.entry)], root);
|
|
427
|
+
run(['tar', '-xzf', picked.name, '-C', parentTar(parent), ...targets.map((t) => t.entry)], root);
|
|
424
428
|
} catch (err) {
|
|
425
429
|
const reason = err && err.message ? err.message : String(err);
|
|
426
430
|
const restored = new Set();
|
|
@@ -493,7 +497,13 @@ async function restoreArchive(root, selector, apply) {
|
|
|
493
497
|
const sensitiveNow = new Set(SENSITIVE_DEFAULTS);
|
|
494
498
|
if (Array.isArray(redactedFiles)) for (const rel of redactedFiles) sensitiveNow.add(rel);
|
|
495
499
|
const redactFlags = [...sensitiveNow].flatMap((rel) => [`--exclude=${base}/${rel}`, `--exclude=*/${rel}`]);
|
|
496
|
-
|
|
500
|
+
try {
|
|
501
|
+
run(['tar', '--exclude=*node_modules*', '--exclude=.system', ...redactFlags, '-czf', snapshotName, '-C', parentTar(parent), base], root);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
// tar 失败留下的空壳快照不能留在恢复点列表里冒充可用备份(#88)
|
|
504
|
+
await fs.rm(`${root}/${snapshotName}`, { force: true }).catch(() => {});
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
497
507
|
// 清掉上一次的 pre-restore 快照(只保留本次),防累积
|
|
498
508
|
for (const name of await fs.readdir(root)) {
|
|
499
509
|
if (name.startsWith('dsh-pre-restore-') && name !== snapshotName) {
|
|
@@ -510,7 +520,7 @@ async function restoreArchive(root, selector, apply) {
|
|
|
510
520
|
aside = `${parent}/${asideName}`;
|
|
511
521
|
}
|
|
512
522
|
try {
|
|
513
|
-
run(['tar', '-xzf', picked.name, '-C', parent], root);
|
|
523
|
+
run(['tar', '-xzf', picked.name, '-C', parentTar(parent)], root);
|
|
514
524
|
} catch (err) {
|
|
515
525
|
const reason = err && err.message ? err.message : String(err);
|
|
516
526
|
let rolledBack = false;
|