pi-webdav-backup 1.3.2 → 1.3.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.md CHANGED
@@ -442,10 +442,13 @@ node "$env:USERPROFILE\.pi\agent\extensions\webdav-backup\cli.mjs" install-cron
442
442
  新电脑往往已经装过一些插件——可能和备份里同名但版本不同,也可能多装了备份里没有的。
443
443
  直接覆盖会**把同名插件降级、把本机多装的声明抹掉、留下版本混合的“缝合”目录**。所以写回有两种模式:
444
444
 
445
- | 模式 | settings.json 的 packages | 本机已装的插件实现 | 本机多装的插件 |
446
- |---|---|---|---|
447
- | **合并**(默认,推荐) | 本机声明优先,追加备份里本机没有的 | **保留本机版本**,不覆盖 | 保留(声明与代码都留) |
448
- | 替换(旧机原样) | 完全用备份版 | 被备份覆盖(同名降级) | 声明被移除(代码不删) |
445
+ | 模式 | settings.json 的 packages | npm/package.json 的 dependencies | 本机已装的插件实现 | 本机多装的插件 |
446
+ |---|---|---|---|---|
447
+ | **合并**(默认,推荐) | 本机声明优先,追加备份里本机没有的 | 本机依赖优先,追加备份里本机没有的 | **保留本机版本**,不覆盖 | 保留(声明与代码都留) |
448
+ | 替换(旧机原样) | 完全用备份版 | 完全用备份版 | 被备份覆盖(同名降级) | 声明被移除(代码不删) |
449
+
450
+ > **为何要合并 `npm/package.json`**:pi 启动时会拿它与 `node_modules` 做协调(npm install)。
451
+ > 若直接覆盖成备份版,本机独有的插件会被 npm 当成多余包【卸载】。所以合并模式会保留本机已有依赖。
449
452
 
450
453
  - 交互式 `/backup restore`:本机有插件声明时会先让你选(默认合并)
451
454
  - 命令行:`--mode merge`(默认)/ `--mode replace`
@@ -633,6 +636,7 @@ node extensions/webdav-backup/verify-load.mjs
633
636
 
634
637
  | 版本 | 内容 |
635
638
  |---|---|
639
+ | **v1.3.3** | **修复合并模式会卸载本机插件**:`restore-apply` 合并模式此前只合并 `settings.json`,却用备份版**覆盖** `npm/package.json`——pi 下次启动的 npm 协调会把本机独有插件当成多余包卸载(实测于 Windows→Linux 同步)。现改为同时合并 `npm/package.json` 的 `dependencies`(本机优先),结果里标 `npm-dependencies`。另:文档补充「多机同步」与「跨平台密钥」流程 |
636
640
  | **v1.3.2** | **跨平台多机同步**:依赖体检修正(npm 提升到父级的依赖不再误报为缺失,按 Node 模块解析算法沿祖先 `node_modules` 查找);文档新增「跨平台密钥(多机同步)」与「多机同步(Windows ↔ Linux)」流程,明确多机用 `file:` / `$ENV` 而非 `dpapi:`,并给出常见坑 |
637
641
  | **v1.3.1** | 依赖体检误报修正(npm 提升的依赖被当成缺失) |
638
642
  | **v1.3.0** | **换机一键还原**:新增 `includePlugins`(把插件实现 `node_modules` 一并备份,重机恢复即可直接用,无需联网重装);新增 `restore-apply` / `/backup restore-apply`(把恢复结果写回当前 `~/.pi/agent`,覆盖前备份、幂等、防止 `__REDACTED__` 覆盖真值);**写回默认【合并模式】**:本机已装插件保留不被覆盖、`settings.json` 只合并 `packages`(避免删掉/降级本机插件),可选 `--mode replace` 完全还原;`/backup restore` 默认询问是否写回当前 pi,已含插件时不再提示联网补装;`/backup-setup` 向导新增插件代码选项;脱敏审计跳过第三方插件代码(避免误报);排除规则重构(`includePlugins` 开关控制 node_modules) |
