pi-patch-keeper 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.ts +17 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ npm 发布版:`pi install npm:pi-patch-keeper`
8
8
 
9
9
  安装后 `/reload` 或重启会话生效(扩展源码改动需重启)。
10
10
 
11
- **是什么**:把「对别人的东西」的改动(**12 个面**:PiDeck builtin ×5 双份 · npm 包 ×4 · app.asar ×3)从「升级后手工重跑」变成**启动自愈 + 明确报告**——外置、模块化、抗上游升级。全量清单见 `~/Desktop/pi-env-snapshot/05-脚本补丁/PATCH-REGISTRY.md`(唯一权威源)。
11
+ **是什么**:把「对别人的东西」的改动(**12 个面**:PiDeck builtin ×5 双份 · npm 包 ×4 · app.asar ×3)从「升级后手工重跑」变成**启动自愈 + 明确报告**——外置、模块化、抗上游升级。全量清单由本机补丁登记表维护(PATCH-REGISTRY,随快照包分发)。
12
12
 
13
13
  ## 来源与借鉴
14
14
 
package/index.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * - 任何失败都只 notify,不抛错、不影响会话(纪律:不合法就跳过 + 记日志,绝不崩溃)。
21
21
  */
22
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
23
- import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync } from "node:fs";
23
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync, renameSync } from "node:fs";
24
24
  import { createHash } from "node:crypto";
25
25
  import * as os from "node:os";
26
26
  import * as path from "node:path";
@@ -140,7 +140,7 @@ function healPiMemory(): Result {
140
140
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
141
141
  const bak = `${PI_MEMORY}.bak-exitsummary-${stamp}`;
142
142
  if (!existsSync(bak)) copyFileSync(PI_MEMORY, bak);
143
- writeFileSync(PI_MEMORY, next, "utf8");
143
+ writeAtomic(PI_MEMORY, next);
144
144
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
145
145
  return { ok: true, detail: "已自动修补(exit summary 空行规范化)—— 本轮即生效" };
146
146
  }
@@ -157,7 +157,7 @@ function healMemorySection(): Result {
157
157
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
158
158
  const bak = `${PI_MEMORY}.bak-memsec-${stamp}`;
159
159
  if (!existsSync(bak)) copyFileSync(PI_MEMORY, bak);
160
- writeFileSync(PI_MEMORY, next, "utf8");
160
+ writeAtomic(PI_MEMORY, next);
161
161
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
162
162
  return { ok: true, detail: "已自动修补(memory 段改 section)—— 重启后生效" };
163
163
  }
@@ -174,7 +174,7 @@ function healTactician(): Result {
174
174
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
175
175
  const bak = `${TACT}.bak-tactician-${stamp}`;
176
176
  if (!existsSync(bak)) copyFileSync(TACT, bak);
177
- writeFileSync(TACT, next, "utf8");
177
+ writeAtomic(TACT, next);
178
178
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
179
179
  return { ok: true, detail: "已自动修补(tactician 改 section)—— 重启后生效" };
180
180
  }
@@ -192,6 +192,13 @@ function safeRead(p: string): string | null {
192
192
  try { return readFileSync(p, "utf8"); } catch { return null; }
193
193
  }
194
194
 
195
+ /** 原子写:临时文件 + rename(崩溃/被杀不留半写文件)。 */
196
+ function writeAtomic(file: string, data: string | Buffer): void {
197
+ const tmp = file + ".tmp-" + process.pid;
198
+ writeFileSync(tmp, data);
199
+ renameSync(tmp, file);
200
+ }
201
+
195
202
  /** 统计 needle 在原串里的出现次数。 */
