@xiaoyuyu6420/dsh-backup 0.14.2 → 0.14.3
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 +1 -1
- package/lib/index.js +223 -95
- package/lib/oplock.js +77 -53
- package/lib/paths.js +56 -0
- package/lib/redact.js +37 -0
- package/package.json +1 -1
- package/rescue/rescue.mjs +30 -3
package/README.en.md
CHANGED
|
@@ -152,6 +152,8 @@ Tried it? Tell us what broke, what's missing, what you liked — it directly sha
|
|
|
152
152
|
<details>
|
|
153
153
|
<summary>Recent releases</summary>
|
|
154
154
|
|
|
155
|
+
- **0.14.3** — ships the security and reliability fixes from #139: validates archive/plugin paths, repairs rollback and operation locking, keeps retained credentials private, and preserves healthy backups when corrupt sessions are detected. Also fixes local-plugin migration, preview validation and large-archive rescue verification. tar/ZIP exports exclude a backup directory inside `DSH_HOME`, including its vault; move that entire directory outside `DSH_HOME` before a full restore. Includes 26 regression scenarios and 71/71 real-host checks on DSH `0.2.2-rc.1`. [Release notes](docs/promo/release-0.14.3.md).
|
|
156
|
+
|
|
155
157
|
- **0.14.2** — review fixes: encryption/decryption now **stream in chunks** and write to a temp file before an atomic rename (no whole-archive buffering, no truncated ciphertext on crash); added the missing client descriptor for `setGithubToken` plus a reverse assertion that every panel method has a descriptor; braced `redactSyncChannel` (s3/gist no longer report a bogus `passwordSet:false`); copy now mentions Codex, the import preview no longer promises per-item override UI, multi-channel sync is documented as push-only, About gains a feedback link, and `rescue --fix --map` requires `--yes`. Verified: smoke 596 / settings 47 / client 62 / real-host e2e 70.
|
|
156
158
|
|
|
157
159
|
- **0.14.1** — follow-up release: npm 0.14.0 shipped before Codex support landed; this version adds Codex chat-history import to the npm track (Claude Code / Codex: scan → preview → import as a new session, function calls paired by call_id into tool call/result, originals untouched) and syncs the verified numbers (smoke 587 / settings 47 / client 60 / real-host e2e 70).
|
package/README.md
CHANGED
|
@@ -154,6 +154,8 @@ dsh plugin --profile web add github:xiaoyuyu6420/dsh-backup
|
|
|
154
154
|
<details>
|
|
155
155
|
<summary>最近的版本</summary>
|
|
156
156
|
|
|
157
|
+
- **0.14.3** —— 发布 #139 的安全与可靠性修复:限制归档与插件导入路径,修复失败回滚、凭据留档权限和操作锁;损坏会话不再触发删除健康备份;修复本地插件迁移、预览结果校验和救援大文件校验。备份目录位于 `DSH_HOME` 内时,tar/ZIP 会排除整个备份目录及 vault,避免重复打包和文件变化错误;整包恢复前需先将整个备份目录移到 `DSH_HOME` 外。新增 26 组回归,真实 DSH `0.2.2-rc.1` e2e 71/71。详情:[发布说明](docs/promo/release-0.14.3.md)。
|
|
158
|
+
|
|
157
159
|
- **0.14.2** —— 复核修复:加密/解密改**分块流式**并「写临时文件 → 原子替换」(大归档不再整包入内存,崩溃不留半截密文);补 `setGithubToken` 缺失的客户端 descriptor + 「面板方法必有 descriptor」反向断言;`redactSyncChannel` 补花括号(修掉 s3/gist 通道假的 `passwordSet:false`);文案补 Codex、导入预览不再承诺界面逐项覆盖、多通道同步写清为单向推送、About 加反馈入口、`rescue --fix --map` 需 `--yes`;验收 smoke 596 / settings 47 / client 62 / 真机 e2e 70。
|
|
158
160
|
|
|
159
161
|
- **0.14.1** —— 补发:npm 上的 0.14.0 先于 Codex 支持发布,本版把「Codex 聊天记录导入」补齐到 npm 版本(Claude Code / Codex 双来源:扫描 → 预览 → 导入为新会话,函数调用按 call_id 配对成工具调用/结果,原数据零改动),并同步验收数字(smoke 587 / settings 47 / client 60 / 真机 e2e 70)。
|
package/lib/client.js
CHANGED
|
@@ -553,5 +553,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
553
553
|
[data-dsh-backup] .dsb-item-actions { flex-wrap: wrap; }
|
|
554
554
|
[data-dsh-backup] .dsb-kv { grid-template-columns: 1fr; }
|
|
555
555
|
}
|
|
556
|
-
`;var ym={name:"@xiaoyuyu6420/dsh-backup",version:"0.14.2",description:"\u4E00\u6761\u547D\u4EE4\u5907\u4EFD\u3001\u6062\u590D\u4E0E\u8FC1\u79FB DeepSeek Harness\uFF08dsh\uFF09\u7684\u5168\u90E8\u6570\u636E\uFF08~/.dsh\uFF1A\u4F1A\u8BDD\u3001\u8BBE\u7F6E\u3001\u63D2\u4EF6\u3001MCP\u3001\u6280\u80FD\uFF09\uFF1A\u5B9A\u65F6\u81EA\u52A8\u5907\u4EFD\u3001\u5347\u7EA7\u524D\u5FEB\u7167\u3001\u6362\u673A\u8FC1\u79FB\u3001\u4F1A\u8BDD\u65E5\u5FD7\u4F53\u68C0\u4FEE\u590D\u3001\u5BBF\u4E3B\u8D77\u4E0D\u6765\u4E5F\u80FD\u7528\u7684\u8FDB\u7A0B\u5916\u6551\u63F4\uFF1B\u51ED\u636E\u9ED8\u8BA4\u8131\u654F\u53EA\u5B58\u672C\u673A\u3002 Backup, restore & migrate all DeepSeek Harness (~/.dsh) data \u2014 sessions, settings, plugins, MCP, skills: scheduled backups, pre-upgrade snapshots, cross-machine migration, session-log doctor, out-of-process rescue console; credentials redacted to a local-only vault.",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","migration","\u8FC1\u79FB","\u6362\u673A","schedule","\u81EA\u52A8\u5907\u4EFD","cross-platform","cross-machine","rescue","doctor","upgrade-snapshot","mcp","skills","\u6280\u80FD","sessions","\u4F1A\u8BDD","workspace","\u5DE5\u4F5C\u533A","vault","\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 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-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 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-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 hi="settings.backupPanel",xb=ym.name,_b=["slots","locale","remote"],wb=c.object({destination:c.string(),dshHome:c.string(),keepDefault:c.number().int(),autoHours:c.number().int(),lastAuto:c.string().nullable(),backups:c.array(c.object({name:c.string(),size:c.number().int().nullable()})),typedBackups:c.array(c.object({name:c.string(),size:c.number().int().nullable(),types:c.array(c.string())})).optional()}),Sb=c.object({ok:c.boolean(),summary:c.string(),path:c.string(),sha:c.string(),stale:c.number().int(),keep:c.number().int(),encrypted:c.boolean().optional(),types:c.array(c.string()).optional(),hasCredentials:c.boolean().optional()}),zb=c.object({ok:c.boolean(),summary:c.string(),results:c.array(c.object({name:c.string(),ok:c.boolean(),note:c.string()}))}),Ib=c.object({ok:c.boolean(),dryRun:c.boolean(),summary:c.string(),archive:c.string().nullable().optional(),files:c.number().int().nullable().optional(),aside:c.string().nullable().optional(),snapshotPath:c.string().nullable().optional(),sample:c.array(c.string()).optional(),preflight:c.array(c.string()).optional(),targetExists:c.boolean().nullable().optional(),merge:c.boolean().optional(),types:c.array(c.string()).optional(),willOverwrite:c.array(c.string()).optional(),restored:c.number().int().optional(),kept:c.array(c.string()).optional(),encrypted:c.boolean().optional()}),Pb=c.object({ok:c.boolean(),hours:c.number().int(),summary:c.string()}),Nb=c.object({repoRaw:c.string().nullable(),repo:c.string().nullable(),tokenSet:c.boolean(),syncDir:c.string(),lastPush:c.string().nullable(),lastError:c.string().nullable()}),jb=c.object({ok:c.boolean(),summary:c.string(),pushed:c.boolean(),tooBig:c.array(c.string())}),Db=c.object({ok:c.boolean(),summary:c.string(),pulled:c.array(c.string()),corrupt:c.array(c.string()),total:c.number().int()}),Ub=c.object({ok:c.boolean(),summary:c.string()}),Tb=c.object({ok:c.boolean(),repo:c.string().nullable(),summary:c.string()}),Ob=c.object({ok:c.boolean(),current:c.string(),latest:c.string().nullable(),update:c.boolean(),error:c.string().nullable(),summary:c.string()}),Zb=c.object({ok:c.boolean(),updated:c.boolean(),current:c.string().nullable(),latest:c.string().nullable(),summary:c.string()}),Eb=c.object({ok:c.boolean(),summary:c.string(),path:c.string().optional(),name:c.string().optional(),size:c.number().int().optional(),encrypted:c.boolean().optional(),manifest:c.unknown().optional(),redactedFiles:c.array(c.string()).optional()}),Cb=c.object({ok:c.boolean(),summary:c.string(),encrypted:c.boolean().optional(),manifest:c.unknown().optional(),sections:c.array(c.object({key:c.string(),label:c.string().optional(),files:c.number().int().optional(),bytes:c.number().optional(),conflicts:c.array(c.object({name:c.string(),rel:c.string().optional(),exists:c.boolean().optional()})).optional()})).optional(),totalFiles:c.number().int().optional(),totalBytes:c.number().optional(),conflicts:c.array(c.object({name:c.string(),rel:c.string().optional(),section:c.string().optional(),exists:c.boolean().optional()})).optional(),remap:c.array(c.unknown()).optional(),compatibility:c.object({score:c.string().optional(),reasons:c.array(c.string()).optional()}).optional()}),Ab=c.object({ok:c.boolean(),summary:c.string(),encrypted:c.boolean().optional(),applied:c.number().int().optional(),skipped:c.number().int().optional(),overwrote:c.number().int().optional(),snapshot:c.string().nullable().optional(),remapped:c.number().int().optional()}),Rb=c.object({ok:c.boolean(),summary:c.string(),channels:c.object({github:c.unknown().optional(),webdav:c.unknown().optional(),s3:c.unknown().optional(),gist:c.unknown().optional()}).optional()}),Lb=c.object({ok:c.boolean(),summary:c.string(),pushed:c.boolean().optional(),count:c.number().int().optional(),at:c.string().optional(),tooBig:c.array(c.string()).optional(),gistUrl:c.string().nullable().optional()}),Fb=c.object({ok:c.boolean(),summary:c.string(),channel:c.string().optional()}),Vb=c.object({ok:c.boolean(),summary:c.string()}),Jb=c.object({ok:c.boolean(),held:c.boolean().optional(),stale:c.boolean().optional(),pid:c.number().int().nullable().optional(),operation:c.string().nullable().optional(),startedAt:c.string().nullable().optional(),heartbeatAt:c.string().nullable().optional()}),Mb=c.object({ok:c.boolean(),summary:c.string()}),Bb=c.object({ok:c.boolean(),total:c.number().optional(),totalText:c.string().optional(),categories:c.array(c.object({key:c.string(),label:c.string().optional(),bytes:c.number().optional(),files:c.number().int().optional()})).optional(),summary:c.string()}),Wb=c.object({ok:c.boolean(),removed:c.number().int().optional(),names:c.array(c.string()).optional(),summary:c.string()}),Kb=c.object({ok:c.boolean(),weekly:c.array(c.object({day:c.number().int(),time:c.string()})).optional(),summary:c.string()}),Hb=c.object({ok:c.boolean(),version:c.string().optional(),dshVersion:c.string().optional(),platform:c.string().optional(),node:c.string().optional(),profile:c.string().optional(),pluginDir:c.string().optional()}),Gb=c.object({ok:c.boolean(),summary:c.string(),profiles:c.array(c.object({name:c.string(),version:c.string().optional(),appOwned:c.boolean().optional(),readonly:c.boolean().optional(),running:c.boolean().optional(),pid:c.number().int().nullable().optional()})).optional()}),qb=c.object({ok:c.boolean(),summary:c.string(),name:c.string().optional()}),Xb=c.object({ok:c.boolean(),summary:c.string(),from:c.string().optional(),to:c.string().optional()}),Yb=c.object({ok:c.boolean(),summary:c.string(),name:c.string().optional()}),Qb=c.object({ok:c.boolean(),summary:c.string(),pid:c.number().int().nullable().optional(),running:c.boolean().optional()}),eh=c.object({ok:c.boolean(),summary:c.string(),running:c.boolean().optional()}),th=c.object({id:c.string(),name:c.string(),desc:c.string().optional(),sections:c.array(c.string()).optional(),url:c.string().optional(),source:c.string().optional(),risk:c.string().optional(),version:c.string().optional()}),_m=c.object({ok:c.boolean(),enabled:c.boolean().optional(),entries:c.array(th).optional(),summary:c.string()}),rh=_m,nh=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),entry:c.object({id:c.string(),name:c.string().optional()}).optional(),preview:c.unknown().optional(),applied:c.unknown().optional(),supplyChain:c.array(c.string()).optional(),summary:c.string()}),ih=c.object({id:c.string(),label:c.string().optional(),detected:c.boolean().optional(),skills:c.number().int().optional(),instructions:c.string().nullable().optional(),mcpServers:c.array(c.string()).optional()}),oh=c.object({ok:c.boolean(),sources:c.array(ih).optional(),summary:c.string()}),wm=c.object({source:c.string(),label:c.string().optional(),skills:c.number().int().optional(),instructions:c.string().nullable().optional(),mcpServers:c.array(c.string()).optional(),totalFiles:c.number().int().optional(),totalBytes:c.number().optional()}),ah=c.object({ok:c.boolean(),report:wm.optional(),preview:c.unknown().optional(),summary:c.string()}),sh=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),report:wm.optional(),preview:c.unknown().optional(),applied:c.unknown().optional(),summary:c.string()}),ch=c.object({id:c.string(),rel:c.string().optional(),file:c.string().optional(),cwd:c.string().nullable().optional(),version:c.number().int().optional()}),lh=c.object({ok:c.boolean(),sessions:c.array(ch).optional(),summary:c.string()}),uh=c.object({ok:c.boolean(),session:c.string().optional(),targetSeq:c.number().int().optional(),keepRecords:c.number().int().optional(),dropRecords:c.number().int().optional(),newId:c.string().optional(),newPath:c.string().optional(),warnings:c.array(c.string()).optional(),summary:c.string()}),dh=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),session:c.string().optional(),newId:c.string().optional(),newPath:c.string().optional(),keepRecords:c.number().int().optional(),summary:c.string()}),ph=c.object({source:c.string().optional(),id:c.string(),file:c.string().optional(),project:c.string().optional(),messages:c.number().int().optional(),title:c.string().optional()}),mh=c.object({id:c.string(),label:c.string().optional(),detected:c.boolean().optional(),count:c.number().int().optional()}),fh=c.object({ok:c.boolean(),sources:c.array(mh).optional(),detected:c.boolean().optional(),sessions:c.array(ph).optional(),summary:c.string()}),$m=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),source:c.string().optional(),session:c.string().optional(),file:c.string().optional(),messages:c.number().int().optional(),userCount:c.number().int().optional(),asstCount:c.number().int().optional(),toolCount:c.number().int().optional(),skipped:c.number().int().optional(),events:c.number().int().optional(),newId:c.string().optional(),path:c.string().optional(),warnings:c.array(c.string()).optional(),summary:c.string()});function L(t,r){return Object.freeze({mode:"strict",typeSymbol:t,schema:r,create:()=>r})}var gh={name:"keep",wire:"keep",source:"json",codec:L("dsh-backup/types#keep",c.number().int().positive().optional()),acceptsUndefined:!0},Vl={name:"selector",wire:"selector",source:"json",codec:L("dsh-backup/types#selector",c.string().optional()),acceptsUndefined:!0},vh={name:"dryRun",wire:"dryRun",source:"json",codec:L("dsh-backup/types#dryRun",c.boolean().optional()),acceptsUndefined:!0},bh={name:"syncDeps",wire:"syncDeps",source:"json",codec:L("dsh-backup/types#syncDeps",c.boolean().optional()),acceptsUndefined:!0},hh={name:"hours",wire:"hours",source:"json",codec:L("dsh-backup/types#hours",c.number().int().min(0).max(720)),acceptsUndefined:!0},yh={name:"token",wire:"token",source:"json",codec:L("dsh-backup/types#token",c.string()),acceptsUndefined:!1},$h=c.object({ok:c.boolean(),tokenSet:c.boolean().optional(),summary:c.string()}),kh={name:"repo",wire:"repo",source:"json",codec:L("dsh-backup/types#repo",c.string().optional()),acceptsUndefined:!0},km={name:"types",wire:"types",source:"json",codec:L("dsh-backup/types#types",c.array(c.string()).optional()),acceptsUndefined:!0},xm={name:"password",wire:"password",source:"json",codec:L("dsh-backup/types#password",c.string().optional()),acceptsUndefined:!0},$e={name:"options",wire:"options",source:"json",codec:L("dsh-backup/types#options",c.object({}).passthrough())},Jl={name:"channel",wire:"channel",source:"json",codec:L("dsh-backup/types#channel",c.string())},xh={name:"config",wire:"config",source:"json",codec:L("dsh-backup/types#config",c.object({}).passthrough())},_h={name:"force",wire:"force",source:"json",codec:L("dsh-backup/types#force",c.boolean().optional()),acceptsUndefined:!0},wh={name:"scope",wire:"scope",source:"json",codec:L("dsh-backup/types#scope",c.string())},Sh={name:"schedules",wire:"schedules",source:"json",codec:L("dsh-backup/types#schedules",c.array(c.object({day:c.number().int(),time:c.string()})))},bi={name:"name",wire:"name",source:"json",codec:L("dsh-backup/types#name",c.string())},zh={name:"old",wire:"old",source:"json",codec:L("dsh-backup/types#old",c.string())},Ih={name:"new",wire:"new",source:"json",codec:L("dsh-backup/types#new",c.string())},Ph={name:"query",wire:"query",source:"json",codec:L("dsh-backup/types#query",c.string().optional()),acceptsUndefined:!0},Nh={name:"session",wire:"session",source:"json",codec:L("dsh-backup/types#session",c.string())},jh={name:"seq",wire:"seq",source:"json",codec:L("dsh-backup/types#seq",c.number().int().optional()),acceptsUndefined:!0};function N(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:L(`dsh-backup/types#${t}Result`,i)})}var Sm=Object.freeze({package:"dsh-backup",descriptors:Object.freeze([N("status",[],wb,!1),N("backup",[gh,km,xm],Sb,!0),N("verify",[Vl],zb,!0),N("restore",[Vl,vh,km,bh,xm],Ib,!0),N("setAuto",[hh],Pb,!1),N("githubStatus",[],Nb,!1),N("githubSyncNow",[],jb,!0),N("githubPull",[],Db,!0),N("removeEntry",[Vl],Ub,!0),N("setGithubRepo",[kh],Tb,!1),N("setGithubToken",[yh],$h,!1),N("checkUpdate",[],Ob,!0),N("update",[],Zb,!0),N("exportConfig",[$e],Eb,!0),N("importPreview",[$e],Cb,!0),N("importApply",[$e],Ab,!0),N("syncStatus",[],Rb,!1),N("syncNow",[Jl],Lb,!0),N("setSyncChannel",[Jl,xh],Fb,!1),N("removeSyncChannel",[Jl],Vb,!1),N("lockStatus",[],Jb,!1),N("reclaimLock",[_h],Mb,!1),N("diskUsage",[],Bb,!0),N("cleanup",[wh],Wb,!0),N("setWeekly",[Sh],Kb,!1),N("about",[],Hb,!1),N("profilesList",[],Gb,!1),N("profileCreate",[bi],qb,!1),N("profileRename",[zh,Ih],Xb,!1),N("profileDelete",[bi],Yb,!1),N("profileLaunch",[bi],Qb,!1),N("profileStop",[bi],eh,!1),N("marketList",[],_m,!0),N("marketSearch",[Ph],rh,!0),N("marketInstall",[$e],nh,!0),N("foreignSources",[],oh,!1),N("foreignPreview",[$e],ah,!0),N("foreignImport",[$e],sh,!0),N("sessionRewindList",[],lh,!1),N("sessionRewindPreview",[Nh,jh],uh,!0),N("sessionRewindApply",[$e],dh,!0),N("chatSources",[],fh,!1),N("chatPreview",[$e],$m,!0),N("chatApply",[$e],$m,!0)])});function j(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 Dh(t){t.effect(()=>t.locale.register(hi,{zh:vm,en:bm}),"dsh-backup: dictionaries"),t.effect(()=>hm(),"dsh-backup: stylesheet");let r=!1,i=(o,e,n)=>{if(r&&e!==Fl)return;let a=o.locale.bind(hi);o.slots.inject("settings.plugins.tab",()=>o.slots.register({name:"settings.plugins.tab",id:"backup",order:35,label:()=>a("tab"),locale:hi,inject:()=>({panel:n})},e))};t.inject(["slots","locale"],o=>{t.inject(["remote"],e=>{t.effect(()=>{let n=null,a=!0,s=!1;return(async()=>{try{n=await e.remote.$mount(Sm)}catch(m){console.error("dsh-backup: backupPanel mount failed:",m),r=!0,i(o,Fl,{mountError:m})}a=!1,s&&n?.()})(),()=>{s=!0,a||n?.()}},"dsh-backup: remote contribution")}),t.inject(["remote.backupPanel"],e=>{let n=()=>e.remote.backupPanel;i(e,gm,{status:async()=>j(await n().status()),backup:async(s,m,d)=>j(await n().backup(s,m,d)),verify:async s=>j(await n().verify(s)),restore:async(s,m,d,p,b)=>j(await n().restore(s,m,d,p,b)),setAuto:async s=>j(await n().setAuto(s)),githubStatus:async()=>j(await n().githubStatus()),githubSyncNow:async()=>j(await n().githubSyncNow()),githubPull:async()=>j(await n().githubPull()),removeEntry:async s=>j(await n().removeEntry(s)),setGithubRepo:async s=>j(await n().setGithubRepo(s)),setGithubToken:async s=>j(await n().setGithubToken(s)),checkUpdate:async()=>j(await n().checkUpdate()),update:async()=>j(await n().update()),exportConfig:async s=>j(await n().exportConfig(s)),importPreview:async s=>j(await n().importPreview(s)),importApply:async s=>j(await n().importApply(s)),syncStatus:async()=>j(await n().syncStatus()),syncNow:async s=>j(await n().syncNow(s)),setSyncChannel:async(s,m)=>j(await n().setSyncChannel(s,m)),removeSyncChannel:async s=>j(await n().removeSyncChannel(s)),lockStatus:async()=>j(await n().lockStatus()),reclaimLock:async s=>j(await n().reclaimLock(s)),diskUsage:async()=>j(await n().diskUsage()),cleanup:async s=>j(await n().cleanup(s)),setWeekly:async s=>j(await n().setWeekly(s)),about:async()=>j(await n().about()),profilesList:async()=>j(await n().profilesList()),profileCreate:async s=>j(await n().profileCreate(s)),profileRename:async(s,m)=>j(await n().profileRename(s,m)),profileDelete:async s=>j(await n().profileDelete(s)),profileLaunch:async s=>j(await n().profileLaunch(s)),profileStop:async s=>j(await n().profileStop(s)),marketList:async()=>j(await n().marketList()),marketSearch:async s=>j(await n().marketSearch(s)),marketInstall:async s=>j(await n().marketInstall(s)),foreignSources:async()=>j(await n().foreignSources()),foreignPreview:async s=>j(await n().foreignPreview(s)),foreignImport:async s=>j(await n().foreignImport(s)),sessionRewindList:async()=>j(await n().sessionRewindList()),sessionRewindPreview:async(s,m)=>j(await n().sessionRewindPreview(s,m)),sessionRewindApply:async s=>j(await n().sessionRewindApply(s)),chatSources:async()=>j(await n().chatSources()),chatPreview:async s=>j(await n().chatPreview(s)),chatApply:async s=>j(await n().chatApply(s))})})})}
|
|
556
|
+
`;var ym={name:"@xiaoyuyu6420/dsh-backup",version:"0.14.3",description:"\u4E00\u6761\u547D\u4EE4\u5907\u4EFD\u3001\u6062\u590D\u4E0E\u8FC1\u79FB DeepSeek Harness\uFF08dsh\uFF09\u7684\u5168\u90E8\u6570\u636E\uFF08~/.dsh\uFF1A\u4F1A\u8BDD\u3001\u8BBE\u7F6E\u3001\u63D2\u4EF6\u3001MCP\u3001\u6280\u80FD\uFF09\uFF1A\u5B9A\u65F6\u81EA\u52A8\u5907\u4EFD\u3001\u5347\u7EA7\u524D\u5FEB\u7167\u3001\u6362\u673A\u8FC1\u79FB\u3001\u4F1A\u8BDD\u65E5\u5FD7\u4F53\u68C0\u4FEE\u590D\u3001\u5BBF\u4E3B\u8D77\u4E0D\u6765\u4E5F\u80FD\u7528\u7684\u8FDB\u7A0B\u5916\u6551\u63F4\uFF1B\u51ED\u636E\u9ED8\u8BA4\u8131\u654F\u53EA\u5B58\u672C\u673A\u3002 Backup, restore & migrate all DeepSeek Harness (~/.dsh) data \u2014 sessions, settings, plugins, MCP, skills: scheduled backups, pre-upgrade snapshots, cross-machine migration, session-log doctor, out-of-process rescue console; credentials redacted to a local-only vault.",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","migration","\u8FC1\u79FB","\u6362\u673A","schedule","\u81EA\u52A8\u5907\u4EFD","cross-platform","cross-machine","rescue","doctor","upgrade-snapshot","mcp","skills","\u6280\u80FD","sessions","\u4F1A\u8BDD","workspace","\u5DE5\u4F5C\u533A","vault","\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 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-rc.1","@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 || ^0.2.0-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 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-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 hi="settings.backupPanel",xb=ym.name,_b=["slots","locale","remote"],wb=c.object({destination:c.string(),dshHome:c.string(),keepDefault:c.number().int(),autoHours:c.number().int(),lastAuto:c.string().nullable(),backups:c.array(c.object({name:c.string(),size:c.number().int().nullable()})),typedBackups:c.array(c.object({name:c.string(),size:c.number().int().nullable(),types:c.array(c.string())})).optional()}),Sb=c.object({ok:c.boolean(),summary:c.string(),path:c.string(),sha:c.string(),stale:c.number().int(),keep:c.number().int(),encrypted:c.boolean().optional(),types:c.array(c.string()).optional(),hasCredentials:c.boolean().optional()}),zb=c.object({ok:c.boolean(),summary:c.string(),results:c.array(c.object({name:c.string(),ok:c.boolean(),note:c.string()}))}),Ib=c.object({ok:c.boolean(),dryRun:c.boolean(),summary:c.string(),archive:c.string().nullable().optional(),files:c.number().int().nullable().optional(),aside:c.string().nullable().optional(),snapshotPath:c.string().nullable().optional(),sample:c.array(c.string()).optional(),preflight:c.array(c.string()).optional(),targetExists:c.boolean().nullable().optional(),merge:c.boolean().optional(),types:c.array(c.string()).optional(),willOverwrite:c.array(c.string()).optional(),restored:c.number().int().optional(),kept:c.array(c.string()).optional(),encrypted:c.boolean().optional()}),Pb=c.object({ok:c.boolean(),hours:c.number().int(),summary:c.string()}),Nb=c.object({repoRaw:c.string().nullable(),repo:c.string().nullable(),tokenSet:c.boolean(),syncDir:c.string(),lastPush:c.string().nullable(),lastError:c.string().nullable()}),jb=c.object({ok:c.boolean(),summary:c.string(),pushed:c.boolean(),tooBig:c.array(c.string())}),Db=c.object({ok:c.boolean(),summary:c.string(),pulled:c.array(c.string()),corrupt:c.array(c.string()),total:c.number().int()}),Ub=c.object({ok:c.boolean(),summary:c.string()}),Tb=c.object({ok:c.boolean(),repo:c.string().nullable(),summary:c.string()}),Ob=c.object({ok:c.boolean(),current:c.string(),latest:c.string().nullable(),update:c.boolean(),error:c.string().nullable(),summary:c.string()}),Zb=c.object({ok:c.boolean(),updated:c.boolean(),current:c.string().nullable(),latest:c.string().nullable(),summary:c.string()}),Eb=c.object({ok:c.boolean(),summary:c.string(),path:c.string().optional(),name:c.string().optional(),size:c.number().int().optional(),encrypted:c.boolean().optional(),manifest:c.unknown().optional(),redactedFiles:c.array(c.string()).optional()}),Cb=c.object({ok:c.boolean(),summary:c.string(),encrypted:c.boolean().optional(),manifest:c.unknown().optional(),sections:c.array(c.object({key:c.string(),label:c.string().optional(),files:c.number().int().optional(),bytes:c.number().optional(),conflicts:c.array(c.object({name:c.string(),rel:c.string().optional(),exists:c.boolean().optional()})).optional()})).optional(),totalFiles:c.number().int().optional(),totalBytes:c.number().optional(),conflicts:c.array(c.object({name:c.string(),rel:c.string().optional(),section:c.string().optional(),exists:c.boolean().optional()})).optional(),remap:c.array(c.unknown()).optional(),compatibility:c.object({score:c.number().min(0).max(100).optional(),reasons:c.array(c.string()).optional()}).optional()}),Ab=c.object({ok:c.boolean(),summary:c.string(),encrypted:c.boolean().optional(),applied:c.number().int().optional(),skipped:c.number().int().optional(),overwrote:c.number().int().optional(),snapshot:c.string().nullable().optional(),remapped:c.number().int().optional()}),Rb=c.object({ok:c.boolean(),summary:c.string(),channels:c.object({github:c.unknown().optional(),webdav:c.unknown().optional(),s3:c.unknown().optional(),gist:c.unknown().optional()}).optional()}),Lb=c.object({ok:c.boolean(),summary:c.string(),pushed:c.boolean().optional(),count:c.number().int().optional(),at:c.string().optional(),tooBig:c.array(c.string()).optional(),gistUrl:c.string().nullable().optional()}),Fb=c.object({ok:c.boolean(),summary:c.string(),channel:c.string().optional()}),Vb=c.object({ok:c.boolean(),summary:c.string()}),Jb=c.object({ok:c.boolean(),held:c.boolean().optional(),stale:c.boolean().optional(),pid:c.number().int().nullable().optional(),operation:c.string().nullable().optional(),startedAt:c.string().nullable().optional(),heartbeatAt:c.string().nullable().optional()}),Mb=c.object({ok:c.boolean(),summary:c.string()}),Bb=c.object({ok:c.boolean(),total:c.number().optional(),totalText:c.string().optional(),categories:c.array(c.object({key:c.string(),label:c.string().optional(),bytes:c.number().optional(),files:c.number().int().optional()})).optional(),summary:c.string()}),Wb=c.object({ok:c.boolean(),removed:c.number().int().optional(),names:c.array(c.string()).optional(),summary:c.string()}),Kb=c.object({ok:c.boolean(),weekly:c.array(c.object({day:c.number().int(),time:c.string()})).optional(),summary:c.string()}),Hb=c.object({ok:c.boolean(),version:c.string().optional(),dshVersion:c.string().optional(),platform:c.string().optional(),node:c.string().optional(),profile:c.string().optional(),pluginDir:c.string().optional()}),Gb=c.object({ok:c.boolean(),summary:c.string(),profiles:c.array(c.object({name:c.string(),version:c.string().optional(),appOwned:c.boolean().optional(),readonly:c.boolean().optional(),running:c.boolean().optional(),pid:c.number().int().nullable().optional()})).optional()}),qb=c.object({ok:c.boolean(),summary:c.string(),name:c.string().optional()}),Xb=c.object({ok:c.boolean(),summary:c.string(),from:c.string().optional(),to:c.string().optional()}),Yb=c.object({ok:c.boolean(),summary:c.string(),name:c.string().optional()}),Qb=c.object({ok:c.boolean(),summary:c.string(),pid:c.number().int().nullable().optional(),running:c.boolean().optional()}),eh=c.object({ok:c.boolean(),summary:c.string(),running:c.boolean().optional()}),th=c.object({id:c.string(),name:c.string(),desc:c.string().optional(),sections:c.array(c.string()).optional(),url:c.string().optional(),source:c.string().optional(),risk:c.string().optional(),version:c.string().optional()}),_m=c.object({ok:c.boolean(),enabled:c.boolean().optional(),entries:c.array(th).optional(),summary:c.string()}),rh=_m,nh=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),entry:c.object({id:c.string(),name:c.string().optional()}).optional(),preview:c.unknown().optional(),applied:c.unknown().optional(),supplyChain:c.array(c.string()).optional(),summary:c.string()}),ih=c.object({id:c.string(),label:c.string().optional(),detected:c.boolean().optional(),skills:c.number().int().optional(),instructions:c.string().nullable().optional(),mcpServers:c.array(c.string()).optional()}),oh=c.object({ok:c.boolean(),sources:c.array(ih).optional(),summary:c.string()}),wm=c.object({source:c.string(),label:c.string().optional(),skills:c.number().int().optional(),instructions:c.string().nullable().optional(),mcpServers:c.array(c.string()).optional(),totalFiles:c.number().int().optional(),totalBytes:c.number().optional()}),ah=c.object({ok:c.boolean(),report:wm.optional(),preview:c.unknown().optional(),summary:c.string()}),sh=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),report:wm.optional(),preview:c.unknown().optional(),applied:c.unknown().optional(),summary:c.string()}),ch=c.object({id:c.string(),rel:c.string().optional(),file:c.string().optional(),cwd:c.string().nullable().optional(),version:c.number().int().optional()}),lh=c.object({ok:c.boolean(),sessions:c.array(ch).optional(),summary:c.string()}),uh=c.object({ok:c.boolean(),session:c.string().optional(),targetSeq:c.number().int().optional(),keepRecords:c.number().int().optional(),dropRecords:c.number().int().optional(),newId:c.string().optional(),newPath:c.string().optional(),warnings:c.array(c.string()).optional(),summary:c.string()}),dh=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),session:c.string().optional(),newId:c.string().optional(),newPath:c.string().optional(),keepRecords:c.number().int().optional(),summary:c.string()}),ph=c.object({source:c.string().optional(),id:c.string(),file:c.string().optional(),project:c.string().optional(),messages:c.number().int().optional(),title:c.string().optional()}),mh=c.object({id:c.string(),label:c.string().optional(),detected:c.boolean().optional(),count:c.number().int().optional()}),fh=c.object({ok:c.boolean(),sources:c.array(mh).optional(),detected:c.boolean().optional(),sessions:c.array(ph).optional(),summary:c.string()}),$m=c.object({ok:c.boolean(),dryRun:c.boolean().optional(),source:c.string().optional(),session:c.string().optional(),file:c.string().optional(),messages:c.number().int().optional(),userCount:c.number().int().optional(),asstCount:c.number().int().optional(),toolCount:c.number().int().optional(),skipped:c.number().int().optional(),events:c.number().int().optional(),newId:c.string().optional(),path:c.string().optional(),warnings:c.array(c.string()).optional(),summary:c.string()});function L(t,r){return Object.freeze({mode:"strict",typeSymbol:t,schema:r,create:()=>r})}var gh={name:"keep",wire:"keep",source:"json",codec:L("dsh-backup/types#keep",c.number().int().positive().optional()),acceptsUndefined:!0},Vl={name:"selector",wire:"selector",source:"json",codec:L("dsh-backup/types#selector",c.string().optional()),acceptsUndefined:!0},vh={name:"dryRun",wire:"dryRun",source:"json",codec:L("dsh-backup/types#dryRun",c.boolean().optional()),acceptsUndefined:!0},bh={name:"syncDeps",wire:"syncDeps",source:"json",codec:L("dsh-backup/types#syncDeps",c.boolean().optional()),acceptsUndefined:!0},hh={name:"hours",wire:"hours",source:"json",codec:L("dsh-backup/types#hours",c.number().int().min(0).max(720)),acceptsUndefined:!0},yh={name:"token",wire:"token",source:"json",codec:L("dsh-backup/types#token",c.string()),acceptsUndefined:!1},$h=c.object({ok:c.boolean(),tokenSet:c.boolean().optional(),summary:c.string()}),kh={name:"repo",wire:"repo",source:"json",codec:L("dsh-backup/types#repo",c.string().optional()),acceptsUndefined:!0},km={name:"types",wire:"types",source:"json",codec:L("dsh-backup/types#types",c.array(c.string()).optional()),acceptsUndefined:!0},xm={name:"password",wire:"password",source:"json",codec:L("dsh-backup/types#password",c.string().optional()),acceptsUndefined:!0},$e={name:"options",wire:"options",source:"json",codec:L("dsh-backup/types#options",c.object({}).passthrough())},Jl={name:"channel",wire:"channel",source:"json",codec:L("dsh-backup/types#channel",c.string())},xh={name:"config",wire:"config",source:"json",codec:L("dsh-backup/types#config",c.object({}).passthrough())},_h={name:"force",wire:"force",source:"json",codec:L("dsh-backup/types#force",c.boolean().optional()),acceptsUndefined:!0},wh={name:"scope",wire:"scope",source:"json",codec:L("dsh-backup/types#scope",c.string())},Sh={name:"schedules",wire:"schedules",source:"json",codec:L("dsh-backup/types#schedules",c.array(c.object({day:c.number().int(),time:c.string()})))},bi={name:"name",wire:"name",source:"json",codec:L("dsh-backup/types#name",c.string())},zh={name:"old",wire:"old",source:"json",codec:L("dsh-backup/types#old",c.string())},Ih={name:"new",wire:"new",source:"json",codec:L("dsh-backup/types#new",c.string())},Ph={name:"query",wire:"query",source:"json",codec:L("dsh-backup/types#query",c.string().optional()),acceptsUndefined:!0},Nh={name:"session",wire:"session",source:"json",codec:L("dsh-backup/types#session",c.string())},jh={name:"seq",wire:"seq",source:"json",codec:L("dsh-backup/types#seq",c.number().int().optional()),acceptsUndefined:!0};function N(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:L(`dsh-backup/types#${t}Result`,i)})}var Sm=Object.freeze({package:"dsh-backup",descriptors:Object.freeze([N("status",[],wb,!1),N("backup",[gh,km,xm],Sb,!0),N("verify",[Vl],zb,!0),N("restore",[Vl,vh,km,bh,xm],Ib,!0),N("setAuto",[hh],Pb,!1),N("githubStatus",[],Nb,!1),N("githubSyncNow",[],jb,!0),N("githubPull",[],Db,!0),N("removeEntry",[Vl],Ub,!0),N("setGithubRepo",[kh],Tb,!1),N("setGithubToken",[yh],$h,!1),N("checkUpdate",[],Ob,!0),N("update",[],Zb,!0),N("exportConfig",[$e],Eb,!0),N("importPreview",[$e],Cb,!0),N("importApply",[$e],Ab,!0),N("syncStatus",[],Rb,!1),N("syncNow",[Jl],Lb,!0),N("setSyncChannel",[Jl,xh],Fb,!1),N("removeSyncChannel",[Jl],Vb,!1),N("lockStatus",[],Jb,!1),N("reclaimLock",[_h],Mb,!1),N("diskUsage",[],Bb,!0),N("cleanup",[wh],Wb,!0),N("setWeekly",[Sh],Kb,!1),N("about",[],Hb,!1),N("profilesList",[],Gb,!1),N("profileCreate",[bi],qb,!1),N("profileRename",[zh,Ih],Xb,!1),N("profileDelete",[bi],Yb,!1),N("profileLaunch",[bi],Qb,!1),N("profileStop",[bi],eh,!1),N("marketList",[],_m,!0),N("marketSearch",[Ph],rh,!0),N("marketInstall",[$e],nh,!0),N("foreignSources",[],oh,!1),N("foreignPreview",[$e],ah,!0),N("foreignImport",[$e],sh,!0),N("sessionRewindList",[],lh,!1),N("sessionRewindPreview",[Nh,jh],uh,!0),N("sessionRewindApply",[$e],dh,!0),N("chatSources",[],fh,!1),N("chatPreview",[$e],$m,!0),N("chatApply",[$e],$m,!0)])});function j(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 Dh(t){t.effect(()=>t.locale.register(hi,{zh:vm,en:bm}),"dsh-backup: dictionaries"),t.effect(()=>hm(),"dsh-backup: stylesheet");let r=!1,i=(o,e,n)=>{if(r&&e!==Fl)return;let a=o.locale.bind(hi);o.slots.inject("settings.plugins.tab",()=>o.slots.register({name:"settings.plugins.tab",id:"backup",order:35,label:()=>a("tab"),locale:hi,inject:()=>({panel:n})},e))};t.inject(["slots","locale"],o=>{t.inject(["remote"],e=>{t.effect(()=>{let n=null,a=!0,s=!1;return(async()=>{try{n=await e.remote.$mount(Sm)}catch(m){console.error("dsh-backup: backupPanel mount failed:",m),r=!0,i(o,Fl,{mountError:m})}a=!1,s&&n?.()})(),()=>{s=!0,a||n?.()}},"dsh-backup: remote contribution")}),t.inject(["remote.backupPanel"],e=>{let n=()=>e.remote.backupPanel;i(e,gm,{status:async()=>j(await n().status()),backup:async(s,m,d)=>j(await n().backup(s,m,d)),verify:async s=>j(await n().verify(s)),restore:async(s,m,d,p,b)=>j(await n().restore(s,m,d,p,b)),setAuto:async s=>j(await n().setAuto(s)),githubStatus:async()=>j(await n().githubStatus()),githubSyncNow:async()=>j(await n().githubSyncNow()),githubPull:async()=>j(await n().githubPull()),removeEntry:async s=>j(await n().removeEntry(s)),setGithubRepo:async s=>j(await n().setGithubRepo(s)),setGithubToken:async s=>j(await n().setGithubToken(s)),checkUpdate:async()=>j(await n().checkUpdate()),update:async()=>j(await n().update()),exportConfig:async s=>j(await n().exportConfig(s)),importPreview:async s=>j(await n().importPreview(s)),importApply:async s=>j(await n().importApply(s)),syncStatus:async()=>j(await n().syncStatus()),syncNow:async s=>j(await n().syncNow(s)),setSyncChannel:async(s,m)=>j(await n().setSyncChannel(s,m)),removeSyncChannel:async s=>j(await n().removeSyncChannel(s)),lockStatus:async()=>j(await n().lockStatus()),reclaimLock:async s=>j(await n().reclaimLock(s)),diskUsage:async()=>j(await n().diskUsage()),cleanup:async s=>j(await n().cleanup(s)),setWeekly:async s=>j(await n().setWeekly(s)),about:async()=>j(await n().about()),profilesList:async()=>j(await n().profilesList()),profileCreate:async s=>j(await n().profileCreate(s)),profileRename:async(s,m)=>j(await n().profileRename(s,m)),profileDelete:async s=>j(await n().profileDelete(s)),profileLaunch:async s=>j(await n().profileLaunch(s)),profileStop:async s=>j(await n().profileStop(s)),marketList:async()=>j(await n().marketList()),marketSearch:async s=>j(await n().marketSearch(s)),marketInstall:async s=>j(await n().marketInstall(s)),foreignSources:async()=>j(await n().foreignSources()),foreignPreview:async s=>j(await n().foreignPreview(s)),foreignImport:async s=>j(await n().foreignImport(s)),sessionRewindList:async()=>j(await n().sessionRewindList()),sessionRewindPreview:async(s,m)=>j(await n().sessionRewindPreview(s,m)),sessionRewindApply:async s=>j(await n().sessionRewindApply(s)),chatSources:async()=>j(await n().chatSources()),chatPreview:async s=>j(await n().chatPreview(s)),chatApply:async s=>j(await n().chatApply(s))})})})}
|
|
557
557
|
return module.exports; } });
|
package/lib/index.js
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* 插件的自有存储。
|
|
55
55
|
*/
|
|
56
56
|
import { createHash, randomUUID } from 'node:crypto';
|
|
57
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
57
58
|
import { createReadStream, existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
58
59
|
import { spawn } from 'node:child_process';
|
|
59
60
|
import { mkdir, open, copyFile, chmod, readdir, readFile, rename, rm, stat as fsStat, lstat, unlink, writeFile, link } from 'node:fs/promises';
|
|
@@ -66,6 +67,8 @@ import * as zlib from 'node:zlib';
|
|
|
66
67
|
import { decryptFile, encryptFile } from './encrypt.js';
|
|
67
68
|
import { buildZip, listZip, extractZip, readZipEntry } from './zip.js';
|
|
68
69
|
import { gfsProtectedSet } from './gfs.js';
|
|
70
|
+
import { safeRelativePath, safeDestination, backupRootRelative, tarLiteralPattern } from './paths.js';
|
|
71
|
+
import { matchesRedaction } from './redact.js';
|
|
69
72
|
import { acquireLock, releaseLock, heartbeatLock, reclaimLock as reclaimOplock, isLockStale, LOCK_HEARTBEAT_MS } from './oplock.js';
|
|
70
73
|
import { s3Put, webdavPut, gistSync, gistContent } from './sync-channels.js';
|
|
71
74
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
@@ -151,7 +154,7 @@ const EXPORT_SECTIONS = Object.freeze([
|
|
|
151
154
|
{ key: 'settings', label: '设置', paths: ['settings.yaml', 'settings.json'] },
|
|
152
155
|
{ key: 'ui', label: '界面设置', paths: ['ui.json', 'ui-state.json', '.ui'] },
|
|
153
156
|
{ key: 'providers', label: '模型供应商', paths: ['providers'] },
|
|
154
|
-
{ key: 'plugins', label: '插件 profiles', paths: ['profiles'] },
|
|
157
|
+
{ key: 'plugins', label: '插件 profiles', paths: ['profiles', 'local-plugins'] },
|
|
155
158
|
{ key: 'mcp', label: 'MCP 配置', paths: ['cordis.patch.yml', 'cordis.yml'] },
|
|
156
159
|
{ key: 'skills', label: '技能', paths: ['skills'] },
|
|
157
160
|
{ key: 'prompts', label: '提示词', paths: ['prompts'] },
|
|
@@ -1351,7 +1354,33 @@ export function apply(ctx, pluginConfig) {
|
|
|
1351
1354
|
* (rel 本身可含 glob)。
|
|
1352
1355
|
*/
|
|
1353
1356
|
function redactExcludeFlags(base) {
|
|
1354
|
-
return
|
|
1357
|
+
return sensitivePatterns().flatMap((rel) => [`--exclude=${base}/${rel}`, `--exclude=*/${rel}`]);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Old releases left rollback copies beside live credentials. Exclude these too.
|
|
1361
|
+
function sensitivePatterns() {
|
|
1362
|
+
return sensitivePaths().flatMap((rel) => [rel, `${rel}.pre-merge-*`, `${rel}.pre-import-*`]);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function isSensitiveRelative(rel) {
|
|
1366
|
+
return sensitivePatterns().some((pattern) => matchesRedaction(rel, pattern));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function retainBeforeOverwrite(rel, kind, stamp) {
|
|
1370
|
+
const { dshHome, root } = paths();
|
|
1371
|
+
const sensitive = isSensitiveRelative(rel);
|
|
1372
|
+
const keep = sensitive
|
|
1373
|
+
? await safeDestination(`${root}/${VAULT_DIR}/${PRESERVED_DIR}`, `${kind}-${stamp}/${rel}`)
|
|
1374
|
+
: await safeDestination(dshHome, `${rel}.pre-${kind}-${stamp}`);
|
|
1375
|
+
await mkdir(dirname(keep), { recursive: true, mode: 0o700 });
|
|
1376
|
+
// Create exclusively and privately before copying; never follow a retained
|
|
1377
|
+
// path left by an earlier operation or expose plaintext through the umask.
|
|
1378
|
+
const fh = await open(keep, 'wx', 0o600);
|
|
1379
|
+
// copyFile copies the source mode too, which could turn a private rollback
|
|
1380
|
+
// file back into 0644. Stream through the already-private file descriptor.
|
|
1381
|
+
try { await fh.writeFile(createReadStream(`${dshHome}/${rel}`)); }
|
|
1382
|
+
finally { await fh.close(); }
|
|
1383
|
+
return keep;
|
|
1355
1384
|
}
|
|
1356
1385
|
|
|
1357
1386
|
/**
|
|
@@ -1687,26 +1716,30 @@ export function apply(ctx, pluginConfig) {
|
|
|
1687
1716
|
// 备份/恢复/导入/同步/doctor 这类重活会动 ~/.dsh 与备份目录(vault 刷新、
|
|
1688
1717
|
// .dsh 挪旁、归档轮换),多实例/多 profile 并发会互相踩。锁文件在备份目录根
|
|
1689
1718
|
// .oplock.json,记 pid+token+心跳;死锁(pid 已死)下次自动回收,活锁绝不误踢。
|
|
1690
|
-
//
|
|
1719
|
+
// 同一异步操作链可重入(restore 内部的快照 doBackup 等)。
|
|
1691
1720
|
let heldLock = null;
|
|
1692
|
-
|
|
1721
|
+
const operationScope = new AsyncLocalStorage();
|
|
1693
1722
|
|
|
1694
1723
|
async function withOperationLock(operation, fn) {
|
|
1695
|
-
|
|
1724
|
+
// Only descendants of the current operation may reenter (restore -> snapshot).
|
|
1725
|
+
// An unrelated RPC in the same process must still compete for the lock.
|
|
1726
|
+
if (heldLock && operationScope.getStore() === heldLock.token) return fn();
|
|
1696
1727
|
const { root } = paths();
|
|
1697
1728
|
const lockPath = `${root}/${LOCK_FILE}`;
|
|
1698
1729
|
const r = await acquireLock(lockPath, operation);
|
|
1699
1730
|
if (!r.acquired) {
|
|
1700
|
-
throw new Error(`另一操作正在进行(${r.heldBy?.operation ?? '未知'},pid=${r.heldBy?.pid}
|
|
1731
|
+
throw new Error(`另一操作正在进行(${r.heldBy?.operation ?? '未知'},pid=${r.heldBy?.pid ?? '未知'}):${r.reason ?? ''}——请稍后重试;若确认是残留死锁可用 /backup lock reclaim 回收`);
|
|
1701
1732
|
}
|
|
1702
1733
|
heldLock = r.lock;
|
|
1703
|
-
|
|
1734
|
+
let heartbeat = Promise.resolve();
|
|
1735
|
+
const heartbeatTimer = setInterval(() => { heartbeat = heartbeat.then(() => heartbeatLock(lockPath, r.lock.token)).catch(() => {}); }, LOCK_HEARTBEAT_MS);
|
|
1704
1736
|
try {
|
|
1705
|
-
return await fn
|
|
1737
|
+
return await operationScope.run(r.lock.token, fn);
|
|
1706
1738
|
} finally {
|
|
1707
|
-
|
|
1739
|
+
clearInterval(heartbeatTimer);
|
|
1740
|
+
await heartbeat;
|
|
1708
1741
|
await releaseLock(lockPath, r.lock.token).catch(() => {});
|
|
1709
|
-
heldLock = null;
|
|
1742
|
+
if (heldLock?.token === r.lock.token) heldLock = null;
|
|
1710
1743
|
}
|
|
1711
1744
|
}
|
|
1712
1745
|
|
|
@@ -1716,6 +1749,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
1716
1749
|
|
|
1717
1750
|
async function doBackupInner(keep, signal, opts = {}) {
|
|
1718
1751
|
const { home, dshHome, root } = paths();
|
|
1752
|
+
const backupRel = backupRootRelative(dshHome, root);
|
|
1719
1753
|
const keepN = keep && keep > 0 ? Math.floor(keep) : defaultKeep();
|
|
1720
1754
|
// 状态文件先写:writeText 会自动创建备份目录,替代 mkdir -p(Windows 无 mkdir.exe)。
|
|
1721
1755
|
await saveAutoState();
|
|
@@ -1735,10 +1769,11 @@ export function apply(ctx, pluginConfig) {
|
|
|
1735
1769
|
// 不知情的情况下删掉最后一份好副本。检出项记入 meta.quarantined,恢复侧
|
|
1736
1770
|
// 如实提示。体检失败不阻断备份(fail-open)。
|
|
1737
1771
|
let quarantined = [];
|
|
1772
|
+
let scanFailed = false;
|
|
1738
1773
|
if (!opts?.skipScan) {
|
|
1739
1774
|
try {
|
|
1740
1775
|
quarantined = (await runDoctorScan(signal)).corrupt.map((c) => c.rel);
|
|
1741
|
-
} catch {
|
|
1776
|
+
} catch { scanFailed = true; }
|
|
1742
1777
|
}
|
|
1743
1778
|
const quarantineFlags = quarantined.map((rel) => `--exclude=${base}/${rel}`);
|
|
1744
1779
|
|
|
@@ -1761,8 +1796,9 @@ export function apply(ctx, pluginConfig) {
|
|
|
1761
1796
|
const redacted = skipVault ? await existingSensitive(dshHome) : await refreshVault(dshHome, home, signal);
|
|
1762
1797
|
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
1763
1798
|
const redactFlags = isTyped ? [] : redactExcludeFlags(base);
|
|
1799
|
+
const destinationFlags = backupRel === null ? [] : [`--exclude=${tarLiteralPattern(`${base}/${backupRel}`)}`];
|
|
1764
1800
|
try {
|
|
1765
|
-
await spawnRun([tar, '--exclude=*node_modules*', '--exclude=.system', ...quarantineFlags, ...redactFlags, ...extraExcludes(), '-czf', name, '-C', parentTar(parent), ...(isTyped ? typePaths : [base])], root, signal);
|
|
1801
|
+
await spawnRun([tar, '--exclude=*node_modules*', '--exclude=.system', ...destinationFlags, ...quarantineFlags, ...redactFlags, ...extraExcludes(), '-czf', name, '-C', parentTar(parent), ...(isTyped ? typePaths : [base])], root, signal);
|
|
1766
1802
|
} catch (err) {
|
|
1767
1803
|
// tar 失败前已创建 -f 目标——空壳归档留在备份目录会被面板当正常备份
|
|
1768
1804
|
// 列出来(#88:29 字节静默空包)。删掉空壳再抛,让失败可见。
|
|
@@ -1848,7 +1884,8 @@ export function apply(ctx, pluginConfig) {
|
|
|
1848
1884
|
monthly: Number(gfsCfg.gfsMonthly) > 0 ? Math.floor(Number(gfsCfg.gfsMonthly)) : 0,
|
|
1849
1885
|
yearly: Number(gfsCfg.gfsYearly) > 0 ? Math.floor(Number(gfsCfg.gfsYearly)) : 0,
|
|
1850
1886
|
});
|
|
1851
|
-
const
|
|
1887
|
+
const retentionPaused = quarantined.length > 0 || scanFailed;
|
|
1888
|
+
const rotateStale = opts.skipRotate || retentionPaused || SNAPSHOT_PREFIXES.some((p) => name.startsWith(p))
|
|
1852
1889
|
? []
|
|
1853
1890
|
: rotateTail.filter((b) => !tierProtectedSet(rotateTail, rotateBase[0]?.name).has(b.name) && !gfsProtect.has(b.name)).map((b) => b.name);
|
|
1854
1891
|
if (rotateStale.length) await removeFiles(rotateStale.flatMap((n) => sidecarsFor(n)), root, signal);
|
|
@@ -1871,14 +1908,14 @@ export function apply(ctx, pluginConfig) {
|
|
|
1871
1908
|
await saveAutoState();
|
|
1872
1909
|
}
|
|
1873
1910
|
|
|
1874
|
-
return { path: out, name, sha: shaText, total: rotateBase.length, stale: rotateStale.length, keep: keepN, quarantined, sync, ...(encrypted ? { encrypted: true } : {}), ...(isTyped ? { types, hasCredentials } : {}) };
|
|
1911
|
+
return { path: out, name, sha: shaText, total: rotateBase.length, stale: rotateStale.length, keep: keepN, quarantined, retentionPaused, sync, ...(encrypted ? { encrypted: true } : {}), ...(isTyped ? { types, hasCredentials } : {}) };
|
|
1875
1912
|
}
|
|
1876
1913
|
|
|
1877
1914
|
/** 备份回执的隔离提示行:体检发现坏会话文件未入档时如实告知 + 指路修复。 */
|
|
1878
1915
|
function quarantineNote(r) {
|
|
1879
1916
|
return Array.isArray(r?.quarantined) && r.quarantined.length
|
|
1880
|
-
? `\n⚠️ ${r.quarantined.length}
|
|
1881
|
-
: '';
|
|
1917
|
+
? `\n⚠️ ${r.quarantined.length} 个损坏会话文件未入档,已暂停轮换以保留健康副本;/backup doctor --repair 修复后恢复轮换`
|
|
1918
|
+
: r?.retentionPaused ? '\n⚠️ 会话体检未完成,已暂停轮换;体检恢复后再清理旧备份' : '';
|
|
1882
1919
|
}
|
|
1883
1920
|
|
|
1884
1921
|
// ---------- GitHub 同步 ----------
|
|
@@ -2016,7 +2053,8 @@ export function apply(ctx, pluginConfig) {
|
|
|
2016
2053
|
} finally {
|
|
2017
2054
|
await fh.close();
|
|
2018
2055
|
}
|
|
2019
|
-
|
|
2056
|
+
const quotedCreds = `'${creds.replace(/'/g, `'\\''`)}'`;
|
|
2057
|
+
await spawnRun([git, 'config', 'credential.helper', `store --file=${quotedCreds}`], syncDir, signal);
|
|
2020
2058
|
}
|
|
2021
2059
|
// token 文件绝不能进仓库:git add -A 会把它当普通文件提交
|
|
2022
2060
|
await writeOwned(`${syncDir}/.gitignore`, '.git-credentials\n');
|
|
@@ -2030,6 +2068,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
2030
2068
|
* 的 .git-credentials(credential helper),不进进程参数。
|
|
2031
2069
|
*/
|
|
2032
2070
|
async function githubSync(signal) {
|
|
2071
|
+
return withOperationLock('github-sync', () => githubSyncInner(signal));
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
async function githubSyncInner(signal) {
|
|
2033
2075
|
const { root } = paths();
|
|
2034
2076
|
const wt = await ensureSyncWorktree(signal);
|
|
2035
2077
|
if (wt.reason) return { skipped: wt.reason };
|
|
@@ -2106,6 +2148,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
2106
2148
|
* 拉回的归档先做 sha256 校验(边车随仓库走),损坏即报且不落列表。
|
|
2107
2149
|
*/
|
|
2108
2150
|
async function githubPull(signal) {
|
|
2151
|
+
return withOperationLock('github-pull', () => githubPullInner(signal));
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
async function githubPullInner(signal) {
|
|
2109
2155
|
const { root, home } = paths();
|
|
2110
2156
|
const wt = await ensureSyncWorktree(signal);
|
|
2111
2157
|
if (wt.reason) throw new Error(wt.reason);
|
|
@@ -2281,6 +2327,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
2281
2327
|
* error?, skipped?, gistUrl? },凭据绝不进返回。
|
|
2282
2328
|
*/
|
|
2283
2329
|
async function syncNow(channel, signal) {
|
|
2330
|
+
return withOperationLock('sync', () => syncNowInner(channel, signal));
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
async function syncNowInner(channel, signal) {
|
|
2284
2334
|
if (channel === 'github') return githubSync(signal);
|
|
2285
2335
|
const ch = syncChannels[channel];
|
|
2286
2336
|
if (!ch) return { error: `未配置同步通道 ${channel}(/backup sync ${channel} set … 或面板配置)` };
|
|
@@ -2619,9 +2669,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
2619
2669
|
* cwd 为备份目录、-f 用纯文件名(规避 Windows GNU tar 盘符冒号问题),
|
|
2620
2670
|
* 逐行解析 -tvzf 输出并拒绝备份根之外的任何条目——含不安全条目直接抛错。
|
|
2621
2671
|
*/
|
|
2622
|
-
async function safeArchiveEntries(pickedName, signal) {
|
|
2623
|
-
const
|
|
2624
|
-
const
|
|
2672
|
+
async function safeArchiveEntries(pickedName, signal, options = {}) {
|
|
2673
|
+
const env = paths();
|
|
2674
|
+
const root = options.root ?? env.root;
|
|
2675
|
+
const base = options.base ?? env.dshHome.split('/').pop();
|
|
2625
2676
|
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
2626
2677
|
// 清单流式读取(#119):不用有界收集——大归档的清单超过 spill 上限时会被截断,
|
|
2627
2678
|
// 碎片会被误判成"不安全条目"而拒绝整包(详见 streamListingLines 注释)。
|
|
@@ -2646,7 +2697,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
2646
2697
|
// 救援台全链路失效(#112)。硬链接不能跨文件系统、也不能指向目录,目标同为
|
|
2647
2698
|
// 归档内相对路径时解压不会逃出备份根;符号链接(l)、设备等仍一律拒绝。
|
|
2648
2699
|
const plainOk = Boolean(parsed) && Boolean(name) && (parsed.type === '-' || parsed.type === 'd') && insideBase(name);
|
|
2649
|
-
const hardOk = Boolean(parsed) && parsed.type === 'h' && insideBase(name) && insideBase(link);
|
|
2700
|
+
const hardOk = options.allowHardlinks !== false && Boolean(parsed) && parsed.type === 'h' && insideBase(name) && insideBase(link);
|
|
2650
2701
|
if (!plainOk && !hardOk) {
|
|
2651
2702
|
bad.push(parsed ? name : s);
|
|
2652
2703
|
} else {
|
|
@@ -2763,13 +2814,17 @@ export function apply(ctx, pluginConfig) {
|
|
|
2763
2814
|
targetExists, preflight: preflightLines(preflight, profileDirs),
|
|
2764
2815
|
};
|
|
2765
2816
|
}
|
|
2766
|
-
|
|
2817
|
+
const newTargets = [];
|
|
2818
|
+
for (const entry of fileTargets) {
|
|
2819
|
+
const rel = entry.slice(`${base}/`.length);
|
|
2820
|
+
await safeDestination(dshHome, rel);
|
|
2821
|
+
if (!(await existsStat(`${dshHome}/${rel}`))) newTargets.push(rel);
|
|
2822
|
+
}
|
|
2823
|
+
// Validate every target before creating rollback copies or changing data.
|
|
2767
2824
|
const stamp = stampNow();
|
|
2768
2825
|
const kept = [];
|
|
2769
2826
|
for (const w of willOverwrite) {
|
|
2770
|
-
const keep =
|
|
2771
|
-
await mkdir(dirname(keep), { recursive: true });
|
|
2772
|
-
await copyFile(`${dshHome}/${w.rel}`, keep);
|
|
2827
|
+
const keep = await retainBeforeOverwrite(w.rel, 'merge', stamp);
|
|
2773
2828
|
kept.push({ ...w, keep });
|
|
2774
2829
|
}
|
|
2775
2830
|
try {
|
|
@@ -2780,14 +2835,18 @@ export function apply(ctx, pluginConfig) {
|
|
|
2780
2835
|
// 时把留档路径交代清楚,绝不静默停在半截状态。
|
|
2781
2836
|
const reason = err && err.message ? err.message : String(err);
|
|
2782
2837
|
const restored = new Set();
|
|
2783
|
-
for (const w of
|
|
2838
|
+
for (const w of kept) {
|
|
2784
2839
|
try { await copyFile(w.keep, `${dshHome}/${w.rel}`); restored.add(w.rel); } catch { /* 逐个失败落入下方报告 */ }
|
|
2785
2840
|
}
|
|
2786
|
-
|
|
2841
|
+
const cleanupFailed = [];
|
|
2842
|
+
for (const rel of newTargets) {
|
|
2843
|
+
try { await rm(`${dshHome}/${rel}`, { recursive: true, force: true }); } catch { cleanupFailed.push(rel); }
|
|
2844
|
+
}
|
|
2845
|
+
if (restored.size === willOverwrite.length && !cleanupFailed.length) {
|
|
2787
2846
|
throw new Error(`分类型恢复失败(${reason})。已从留档还原全部覆盖现场,数据保持恢复前状态;排查后可重试`);
|
|
2788
2847
|
}
|
|
2789
|
-
const missing =
|
|
2790
|
-
throw new Error(`分类型恢复失败(${reason}),且部分现场未自动还原: ${missing.join(', ')}
|
|
2848
|
+
const missing = kept.filter((w) => !restored.has(w.rel)).map((w) => `${w.rel}(留档: ${w.keep})`);
|
|
2849
|
+
throw new Error(`分类型恢复失败(${reason}),且部分现场未自动还原: ${[...missing, ...cleanupFailed].join(', ')}。请从上述留档手工还原`);
|
|
2791
2850
|
}
|
|
2792
2851
|
// 可选依赖重装:merge 含 profiles 时对新解出的 profile 跑 pnpm install。
|
|
2793
2852
|
const deps = { installed: [], failed: [], note: null };
|
|
@@ -2833,6 +2892,12 @@ export function apply(ctx, pluginConfig) {
|
|
|
2833
2892
|
};
|
|
2834
2893
|
}
|
|
2835
2894
|
|
|
2895
|
+
// Moving DSH_HOME would also move the selected archive, vault and operation
|
|
2896
|
+
// lock. Require relocating the complete backup directory before replacement.
|
|
2897
|
+
if (backupRootRelative(dshHome, root) !== null) {
|
|
2898
|
+
throw new Error('整包恢复前请把整个备份目录(含 vault)移到 DSH_HOME 外,并更新备份路径;当前备份目录位于待替换的数据目录内');
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2836
2901
|
// 恢复前自动快照当前数据,并把当前数据移到旁边(而非合并覆盖)。
|
|
2837
2902
|
// 快照用 dsh-pre-restore- 前缀与用户常规备份区分,经 listBackups 过滤
|
|
2838
2903
|
// 后不进列表/轮换/latest 选择;每次恢复清掉上一次的快照防累积。
|
|
@@ -2943,7 +3008,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
2943
3008
|
'✅ 分类型恢复完成',
|
|
2944
3009
|
` 已合并写入: ${r.restored} 项 ← 来自 ${r.archive.split('/').pop()}(类型: ${types})`,
|
|
2945
3010
|
];
|
|
2946
|
-
if (r.kept?.length) lines.push(` 覆盖的旧文件已留档: ${r.kept.length}
|
|
3011
|
+
if (r.kept?.length) lines.push(` 覆盖的旧文件已留档: ${r.kept.length} 个(敏感文件在本机 vault/preserved,其余为 .pre-merge-*,确认后可自行删除)`);
|
|
2947
3012
|
for (const p of r.preflight ?? []) if (p.startsWith('⚠️')) lines.push(` ${p}`);
|
|
2948
3013
|
if (r.deps?.note) lines.push(` 插件依赖: ${r.deps.note}`);
|
|
2949
3014
|
else if (r.deps?.installed?.length) lines.push(` 插件依赖: 已装好 ${r.deps.installed.join(', ')}`);
|
|
@@ -3030,10 +3095,12 @@ export function apply(ctx, pluginConfig) {
|
|
|
3030
3095
|
* .git/.system(任意深度);凭据节段按 includeCredentials 决定入包或记入脱敏清单。
|
|
3031
3096
|
*/
|
|
3032
3097
|
async function walkExportFiles(dshHome, includeCredentials, signal) {
|
|
3098
|
+
const backupRel = backupRootRelative(dshHome, paths().root);
|
|
3033
3099
|
const files = [];
|
|
3034
3100
|
const redacted = [];
|
|
3035
3101
|
const skipDirs = new Set(['node_modules', '.git', '.system']);
|
|
3036
3102
|
async function walk(abs, rel) {
|
|
3103
|
+
if (rel === backupRel) return;
|
|
3037
3104
|
if (signal?.aborted) throw new Error('操作已取消');
|
|
3038
3105
|
if (files.length >= EXPORT_MAX_FILES) throw new Error(`导出文件数超过上限(${EXPORT_MAX_FILES}),请检查 ~/.dsh 是否有异常目录树`);
|
|
3039
3106
|
let st;
|
|
@@ -3051,7 +3118,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
3051
3118
|
}
|
|
3052
3119
|
if (!st.isFile()) return;
|
|
3053
3120
|
const section = sectionKeyForTop(rel.split('/')[0]);
|
|
3054
|
-
if (section === 'credentials'
|
|
3121
|
+
if (!includeCredentials && (section === 'credentials' || isSensitiveRelative(rel))) { redacted.push(rel); return; }
|
|
3055
3122
|
files.push({ rel, abs, bytes: st.size, section });
|
|
3056
3123
|
}
|
|
3057
3124
|
await walk(dshHome, '');
|
|
@@ -3076,6 +3143,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
3076
3143
|
|
|
3077
3144
|
/** 导出:把 ~/.dsh 打成单文件 ZIP(可选加密)。 */
|
|
3078
3145
|
async function exportConfig(opts, signal) {
|
|
3146
|
+
return withOperationLock('export', () => exportConfigInner(opts, signal));
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
async function exportConfigInner(opts, signal) {
|
|
3079
3150
|
const { home, dshHome, root } = paths();
|
|
3080
3151
|
const includeCredentials = opts?.includeCredentials === true;
|
|
3081
3152
|
const password = typeof opts?.password === 'string' && opts.password ? opts.password : null;
|
|
@@ -3093,9 +3164,11 @@ export function apply(ctx, pluginConfig) {
|
|
|
3093
3164
|
// 本地 link:/file: 插件迁移:npm pack 成 tarball 打进导出,导入侧解包改 file:
|
|
3094
3165
|
const localPlugins = await scanLocalPlugins(signal);
|
|
3095
3166
|
const packedPlugins = localPlugins.filter((p) => p.status === 'packed');
|
|
3167
|
+
// Different profiles may reference distinct packages with the same npm name/version.
|
|
3168
|
+
for (const p of packedPlugins) p.exportName = `${randomUUID()}-${p.tarballName}`;
|
|
3096
3169
|
const encrypted = Boolean(password);
|
|
3097
3170
|
const manifest = buildManifest(home, encrypted, redacted, sectionStats);
|
|
3098
|
-
manifest.localPlugins = localPlugins.map((p) => ({ profile: p.profile, name: p.name, spec: p.spec, status: p.status, tarball: p.
|
|
3171
|
+
manifest.localPlugins = localPlugins.map((p) => ({ profile: p.profile, name: p.name, spec: p.spec, status: p.status, tarball: p.exportName ?? null, warning: p.warning ?? null }));
|
|
3099
3172
|
const finalName = `dsh-config-${stampNow()}.zip`;
|
|
3100
3173
|
const finalPath = `${root}/${finalName}`;
|
|
3101
3174
|
const tmpPath = `${root}/.export-${stampNow()}.tmp.zip`;
|
|
@@ -3105,7 +3178,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
3105
3178
|
try {
|
|
3106
3179
|
const entryList = [{ name: 'manifest.json', data: Buffer.from(JSON.stringify(manifest)) }];
|
|
3107
3180
|
for (const f of files) entryList.push({ name: f.rel, sourcePath: f.abs });
|
|
3108
|
-
for (const p of packedPlugins) entryList.push({ name: `local-plugins/${p.
|
|
3181
|
+
for (const p of packedPlugins) entryList.push({ name: `local-plugins/${p.exportName}`, sourcePath: p.tarballPath });
|
|
3109
3182
|
await buildZip(tmpPath, entryList, { signal });
|
|
3110
3183
|
if (encrypted) {
|
|
3111
3184
|
const tmpEnc = `${finalPath}.enc-${stampNow()}.tmp`;
|
|
@@ -4100,6 +4173,28 @@ export function apply(ctx, pluginConfig) {
|
|
|
4100
4173
|
if (Number(manifest?.version) > EXPORT_FORMAT_VERSION) {
|
|
4101
4174
|
throw new Error(`导出包版本过新(version=${manifest.version},本机支持 ${EXPORT_FORMAT_VERSION})——请升级插件后导入`);
|
|
4102
4175
|
}
|
|
4176
|
+
if (manifest.localPlugins !== undefined) {
|
|
4177
|
+
if (!Array.isArray(manifest.localPlugins)) throw new Error('manifest.localPlugins 必须是数组');
|
|
4178
|
+
const seen = new Set();
|
|
4179
|
+
manifest.localPlugins = manifest.localPlugins.map((p) => {
|
|
4180
|
+
if (!p || typeof p !== 'object') throw new Error('本地插件元数据无效');
|
|
4181
|
+
safeRelativePath(p.name);
|
|
4182
|
+
if (!/^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/.test(p.name)) throw new Error('本地插件名称无效');
|
|
4183
|
+
safeRelativePath(p.profile);
|
|
4184
|
+
if (!/^[A-Za-z0-9._-]+$/.test(p.profile)) throw new Error('本地插件 profile 无效');
|
|
4185
|
+
const key = `${p.profile}/${p.name}`;
|
|
4186
|
+
if (seen.has(key)) throw new Error(`重复的本地插件: ${key}`);
|
|
4187
|
+
seen.add(key);
|
|
4188
|
+
// Canonical export field is tarball; accept the older import-side spelling too.
|
|
4189
|
+
const tarball = p.tarball ?? p.tarballName;
|
|
4190
|
+
if (p.tarball && p.tarballName && p.tarball !== p.tarballName) throw new Error('本地插件 tarball 字段冲突');
|
|
4191
|
+
if (p.status === 'packed') {
|
|
4192
|
+
safeRelativePath(tarball);
|
|
4193
|
+
if (tarball.includes('/') || !tarball.endsWith('.tgz')) throw new Error('本地插件 tarball 路径无效');
|
|
4194
|
+
}
|
|
4195
|
+
return { ...p, tarball };
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4103
4198
|
return manifest;
|
|
4104
4199
|
}
|
|
4105
4200
|
|
|
@@ -4212,7 +4307,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
4212
4307
|
const willWrite = [];
|
|
4213
4308
|
const willSkip = [];
|
|
4214
4309
|
for (const e of selected) {
|
|
4215
|
-
const targetAbs =
|
|
4310
|
+
const targetAbs = await safeDestination(dshHome, e.name);
|
|
4216
4311
|
const exists = await existsStat(targetAbs);
|
|
4217
4312
|
if (exists && decisions[e.name] !== 'overwrite') { willSkip.push(e.name); continue; }
|
|
4218
4313
|
willWrite.push({ ...e, targetAbs, conflict: exists });
|
|
@@ -4240,73 +4335,75 @@ export function apply(ctx, pluginConfig) {
|
|
|
4240
4335
|
await prunePrefixedSnapshots(IMPORT_SNAPSHOT_PREFIX, 1, snap.name);
|
|
4241
4336
|
} catch { /* 快照失败不阻断导入 */ }
|
|
4242
4337
|
|
|
4243
|
-
|
|
4338
|
+
|
|
4244
4339
|
// 解压到暂存目录(extractZip 内 Zip Slip/符号链接/zip bomb/CRC 全套防线),再拷入 ~/.dsh
|
|
4245
|
-
staging = `${root}/.import-stage-${
|
|
4340
|
+
staging = `${root}/.import-stage-${randomUUID()}`;
|
|
4246
4341
|
await extractZip(zipPath, staging, { names: new Set(willWrite.map((e) => e.name)), signal });
|
|
4247
4342
|
const stamp = stampNow();
|
|
4248
4343
|
const backups = [];
|
|
4249
4344
|
const applied = [];
|
|
4250
4345
|
const overwrote = [];
|
|
4346
|
+
const createdPluginDirs = [];
|
|
4347
|
+
let remapped = 0;
|
|
4348
|
+
let pluginsRestored = { restored: [], skipped: [] };
|
|
4251
4349
|
try {
|
|
4252
4350
|
for (const e of willWrite) {
|
|
4253
4351
|
if (signal?.aborted) throw new Error('操作已取消');
|
|
4352
|
+
await safeDestination(dshHome, e.name);
|
|
4254
4353
|
await mkdir(dirname(e.targetAbs), { recursive: true });
|
|
4255
4354
|
if (e.conflict) {
|
|
4256
|
-
const keep =
|
|
4257
|
-
await copyFile(e.targetAbs, keep);
|
|
4355
|
+
const keep = await retainBeforeOverwrite(e.name, 'import', stamp);
|
|
4258
4356
|
backups.push({ name: e.name, targetAbs: e.targetAbs, keep });
|
|
4259
4357
|
}
|
|
4260
|
-
|
|
4358
|
+
// Record before copyFile: even a failed copy may leave partial new bytes.
|
|
4261
4359
|
applied.push(e.name);
|
|
4360
|
+
await copyFile(`${staging}/${e.name}`, e.targetAbs);
|
|
4262
4361
|
if (e.conflict) overwrote.push(e.name);
|
|
4263
4362
|
}
|
|
4363
|
+
// 校验:写入的文件必须存在(防半截写入被误判成功)
|
|
4364
|
+
const missing = [];
|
|
4365
|
+
for (const e of willWrite) if (!(await existsStat(e.targetAbs))) missing.push(e.name);
|
|
4366
|
+
if (missing.length) throw new Error(`导入校验失败,部分文件未落盘: ${missing.slice(0, 5).join(', ')}(快照: ${snapshotName ?? '未生成'})`);
|
|
4367
|
+
|
|
4368
|
+
// 跨机 remap:对写入的文本配置做 from→to 前缀替换(settings / cordis)
|
|
4369
|
+
if (Array.isArray(opts?.remap) && opts.remap.length) {
|
|
4370
|
+
for (const m of opts.remap) {
|
|
4371
|
+
if (!m || typeof m.from !== 'string' || !m.from || typeof m.to !== 'string') continue;
|
|
4372
|
+
for (const rel of ['settings.yaml', 'settings.json', 'cordis.yml', 'cordis.patch.yml']) {
|
|
4373
|
+
if (!willWrite.some((e) => e.name === rel)) continue;
|
|
4374
|
+
const p = `${dshHome}/${rel}`;
|
|
4375
|
+
if (!(await existsStat(p))) continue;
|
|
4376
|
+
const text = await readFile(p, 'utf8');
|
|
4377
|
+
const next = text.split(m.from).join(m.to);
|
|
4378
|
+
if (next !== text) { await writeFile(p, next, 'utf8'); remapped += 1; }
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// Only selected, actually written tarballs and profile manifests may be changed.
|
|
4384
|
+
pluginsRestored = await restoreLocalPlugins(manifest.localPlugins, signal, { willWrite, createdPluginDirs });
|
|
4264
4385
|
} catch (err) {
|
|
4265
4386
|
// 回滚:覆盖的先还原留档,新增的删除;回滚再失败则给出快照/留档指引,绝不静默停在半截。
|
|
4266
4387
|
const failed = [];
|
|
4388
|
+
for (const dir of createdPluginDirs.reverse()) {
|
|
4389
|
+
try { await rm(dir, { recursive: true, force: true }); } catch { failed.push(dir); }
|
|
4390
|
+
}
|
|
4267
4391
|
for (const b of backups) {
|
|
4268
|
-
try { await copyFile(b.keep, b.targetAbs); } catch { failed.push(b.name); }
|
|
4392
|
+
try { await copyFile(b.keep, b.targetAbs); } catch { failed.push(`${b.name}(留档: ${b.keep})`); }
|
|
4269
4393
|
}
|
|
4270
4394
|
for (const n of applied) {
|
|
4271
|
-
if (!backups.some((b) => b.name === n))
|
|
4395
|
+
if (!backups.some((b) => b.name === n)) {
|
|
4396
|
+
try { await rm(`${dshHome}/${n}`, { force: true }); } catch { failed.push(n); }
|
|
4397
|
+
}
|
|
4272
4398
|
}
|
|
4273
4399
|
const snapNote = snapshotName ? `\n已自动快照: ${snapshotName},可 /backup restore ${snapshotName} 整体回滚。` : '';
|
|
4274
4400
|
if (failed.length) {
|
|
4275
|
-
throw new Error(`导入失败并已尽力回滚:${friendlyReason(err)}。部分留档未还原: ${failed.join(', ')}
|
|
4401
|
+
throw new Error(`导入失败并已尽力回滚:${friendlyReason(err)}。部分留档未还原: ${failed.join(', ')}。${snapNote}`);
|
|
4276
4402
|
}
|
|
4277
4403
|
throw new Error(`导入失败,已自动回滚到导入前状态:${friendlyReason(err)}。${snapNote}`);
|
|
4278
4404
|
}
|
|
4279
4405
|
|
|
4280
|
-
|
|
4281
|
-
const missing = [];
|
|
4282
|
-
for (const e of willWrite) if (!(await existsStat(e.targetAbs))) missing.push(e.name);
|
|
4283
|
-
if (missing.length) throw new Error(`导入校验失败,部分文件未落盘: ${missing.slice(0, 5).join(', ')}(快照: ${snapshotName ?? '未生成'})`);
|
|
4284
|
-
|
|
4285
|
-
// 跨机 remap:对写入的文本配置做 from→to 前缀替换(settings / cordis)
|
|
4286
|
-
let remapped = 0;
|
|
4287
|
-
if (Array.isArray(opts?.remap) && opts.remap.length) {
|
|
4288
|
-
for (const m of opts.remap) {
|
|
4289
|
-
if (!m || typeof m.from !== 'string' || !m.from || typeof m.to !== 'string') continue;
|
|
4290
|
-
for (const rel of ['settings.yaml', 'settings.json', 'cordis.yml', 'cordis.patch.yml']) {
|
|
4291
|
-
if (!willWrite.some((e) => e.name === rel)) continue;
|
|
4292
|
-
const p = `${dshHome}/${rel}`;
|
|
4293
|
-
if (!(await existsStat(p))) continue;
|
|
4294
|
-
const text = await readFile(p, 'utf8');
|
|
4295
|
-
const next = text.split(m.from).join(m.to);
|
|
4296
|
-
if (next !== text) { await writeFile(p, next, 'utf8'); remapped += 1; }
|
|
4297
|
-
}
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
|
|
4301
|
-
// 本地 link:/file: 插件迁移:解包 local-plugins tarball 到 ~/.dsh/local-plugins/ 并改回 file:
|
|
4302
|
-
const localPluginEntries = entries.filter((e) => !e.isDirectory && e.name.startsWith('local-plugins/'));
|
|
4303
|
-
let pluginsRestored = { restored: [], skipped: [] };
|
|
4304
|
-
if (localPluginEntries.length && Array.isArray(manifest?.localPlugins)) {
|
|
4305
|
-
await extractZip(zipPath, dshHome, { names: new Set(localPluginEntries.map((e) => e.name)), signal });
|
|
4306
|
-
pluginsRestored = await restoreLocalPlugins(manifest.localPlugins, signal);
|
|
4307
|
-
}
|
|
4308
|
-
|
|
4309
|
-
const summary = `✅ 导入完成\n 已写入: ${applied.length} 个文件(覆盖 ${overwrote.length}、新增 ${applied.length - overwrote.length}、跳过已存在 ${willSkip.length})${remapped ? `\n 已 remap: ${remapped} 个配置文件` : ''}${pluginsRestored.restored.length ? `\n 本地插件已装回 file:: ${pluginsRestored.restored.map((p) => p.name).join(', ')}` : ''}${pluginsRestored.skipped.length ? `\n ⚠️ 本地插件需手动处理: ${pluginsRestored.skipped.map((p) => `${p.name}(${p.reason})`).join(', ')}` : ''}${snapshotName ? `\n 导入前快照: ${snapshotName}` : '\n ⚠️ 导入前快照未生成(建议确认无误后手动备份一次)'}${backups.length ? `\n 覆盖的旧文件已留档 *.pre-import-${stamp}(确认后可删)` : ''}\n ⏸️ 待办: 重启 dsh 生效`;
|
|
4406
|
+
const summary = `✅ 导入完成\n 已写入: ${applied.length} 个文件(覆盖 ${overwrote.length}、新增 ${applied.length - overwrote.length}、跳过已存在 ${willSkip.length})${remapped ? `\n 已 remap: ${remapped} 个配置文件` : ''}${pluginsRestored.restored.length ? `\n 本地插件已装回 file:: ${pluginsRestored.restored.map((p) => p.name).join(', ')}` : ''}${pluginsRestored.skipped.length ? `\n ⚠️ 本地插件需手动处理: ${pluginsRestored.skipped.map((p) => `${p.name}(${p.reason})`).join(', ')}` : ''}${snapshotName ? `\n 导入前快照: ${snapshotName}` : '\n ⚠️ 导入前快照未生成(建议确认无误后手动备份一次)'}${backups.length ? `\n 覆盖的旧文件已留档(敏感文件在本机 vault/preserved,其余在原目录旁;确认后可删)` : ''}\n ⏸️ 待办: 重启 dsh 生效`;
|
|
4310
4407
|
return { ok: true, encrypted, applied: applied.length, skipped: willSkip.length, overwrote: overwrote.length, snapshot: snapshotName, remapped, pluginsRestored, summary };
|
|
4311
4408
|
} finally {
|
|
4312
4409
|
if (staging) await rm(staging, { recursive: true, force: true }).catch(() => {});
|
|
@@ -5342,6 +5439,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
5342
5439
|
|
|
5343
5440
|
/** 分级清理:'cache' 只删可再生缓存/暂存;'expired' 再删过期备份(快照/同步/vault 永不删)。 */
|
|
5344
5441
|
async function cleanup(scope, signal) {
|
|
5442
|
+
return withOperationLock('cleanup', () => cleanupInner(scope, signal));
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
async function cleanupInner(scope, signal) {
|
|
5345
5446
|
const { root } = paths();
|
|
5346
5447
|
const removed = [];
|
|
5347
5448
|
const rmdir = (dir) => rm(`${root}/${dir}`, { recursive: true, force: true });
|
|
@@ -5356,6 +5457,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
5356
5457
|
}
|
|
5357
5458
|
}
|
|
5358
5459
|
if (scope === 'expired') {
|
|
5460
|
+
const scan = await runDoctorScan(signal);
|
|
5461
|
+
if (scan.corruptCount) {
|
|
5462
|
+
return { ok: true, removed: removed.length, names: removed, summary: `检测到 ${scan.corruptCount} 个损坏会话,已暂停过期备份清理,保留健康副本;请先修复会话` };
|
|
5463
|
+
}
|
|
5359
5464
|
// 过期备份:keep 窗口外且不在日/周/月/年分层保护内的全量归档才删
|
|
5360
5465
|
const keepN = defaultKeep();
|
|
5361
5466
|
const all = await listBackups();
|
|
@@ -5408,8 +5513,9 @@ export function apply(ctx, pluginConfig) {
|
|
|
5408
5513
|
/** 校验 profile 名(拒绝空/路径段/非法字符/保留名)。 */
|
|
5409
5514
|
function validateProfileName(name) {
|
|
5410
5515
|
if (typeof name !== 'string' || !name.trim()) return 'profile 名不能为空';
|
|
5411
|
-
if (!/^[A-Za-z0-9._-]+$/.test(name)) return `profile 名只能含字母数字 . _
|
|
5412
|
-
|
|
5516
|
+
if (name === '.' || name === '..' || !/^[A-Za-z0-9._-]+$/.test(name)) return `profile 名只能含字母数字 . _ -,且不能是 . 或 ..(收到 ${name})`;
|
|
5517
|
+
try { safeRelativePath(name); } catch { return `不安全的 profile 名: ${name}`; }
|
|
5518
|
+
if (APP_OWNED_PROFILES.has(name.toLowerCase())) return `profile ${name} 是应用内置 profile,只读不可改`;
|
|
5413
5519
|
return null;
|
|
5414
5520
|
}
|
|
5415
5521
|
|
|
@@ -5568,32 +5674,54 @@ export function apply(ctx, pluginConfig) {
|
|
|
5568
5674
|
return result;
|
|
5569
5675
|
}
|
|
5570
5676
|
|
|
5571
|
-
/**
|
|
5572
|
-
async function restoreLocalPlugins(localPlugins, signal) {
|
|
5573
|
-
const { dshHome
|
|
5677
|
+
/** 导入侧:解包到本机独立目录并把选中的 profile 依赖改回 file:。 */
|
|
5678
|
+
async function restoreLocalPlugins(localPlugins, signal, { willWrite, createdPluginDirs }) {
|
|
5679
|
+
const { dshHome } = paths();
|
|
5574
5680
|
const restored = [];
|
|
5575
5681
|
const skipped = [];
|
|
5682
|
+
const written = new Set(willWrite.map((e) => e.name));
|
|
5576
5683
|
for (const p of Array.isArray(localPlugins) ? localPlugins : []) {
|
|
5577
|
-
if (p.status !== 'packed'
|
|
5578
|
-
|
|
5579
|
-
const
|
|
5580
|
-
if (!(
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
const
|
|
5684
|
+
if (p.status !== 'packed') { skipped.push({ name: p.name, reason: p.warning || p.status }); continue; }
|
|
5685
|
+
const tarRel = `local-plugins/${p.tarball}`;
|
|
5686
|
+
const pkgRel = `profiles/${p.profile}/package.json`;
|
|
5687
|
+
if (!written.has(tarRel) || !written.has(pkgRel)) {
|
|
5688
|
+
skipped.push({ name: p.name, reason: '插件包或 profile 未选中,或已选择保留本机' });
|
|
5689
|
+
continue;
|
|
5690
|
+
}
|
|
5691
|
+
const src = await safeDestination(dshHome, tarRel);
|
|
5692
|
+
const pkgPath = await safeDestination(dshHome, pkgRel);
|
|
5693
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
5694
|
+
const fields = ['dependencies', 'devDependencies'].filter((field) =>
|
|
5695
|
+
pkg[field] && Object.hasOwn(pkg[field], p.name) && /^(?:link|file):/.test(pkg[field][p.name]));
|
|
5696
|
+
if (!fields.length) { skipped.push({ name: p.name, reason: 'profile 没有对应的本地插件依赖' }); continue; }
|
|
5697
|
+
// npm pack always wraps files in package/. Reject traversal, links, and special
|
|
5698
|
+
// entries before extracting into a fresh staging directory, never into live data.
|
|
5699
|
+
await safeArchiveEntries(basename(src), signal, { root: dirname(src), base: 'package', allowHardlinks: false });
|
|
5700
|
+
const installRel = `local-plugins/installed-${randomUUID()}`;
|
|
5701
|
+
const installRoot = await safeDestination(dshHome, installRel);
|
|
5702
|
+
await mkdir(dirname(installRoot), { recursive: true });
|
|
5703
|
+
await mkdir(installRoot, { mode: 0o700 });
|
|
5704
|
+
createdPluginDirs.push(installRoot);
|
|
5705
|
+
// Stage on the target filesystem so external-disk backup destinations do
|
|
5706
|
+
// not make the final rename fail with EXDEV.
|
|
5707
|
+
const stage = `${installRoot}/.unpack`;
|
|
5708
|
+
await mkdir(stage, { mode: 0o700 });
|
|
5587
5709
|
try {
|
|
5588
|
-
const
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5710
|
+
const tar = await ctx.subprocess.resolveExecutable('tar');
|
|
5711
|
+
await spawnRun([tar, '-xzf', basename(src), '-C', stage], dirname(src), signal);
|
|
5712
|
+
const packageJson = await safeDestination(stage, 'package/package.json');
|
|
5713
|
+
const packed = JSON.parse(await readFile(packageJson, 'utf8'));
|
|
5714
|
+
if (!packed || typeof packed !== 'object' || Array.isArray(packed)) throw new Error('本地插件 package.json 无效');
|
|
5715
|
+
const targetDir = await safeDestination(installRoot, p.name);
|
|
5716
|
+
await mkdir(dirname(targetDir), { recursive: true });
|
|
5717
|
+
// Point file: at the actual package root, not the tar's parent directory.
|
|
5718
|
+
await rename(`${stage}/package`, targetDir);
|
|
5719
|
+
for (const field of fields) pkg[field][p.name] = `file:${targetDir}`;
|
|
5594
5720
|
await writeOwned(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
5595
|
-
|
|
5596
|
-
|
|
5721
|
+
restored.push({ name: p.name, profile: p.profile, dir: targetDir });
|
|
5722
|
+
} finally {
|
|
5723
|
+
await rm(stage, { recursive: true, force: true }).catch(() => {});
|
|
5724
|
+
}
|
|
5597
5725
|
}
|
|
5598
5726
|
return { restored, skipped };
|
|
5599
5727
|
}
|
package/lib/oplock.js
CHANGED
|
@@ -31,7 +31,7 @@ export function isPidAlive(pid) {
|
|
|
31
31
|
process.kill(pid, 0);
|
|
32
32
|
return true;
|
|
33
33
|
} catch (err) {
|
|
34
|
-
return err
|
|
34
|
+
return err?.code !== 'ESRCH'; // Only a confirmed missing process may be reclaimed
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -53,71 +53,95 @@ async function readLockFile(lockPath) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* A short filesystem transaction serializes all lock mutations across processes.
|
|
58
|
+
* mkdir is exclusive; atomic rename of JSON alone is not mutual exclusion.
|
|
59
|
+
* An unreadable/orphaned guard fails closed (never steal a potentially live writer).
|
|
60
|
+
* The owner record identifies the process for manual recovery after an interrupted
|
|
61
|
+
* metadata transaction. Normal crash recovery uses the main lock's dead PID.
|
|
62
|
+
*/
|
|
63
|
+
export async function withLockGuard(lockPath, fn, { wait = false } = {}) {
|
|
64
|
+
const guard = `${lockPath}.guard`;
|
|
57
65
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
for (let attempt = 0; ; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
await mkdir(guard, { mode: 0o700 });
|
|
69
|
+
break;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err.code !== 'EEXIST') throw err;
|
|
72
|
+
if (!wait || attempt >= 100) return { busy: true, guard };
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const fh = await open(`${guard}/owner.json`, 'wx', 0o600);
|
|
78
|
+
try { await fh.writeFile(JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() })); }
|
|
79
|
+
finally { await fh.close(); }
|
|
80
|
+
return { value: await fn() };
|
|
81
|
+
} finally {
|
|
82
|
+
await rm(guard, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function writeLockFile(lockPath, lock) {
|
|
66
87
|
const tmp = `${lockPath}.${randomUUID()}.tmp`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
try {
|
|
89
|
+
const fh = await open(tmp, 'wx', 0o600);
|
|
90
|
+
try { await fh.writeFile(`${JSON.stringify(lock)}\n`, 'utf8'); }
|
|
91
|
+
finally { await fh.close(); }
|
|
92
|
+
await rename(tmp, lockPath);
|
|
93
|
+
} finally {
|
|
94
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
95
|
+
}
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* acquired=true 时 lock 是本次写下的锁(含 token,供 release/heartbeat 使用)。
|
|
76
|
-
*/
|
|
98
|
+
const heldBy = (lock) => ({ pid: lock.pid, operation: lock.operation, startedAt: lock.startedAt, heartbeatAt: lock.heartbeatAt });
|
|
99
|
+
const busyReason = (guard) => `锁元数据正在更新(${guard});若进程异常退出,请先确认 guard/owner.json 中的进程已结束再清理该目录`;
|
|
100
|
+
|
|
77
101
|
export async function acquireLock(lockPath, operation) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
const result = await withLockGuard(lockPath, async () => {
|
|
103
|
+
const existing = await readLockFile(lockPath);
|
|
104
|
+
if (existing && (existing.unreadable || !isLockStale(existing))) {
|
|
105
|
+
return { acquired: false, reason: '另一操作正在运行', heldBy: heldBy(existing) };
|
|
106
|
+
}
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
const lock = { pid: process.pid, token: randomUUID(), startedAt: now, heartbeatAt: now, operation: operation || 'unknown' };
|
|
109
|
+
await writeLockFile(lockPath, lock);
|
|
110
|
+
return { acquired: true, lock };
|
|
111
|
+
});
|
|
112
|
+
return result.busy ? { acquired: false, reason: busyReason(result.guard) } : result.value;
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
/** 刷新心跳(仅当 token 匹配、且 pid 仍是本进程时写)。 */
|
|
90
115
|
export async function heartbeatLock(lockPath, token) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
116
|
+
const result = await withLockGuard(lockPath, async () => {
|
|
117
|
+
const existing = await readLockFile(lockPath);
|
|
118
|
+
if (!existing || existing.token !== token || existing.pid !== process.pid) return false;
|
|
119
|
+
await writeLockFile(lockPath, { ...existing, heartbeatAt: new Date().toISOString() });
|
|
120
|
+
return true;
|
|
121
|
+
}, { wait: true });
|
|
122
|
+
return result.busy ? false : result.value;
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
/** 释放锁(仅 token 匹配才删,绝不删别人的锁)。 */
|
|
101
125
|
export async function releaseLock(lockPath, token) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
const result = await withLockGuard(lockPath, async () => {
|
|
127
|
+
const existing = await readLockFile(lockPath);
|
|
128
|
+
if (!existing || existing.token !== token) return false;
|
|
129
|
+
await rm(lockPath, { force: true });
|
|
105
130
|
return true;
|
|
106
|
-
}
|
|
107
|
-
|
|
131
|
+
}, { wait: true });
|
|
132
|
+
if (result.busy) throw new Error(busyReason(result.guard));
|
|
133
|
+
return result.value;
|
|
108
134
|
}
|
|
109
135
|
|
|
110
|
-
/**
|
|
111
|
-
* 回收入口:死锁直接回收;活锁默认拒绝,force=true 时按用户显式确认强删。
|
|
112
|
-
* @returns {Promise<{reclaimed:boolean, reason?:string, heldBy?:object}>}
|
|
113
|
-
*/
|
|
114
136
|
export async function reclaimLock(lockPath, { force = false } = {}) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
const result = await withLockGuard(lockPath, async () => {
|
|
138
|
+
const existing = await readLockFile(lockPath);
|
|
139
|
+
if (!existing) return { reclaimed: false, reason: '没有锁' };
|
|
140
|
+
if (isLockStale(existing) || force) {
|
|
141
|
+
await rm(lockPath, { force: true });
|
|
142
|
+
return { reclaimed: true, heldBy: heldBy(existing) };
|
|
143
|
+
}
|
|
144
|
+
return { reclaimed: false, reason: '锁持有进程仍在运行(未回收,避免误踢活锁)', heldBy: heldBy(existing) };
|
|
145
|
+
});
|
|
146
|
+
return result.busy ? { reclaimed: false, reason: busyReason(result.guard) } : result.value;
|
|
123
147
|
}
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Resolve existing ancestors too: a destination may not exist yet or may be
|
|
6
|
+
// reached through a symlink outside DSH_HOME.
|
|
7
|
+
function canonicalPath(value) {
|
|
8
|
+
const abs = resolve(value);
|
|
9
|
+
try { return realpathSync(abs); } catch (err) {
|
|
10
|
+
if (err.code !== 'ENOENT' || dirname(abs) === abs) throw err;
|
|
11
|
+
return resolve(canonicalPath(dirname(abs)), basename(abs));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Relative backup subtree to exclude, or null for an external destination. */
|
|
16
|
+
export function backupRootRelative(dshHome, root) {
|
|
17
|
+
const rel = relative(canonicalPath(dshHome), canonicalPath(root));
|
|
18
|
+
if (!rel) throw new Error('备份目录不能等于 DSH_HOME,请选择独立的备份目录');
|
|
19
|
+
return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel) ? null : rel.split(sep).join('/');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** tar --exclude is a glob, even when the user configured a literal directory. */
|
|
23
|
+
export function tarLiteralPattern(value) {
|
|
24
|
+
return value.replace(/[\\*?\[\]]/g, '\\$&');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Portable relative paths only; metadata is untrusted even when ZIP names are safe. */
|
|
28
|
+
export function safeRelativePath(value) {
|
|
29
|
+
if (typeof value !== 'string' || !value || /[\\\u0000-\u001f\u007f:]/.test(value)
|
|
30
|
+
|| value.startsWith('/') || value.split('/').some((part) => !part || part === '.' || part === '..'
|
|
31
|
+
|| /[. ]$/.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
|
|
32
|
+
throw new Error(`不安全的相对路径: ${String(value)}`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Reject existing symlinks as well as lexical traversal before writing into a tree. */
|
|
38
|
+
export async function safeDestination(root, rel) {
|
|
39
|
+
safeRelativePath(rel);
|
|
40
|
+
const base = resolve(root);
|
|
41
|
+
const target = resolve(base, rel);
|
|
42
|
+
const inside = relative(base, target);
|
|
43
|
+
if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)) {
|
|
44
|
+
throw new Error(`写入路径越界: ${rel}`);
|
|
45
|
+
}
|
|
46
|
+
let current = base;
|
|
47
|
+
for (const part of ['', ...rel.split('/')]) {
|
|
48
|
+
if (part) current = resolve(current, part);
|
|
49
|
+
const info = await lstat(current).catch((err) => {
|
|
50
|
+
if (err.code === 'ENOENT') return null;
|
|
51
|
+
throw err;
|
|
52
|
+
});
|
|
53
|
+
if (info?.isSymbolicLink()) throw new Error(`写入路径含符号链接: ${rel}`);
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
56
|
+
}
|
package/lib/redact.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const literal = (c) => c.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
2
|
+
|
|
3
|
+
// tar excludes are unanchored globs and '*' may cross directory separators.
|
|
4
|
+
// POSIX character classes depend on the tar process locale; conservatively match
|
|
5
|
+
// any character for those classes instead of risking a plaintext ZIP disclosure.
|
|
6
|
+
export function matchesRedaction(rel, pattern) {
|
|
7
|
+
let source = '';
|
|
8
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
9
|
+
const c = pattern[i];
|
|
10
|
+
if (c === '*') source += '[\\s\\S]*';
|
|
11
|
+
else if (c === '?') source += '[\\s\\S]';
|
|
12
|
+
else if (c === '\\' && i + 1 < pattern.length) source += literal(pattern[++i]);
|
|
13
|
+
else if (c === '[') {
|
|
14
|
+
let end = i + 1;
|
|
15
|
+
if (pattern[end] === '!' || pattern[end] === '^') end++;
|
|
16
|
+
if (pattern[end] === ']') end++;
|
|
17
|
+
while (end < pattern.length && pattern[end] !== ']') {
|
|
18
|
+
if (pattern[end] === '[' && ':=.'.includes(pattern[end + 1] ?? ' ')) {
|
|
19
|
+
const innerEnd = pattern.indexOf(`${pattern[end + 1]}]`, end + 2);
|
|
20
|
+
if (innerEnd !== -1) { end = innerEnd + 2; continue; }
|
|
21
|
+
}
|
|
22
|
+
end++;
|
|
23
|
+
}
|
|
24
|
+
if (end === pattern.length) { source += '\\['; continue; }
|
|
25
|
+
const body = pattern.slice(i + 1, end);
|
|
26
|
+
const negated = body.startsWith('!') || body.startsWith('^');
|
|
27
|
+
const chars = (negated ? body.slice(1) : body).replace(/\\/g, '\\\\').replace(/\]/g, '\\]');
|
|
28
|
+
const bracket = `[${negated ? '^' : ''}${chars}]`;
|
|
29
|
+
try {
|
|
30
|
+
new RegExp(bracket);
|
|
31
|
+
source += body.includes('[') ? '[\\s\\S]' : bracket;
|
|
32
|
+
} catch { source += '[\\s\\S]'; }
|
|
33
|
+
i = end;
|
|
34
|
+
} else source += literal(c);
|
|
35
|
+
}
|
|
36
|
+
return new RegExp(`(?:^|/)${source}(?:/|$)`).test(rel);
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.3",
|
|
4
4
|
"description": "一条命令备份、恢复与迁移 DeepSeek Harness(dsh)的全部数据(~/.dsh:会话、设置、插件、MCP、技能):定时自动备份、升级前快照、换机迁移、会话日志体检修复、宿主起不来也能用的进程外救援;凭据默认脱敏只存本机。 Backup, restore & migrate all DeepSeek Harness (~/.dsh) data — sessions, settings, plugins, MCP, skills: scheduled backups, pre-upgrade snapshots, cross-machine migration, session-log doctor, out-of-process rescue console; credentials redacted to a local-only vault.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.24.0",
|
package/rescue/rescue.mjs
CHANGED
|
@@ -93,8 +93,6 @@ function run(argv, cwd) {
|
|
|
93
93
|
const parentTar = (p) => (p.endsWith('/') ? p : `${p}/`);
|
|
94
94
|
|
|
95
95
|
async function sha256File(absPath) {
|
|
96
|
-
const info = await fs.stat(absPath);
|
|
97
|
-
if (info.size > HASH_MAX_BYTES) throw new Error(`文件 ${Math.floor(info.size / 1048576)}MB 超过哈希上限`);
|
|
98
96
|
return new Promise((resolve, reject) => {
|
|
99
97
|
const h = createHash('sha256');
|
|
100
98
|
createReadStream(absPath).on('data', (d) => h.update(d)).on('error', reject).on('end', () => resolve(h.digest('hex')));
|
|
@@ -510,6 +508,17 @@ async function restoreArchive(root, selector, apply) {
|
|
|
510
508
|
return { dryRun: true, archive: picked.name, files: entries.length, sample: entries.slice(0, 12), preflight, profileDirs };
|
|
511
509
|
}
|
|
512
510
|
|
|
511
|
+
// Keep this standalone rescue script dependency-free. A nested backup root
|
|
512
|
+
// would disappear when the source tree is moved aside during replacement.
|
|
513
|
+
const sourceReal = await fs.realpath(dshHome).catch((err) => {
|
|
514
|
+
if (err.code === 'ENOENT') return path.resolve(dshHome);
|
|
515
|
+
throw err;
|
|
516
|
+
});
|
|
517
|
+
const backupRel = path.relative(sourceReal, await fs.realpath(root));
|
|
518
|
+
if (!backupRel || (backupRel !== '..' && !backupRel.startsWith(`..${path.sep}`) && !path.isAbsolute(backupRel))) {
|
|
519
|
+
throw new Error('整包恢复前请把整个备份目录(含 vault)移到 DSH_HOME 外,并更新备份路径;当前备份目录位于待替换的数据目录内');
|
|
520
|
+
}
|
|
521
|
+
|
|
513
522
|
// 现有数据:快照 + 挪旁;缺失(新机/全失)则直接解压
|
|
514
523
|
let targetExists = true;
|
|
515
524
|
try {
|
|
@@ -755,12 +764,30 @@ function isPidAlive(pid) {
|
|
|
755
764
|
process.kill(pid, 0);
|
|
756
765
|
return true;
|
|
757
766
|
} catch (err) {
|
|
758
|
-
return err
|
|
767
|
+
return err?.code !== 'ESRCH';
|
|
759
768
|
}
|
|
760
769
|
}
|
|
761
770
|
|
|
762
771
|
/** 回收残留死锁(owner 证明已死才回收)。 */
|
|
763
772
|
async function recoverStaleLock(root) {
|
|
773
|
+
// Use the host's same exclusive transaction guard: never remove a lock while
|
|
774
|
+
// a live host is publishing a new owner or heartbeat.
|
|
775
|
+
const guard = `${root}/${LOCK_FILE}.guard`;
|
|
776
|
+
await fs.mkdir(root, { recursive: true });
|
|
777
|
+
try { await fs.mkdir(guard, { mode: 0o700 }); }
|
|
778
|
+
catch (err) {
|
|
779
|
+
if (err.code === 'EEXIST') throw new Error(`锁元数据正在更新(${guard}),请稍后重试`);
|
|
780
|
+
throw err;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
await fs.writeFile(`${guard}/owner.json`, JSON.stringify({ pid: process.pid }), { mode: 0o600 });
|
|
784
|
+
return await recoverStaleLockInner(root);
|
|
785
|
+
} finally {
|
|
786
|
+
await fs.rm(guard, { recursive: true, force: true });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function recoverStaleLockInner(root) {
|
|
764
791
|
const lockPath = `${root}/${LOCK_FILE}`;
|
|
765
792
|
let lock;
|
|
766
793
|
try {
|