@@ -64,6 +64,11 @@ export function isPluginImplementationPath(rel) {
64
64
  return r.startsWith("git/") || r.includes("/node_modules/");
65
65
  }
66
66
 
67
+ /** agentDir 下的 npm 包清单(决定 pi 启动时 npm 装/卸哪些插件) */
68
+ export function isNpmManifestPath(rel) {
69
+ return String(rel).replace(/\\/g, "/") === "npm/package.json";
70
+ }
71
+
67
72
  /**
68
73
  * 求一个插件实现文件所属的「插件根目录」相对路径,用于判定本机是否已装该插件:
69
74
  * npm/node_modules/foo/lib/x.js → npm/node_modules/foo
@@ -145,6 +150,46 @@ export function mergeSettingsPackages(backupText, localText) {
145
150
  return JSON.stringify({ ...backup, packages: [...localPkgs, ...extra] }, null, 2);
146
151
  }
147
152
 
153
+ /**
154
+ * 合并 npm/package.json 的 dependencies:本机已有优先(保留版本),追加备份里本机没有的。
155
+ *
156
+ * 为何必须合并:pi 启动时会拿 npm/package.json 与 node_modules 做协调(npm install)。
157
+ * 若 merge 写回时用备份版直接覆盖,本机独有的插件会被 npm 当成多余包【卸载】。
158
+ * 返回合并后的 JSON 文本;无可合并时返回 null(调用方按普通逻辑处理)。
159
+ */
160
+ export function mergeNpmDependencies(backupText, localText) {
161
+ let backup;
162
+ let local;
163
+ try {
164
+ backup = JSON.parse(String(backupText).replace(/^\uFEFF/, ""));
165
+ } catch {
166
+ return null;
167
+ }
168
+ try {
169
+ local = JSON.parse(String(localText).replace(/^\uFEFF/, ""));
170
+ } catch {
171
+ return null;
172
+ }
173
+ if (!backup || typeof backup !== "object") return null;
174
+
175
+ const backupDeps = backup.dependencies && typeof backup.dependencies === "object" ? backup.dependencies : {};
176
+ const localDeps = local?.dependencies && typeof local.dependencies === "object" ? local.dependencies : {};
177
+ if (Object.keys(localDeps).length === 0) return null;
178
+
179
+ // 本机已声明的依赖保留本机版本;备份独有的追加进来
180
+ const merged = { ...localDeps };
181
+ let added = 0;
182
+ for (const [name, range] of Object.entries(backupDeps)) {
183
+ if (!(name in merged)) {
184
+ merged[name] = range;
185
+ added++;
186
+ }
187
+ }
188
+ // 本机声明已包含备份全部依赖且无新增 → 无需改写
189
+ if (added === 0 && Object.keys(merged).length === Object.keys(localDeps).length) return null;
190
+ return JSON.stringify({ ...backup, dependencies: merged }, null, 2);
191
+ }
192
+
148
193
  /** 递归列出恢复目录下、应写回 agentDir 的文件(跳过 extra/ 与元数据) */
149
194
  export function listRestoredFiles(restoredDir, { extraPrefix = "extra/" } = {}) {
150
195
  const root = resolve(restoredDir);
@@ -224,6 +269,21 @@ export function applyRestoredFiles(restoredDir, agentDir, opts = {}) {
224
269
  }
225
270
  }
226
271
 
272
+ // merge:npm/package.json 只合并 dependencies,保留本机独有插件。
273
+ // 否则覆盖后,pi 下次启动的 npm 协调会把本机独有的包当成多余包卸载。
274
+ let mergedNpm = false;
275
+ if (merge && isNpmManifestPath(f.rel) && existsSync(target)) {
276
+ try {
277
+ const mergedText = mergeNpmDependencies(srcBuf.toString("utf8"), readFileSync(target, "utf8"));
278
+ if (mergedText !== null) {
279
+ srcBuf = Buffer.from(mergedText, "utf8");
280
+ mergedNpm = true;
281
+ }
282
+ } catch {
283
+ /* 合并失败则退回普通处理 */
284
+ }
285
+ }
286
+
227
287
  // merge:本机已装该插件(插件根目录已存在)→ 跳过整个插件,避免降级与版本混合。