196
203
  function count(hay: string, needle: string): number {
197
204
  let n = 0, i = 0;
@@ -230,7 +237,7 @@ function healPiLens(): Result {
230
237
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
231
238
  const bak = `${PI_LENS}.bak-wordindex-${stamp}`;
232
239
  if (!existsSync(bak)) copyFileSync(PI_LENS, bak);
233
- writeFileSync(PI_LENS, next, "utf8");
240
+ writeAtomic(PI_LENS, next);
234
241
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
235
242
  return { ok: true, detail: state === "upstream" ? `上游已修复 #4583 —— 跳过 A,仅补 B${a1 ? "(A 残留,建议 npm 重装清除)" : ""}` : `已自动修补(原状态 ${state})—— 本轮即生效` };
236
243
  }
@@ -265,7 +272,7 @@ function healMemoryFormat(): Result {
265
272
  const txt = safeRead(p);
266
273
  if (txt === null) continue;
267
274
  const norm = normMd(txt);
268
- if (norm !== txt) { try { writeFileSync(p, norm, "utf8"); fixed += 1; } catch { /* 静默 */ } }
275
+ if (norm !== txt) { try { writeAtomic(p, norm); fixed += 1; } catch { /* 静默 */ } }
269
276
  }
270
277
  };
271
278
  walk(dir);
@@ -320,7 +327,7 @@ function fingerprintDrift(): string[] {
320
327
  let prev: Record<string, string[]> | null = null;
321
328
  const prevRaw = safeRead(FINGERPRINT);
322
329
  if (prevRaw) { try { prev = JSON.parse(prevRaw) as Record<string, string[]>; } catch { prev = null; } }
323
- try { writeFileSync(FINGERPRINT, JSON.stringify(cur, null, 1), "utf8"); } catch { /* 静默 */ }
330
+ try { writeAtomic(FINGERPRINT, JSON.stringify(cur, null, 1)); } catch { /* 静默 */ }
324
331
  if (prev === null) return [];
325
332
  const out: string[] = [];
326
333
  for (const k of ["packages", "defaultTools", "enabledModels", "extensions", "codemode"] as const) {
@@ -371,7 +378,7 @@ function manualDrift(): string | null {
371
378
  let prev: { ver?: string; sig?: string } | null = null;
372
379
  const raw = safeRead(MANUAL_STATE);
373
380
  if (raw) { try { prev = JSON.parse(raw); } catch { prev = null; } }
374
- try { writeFileSync(MANUAL_STATE, JSON.stringify({ ver, sig })); } catch { /* ignore */ }
381
+ try { writeAtomic(MANUAL_STATE, JSON.stringify({ ver, sig })); } catch { /* ignore */ }
375
382
  if (prev && prev.ver === ver && prev.sig !== sig) return `手册内容已变但版本号未升(${ver})—— bump-manual 或撤销未登记改动`;
376
383
  return null;
377
384
  }
@@ -411,7 +418,7 @@ function fixForceOrder(): string | null {
411
418
  try {
412
419
  copyFileSync(SETTINGS, `${SETTINGS}.bak-force-order-${Date.now()}`);
413
420
  s.packages = next;
414
- writeFileSync(SETTINGS, JSON.stringify(s, null, 2), "utf8");
421
+ writeAtomic(SETTINGS, JSON.stringify(s, null, 2));
415
422
  } catch (e) { return `force 顺序自动修复失败:${(e as Error).message}`; }
416
423
  return `force 顺序已自动修复:${FORCE_TAIL.filter((n) => next.includes(n)).join(" / ")} 移到 packages 末尾`;
417
424
  }
@@ -472,7 +479,7 @@ function healSelfExtension(): Result {
472
479
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
473
480
  const bak = `${SELF_EXT}.bak-overwrite-${stamp}`;
474
481
  if (!existsSync(bak)) copyFileSync(SELF_EXT, bak);
475
- writeFileSync(SELF_EXT, snap, "utf8");
482
+ writeAtomic(SELF_EXT, snap);
476
483
  } catch (e) { return { ok: false, detail: `恢复失败:${(e as Error).message}` }; }
477
484
  return { ok: true, detail: "疑似被发布精简版覆盖 —— 已从快照恢复(重启后生效)" };
478
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-patch-keeper",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Startup self-healing for pi/pi-lens/pi-memory/PiDeck patches, plus force-typed extension order guard and status report.",
5
5
  "type": "module",
6
6
  "license": "MIT",