228
288
  // 但若该文件与本地完全一致,则归为 unchanged(保证重复写回幂等)。
229
289
  if (merge) {
@@ -281,11 +341,13 @@ export function applyRestoredFiles(restoredDir, agentDir, opts = {}) {
281
341
  let backup = null;
282
342
  if (existsSync(target)) backup = backupExisting(target, "apply");
283
343
  writeFileSync(target, srcBuf);
284
- applied.push({ rel: f.rel, target, backup, merged: mergedSettings });
285
- if (mergedSettings) merged.push({ rel: f.rel, target });
344
+ applied.push({ rel: f.rel, target, backup, merged: mergedSettings || mergedNpm });
345
+ if (mergedSettings) merged.push({ rel: f.rel, target, kind: "settings-packages" });
346
+ if (mergedNpm) merged.push({ rel: f.rel, target, kind: "npm-dependencies" });
286
347
  log(
287
348
  ` ${backup ? "已覆盖" : "已写入"} ${f.rel} → ${target}` +
288
349
  (mergedSettings ? "(已合并 packages,保留本机插件声明)" : "") +
350
+ (mergedNpm ? "(已合并 dependencies,保留本机插件依赖)" : "") +
289
351
  (backup ? `(原文件备份为 ${backup})` : ""),
290
352
  );
291
353
  } catch (e) {
@@ -246,7 +246,11 @@ async function main() {
246
246
  if (!has("--dry-run")) {
247
247
  logTo(`已写回 agent 目录:${agentDir}(${mergeMode ? "合并模式" : "替换模式"})`);
248
248
  logTo(` 写入/覆盖 ${r.applied.length} 个,内容一致 ${r.unchanged.length} 个,失败 ${r.failed.length} 个`);
249
- if (r.merged?.length) logTo(` 🔀 settings.json 已合并 packages,保留本机已有插件声明`);
249
+ if (r.merged?.length) {
250
+ const kinds = r.merged.map((m) => m.kind).filter(Boolean);
251
+ if (kinds.includes("settings-packages")) logTo(` 🔀 settings.json 已合并 packages,保留本机已有插件声明`);
252
+ if (kinds.includes("npm-dependencies")) logTo(` 🔀 npm/package.json 已合并 dependencies,保留本机独有插件依赖`);
253
+ }
250
254
  for (const x of r.skipped) logTo(` ➖ ${x.rel}(${x.reason})`);
251
255
  for (const x of r.failed) logTo(` ❌ ${x.rel} — ${x.error}`);
252
256
  if (pluginReady) logTo("插件实现代码已随备份恢复,无需联网重装。");
@@ -298,7 +298,7 @@ async function main() {
298
298
  rmSync(live2, { recursive: true, force: true });
299
299
  }
300
300
 
301
- // ── 8. mergeSettingsPackages:本机无声明时返回 null(用备份原样)
301
+ // 8. mergeSettingsPackages:本机无声明时返回 null(用备份原样)
302
302
  {
303
303
  check("本机无 packages 声明时不合并", mergeSettingsPackages('{"packages":["npm:A"]}', '{"theme":"x"}') === null);
304
304
  const m = mergeSettingsPackages('{"packages":["npm:A","npm:B"],"theme":"light"}', '{"packages":["npm:B@2"],"theme":"dark"}');
@@ -306,6 +306,50 @@ async function main() {
306
306
  check("合并保留本机声明优先、追加备份独有", JSON.stringify(o.packages) === JSON.stringify(["npm:B@2", "npm:A"]) && o.theme === "light", String(m));
307
307
  }
308
308
 
309
+ // ── 9. 回归:npm/package.json 必须合并 dependencies(否则 pi 会卸载本机独有插件)
310
+ {
311
+ // 备份来源(Windows):只依赖 A、B
312
+ const backup = mkdtempSync(join(tmpdir(), "pi-npm-merge-bk-"));
313
+ mkdirSync(join(backup, "npm"), { recursive: true });
314
+ writeFileSync(join(backup, "settings.json"), JSON.stringify({ packages: ["npm:A", "npm:B"] }));
315
+ writeFileSync(
316
+ join(backup, "npm", "package.json"),
317
+ JSON.stringify({ name: "pi-extensions", private: true, dependencies: { A: "^1.0.0", B: "^2.0.0" } }),
318
+ );
319
+
320
+ // 目标机器(Linux):已装 A(本机版本)+ 本机独有的 Z
321
+ const live = mkdtempSync(join(tmpdir(), "pi-npm-merge-live-"));
322
+ mkdirSync(join(live, "npm"), { recursive: true });
323
+ writeFileSync(join(live, "settings.json"), JSON.stringify({ packages: ["npm:A@9", "npm:Z"] }));
324
+ writeFileSync(
325
+ join(live, "npm", "package.json"),
326
+ JSON.stringify({ name: "pi-extensions", private: true, dependencies: { A: "^9.0.0", Z: "^0.1.0" } }),
327
+ );
328
+
329
+ const r = applyRestoredFiles(backup, live, { merge: true });
330
+ const deps = JSON.parse(readFileSync(join(live, "npm", "package.json"), "utf8")).dependencies;
331
+ check("merge 后 npm 依赖保留本机 A@9", deps.A === "^9.0.0", JSON.stringify(deps));
332
+ check("merge 后 npm 依赖保留本机独有 Z", deps.Z === "^0.1.0", JSON.stringify(deps));
333
+ check("merge 后 npm 依赖追加备份独有的 B", deps.B === "^2.0.0", JSON.stringify(deps));
334
+ check("merged 列表标出 npm-dependencies", r.merged?.some((m) => m.kind === "npm-dependencies"), JSON.stringify(r.merged));
335
+
336
+ // replace:完全用备份版(Z 会从 package.json 消失,由 pi 下次启动自行卸载)
337
+ const live2 = mkdtempSync(join(tmpdir(), "pi-npm-merge-live2-"));
338
+ mkdirSync(join(live2, "npm"), { recursive: true });
339
+ writeFileSync(join(live2, "settings.json"), JSON.stringify({ packages: ["npm:A@9", "npm:Z"] }));
340
+ writeFileSync(
341
+ join(live2, "npm", "package.json"),
342
+ JSON.stringify({ name: "pi-extensions", private: true, dependencies: { A: "^9.0.0", Z: "^0.1.0" } }),
343
+ );
344
+ applyRestoredFiles(backup, live2, { merge: false });
345
+ const deps2 = JSON.parse(readFileSync(join(live2, "npm", "package.json"), "utf8")).dependencies;
346
+ check("replace 后 npm 依赖完全变回备份版", JSON.stringify(deps2) === JSON.stringify({ A: "^1.0.0", B: "^2.0.0" }), JSON.stringify(deps2));
347
+
348
+ rmSync(backup, { recursive: true, force: true });
349
+ rmSync(live, { recursive: true, force: true });
350
+ rmSync(live2, { recursive: true, force: true });
351
+ }
352
+
309
353
  console.log(lines.join("\n"));
310
354
  console.log(`\n结果:${pass} 通过,${fail} 失败`);
311
355
  process.exit(fail === 0 ? 0 : 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-webdav-backup",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "pi 配置与插件数据自动备份到 WebDAV(坚果云 / Nextcloud / 群晖 / 自建):归档与快照两种模式、AES-256-GCM 加密、DPAPI 密钥管理、敏感字段脱敏、agentDir 之外的额外数据(extraScope)、插件实现代码随备份(includePlugins)实现换机一键还原、退出自动备份与每日定时备份。",
5
5
  "keywords": [
6
6
  "pi-package",