@bigking67/pi-67 0.10.2 → 0.10.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/CHANGELOG.md +12 -0
- package/README.md +7 -3
- package/package.json +1 -1
- package/scripts/check.mjs +14 -0
- package/src/commands/themes.mjs +4 -0
- package/src/commands/update.mjs +2 -0
- package/src/lib/update-safety.mjs +64 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.3]
|
|
4
|
+
|
|
5
|
+
- Deduplicates real update/repair runtime backups when preserved config files
|
|
6
|
+
are unchanged from the latest same-operation backup.
|
|
7
|
+
- Deduplicates direct PowerShell `scripts/pi67-update.ps1` dirty runtime-config
|
|
8
|
+
preservation backups under `~/.pi/pi67/backups/`.
|
|
9
|
+
- Removes the legacy PowerShell updater path that wrote new
|
|
10
|
+
`~/.pi/agent-backups/pre-update-*` snapshots; runtime preservation now stays
|
|
11
|
+
under `~/.pi/pi67/backups/`.
|
|
12
|
+
- Avoids writing a theme backup when `pi-67 themes set <name>` is already the
|
|
13
|
+
active theme.
|
|
14
|
+
|
|
3
15
|
## [0.10.2]
|
|
4
16
|
|
|
5
17
|
- Reads `Manager latest` through the npm registry HTTP API instead of spawning
|
package/README.md
CHANGED
|
@@ -78,6 +78,9 @@ Before a real `update` or `repair`, the npm manager acquires
|
|
|
78
78
|
`~/.pi/pi67/backups/<timestamp>-update/`. This makes the public `npx -y
|
|
79
79
|
@bigking67/pi-67@latest update --repair` path safe even for in-place checkouts
|
|
80
80
|
where `settings.json` is tracked but user-owned.
|
|
81
|
+
If the preserved runtime files are unchanged from the latest same-operation
|
|
82
|
+
backup, the manager reuses that snapshot instead of writing another timestamped
|
|
83
|
+
directory.
|
|
81
84
|
|
|
82
85
|
Runtime backups are first-class CLI state:
|
|
83
86
|
|
|
@@ -94,9 +97,10 @@ The restore command only restores preserved runtime files and writes a
|
|
|
94
97
|
pre-restore backup before overwriting current local config.
|
|
95
98
|
|
|
96
99
|
Legacy PowerShell `~/.pi/agent-backups/pre-update-*` directories are read-only
|
|
97
|
-
known-conflict snapshots from older migration paths
|
|
98
|
-
`--include-legacy` and are
|
|
99
|
-
backups under
|
|
100
|
+
known-conflict snapshots from older migration paths; the normal updater no
|
|
101
|
+
longer writes new ones. They are listed only with `--include-legacy` and are
|
|
102
|
+
intentionally separate from restorable runtime backups under
|
|
103
|
+
`~/.pi/pi67/backups/`.
|
|
100
104
|
|
|
101
105
|
Theme changes are explicit only:
|
|
102
106
|
|
package/package.json
CHANGED
package/scripts/check.mjs
CHANGED
|
@@ -370,6 +370,20 @@ function runUpdateSafetySelfTests() {
|
|
|
370
370
|
listRuntimeBackups(ctx).some((item) => item.path === lifecycle.backupDir),
|
|
371
371
|
"update lifecycle backups must be listable",
|
|
372
372
|
);
|
|
373
|
+
const backupCountBeforeDedupe = listRuntimeBackups(ctx).length;
|
|
374
|
+
const duplicateLifecycle = beginUpdateLifecycle({
|
|
375
|
+
agentDir,
|
|
376
|
+
repoRoot: agentDir,
|
|
377
|
+
stateDir,
|
|
378
|
+
}, {
|
|
379
|
+
operation: "test",
|
|
380
|
+
});
|
|
381
|
+
assert(duplicateLifecycle.backupSkipped, "unchanged update lifecycle snapshots must be deduplicated");
|
|
382
|
+
duplicateLifecycle.release();
|
|
383
|
+
assert(
|
|
384
|
+
listRuntimeBackups(ctx).length === backupCountBeforeDedupe,
|
|
385
|
+
"deduplicated update lifecycle snapshots must not create new backup directories",
|
|
386
|
+
);
|
|
373
387
|
assert(
|
|
374
388
|
inspectRuntimeBackup(ctx, lifecycle.backupDir).fileCount >= 2,
|
|
375
389
|
"update lifecycle backups must be inspectable",
|
package/src/commands/themes.mjs
CHANGED
|
@@ -56,6 +56,10 @@ function setTheme(ctx, argv) {
|
|
|
56
56
|
const settingsFile = path.join(ctx.agentDir, "settings.json");
|
|
57
57
|
const settings = readJsonFileIfExists(settingsFile) || {};
|
|
58
58
|
const previous = settings.theme || "";
|
|
59
|
+
if (previous === name) {
|
|
60
|
+
pass(`theme already set: ${name}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
59
63
|
settings.theme = name;
|
|
60
64
|
if (ctx.dryRun || options.dryRun) {
|
|
61
65
|
info(`DRY-RUN set theme ${previous || "unset"} -> ${name}`);
|
package/src/commands/update.mjs
CHANGED
|
@@ -50,6 +50,8 @@ export async function updateCommand(ctx, argv) {
|
|
|
50
50
|
});
|
|
51
51
|
if (!dryRun && lifecycle.backedUp.length > 0) {
|
|
52
52
|
info(`Preserved runtime backup: ${lifecycle.backupDir}`);
|
|
53
|
+
} else if (!dryRun && lifecycle.backupSkipped) {
|
|
54
|
+
info(`Preserved runtime backup skipped: ${lifecycle.backupReason}`);
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
const args = isWindows()
|
|
@@ -35,14 +35,19 @@ export function beginUpdateLifecycle(ctx, options = {}) {
|
|
|
35
35
|
acquireLock(lockPath, { operation });
|
|
36
36
|
let released = false;
|
|
37
37
|
try {
|
|
38
|
-
const
|
|
38
|
+
const backup = createRuntimeBackup(ctx, backupDir, {
|
|
39
39
|
operation,
|
|
40
40
|
plan: options.plan,
|
|
41
|
+
dedupe: true,
|
|
42
|
+
returnResult: true,
|
|
41
43
|
});
|
|
42
44
|
return {
|
|
43
45
|
lockPath,
|
|
44
|
-
backupDir,
|
|
45
|
-
backedUp,
|
|
46
|
+
backupDir: backup.backupDir,
|
|
47
|
+
backedUp: backup.backedUp,
|
|
48
|
+
backupSkipped: backup.skipped,
|
|
49
|
+
backupReason: backup.reason || "",
|
|
50
|
+
reusedBackupDir: backup.reusedBackupDir || "",
|
|
46
51
|
release() {
|
|
47
52
|
if (released) return;
|
|
48
53
|
released = true;
|
|
@@ -56,34 +61,37 @@ export function beginUpdateLifecycle(ctx, options = {}) {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export function createRuntimeBackup(ctx, backupDir, options = {}) {
|
|
64
|
+
const operation = options.operation || "update";
|
|
65
|
+
const preserved = collectPreservedRuntimeFiles(ctx);
|
|
66
|
+
if (options.dedupe) {
|
|
67
|
+
const equivalent = findEquivalentRuntimeBackup(ctx, preserved, operation);
|
|
68
|
+
if (equivalent) {
|
|
69
|
+
const result = {
|
|
70
|
+
backupDir: equivalent.path,
|
|
71
|
+
backedUp: [],
|
|
72
|
+
skipped: true,
|
|
73
|
+
reusedBackupDir: equivalent.path,
|
|
74
|
+
reason: `same preserved runtime snapshot already exists: ${equivalent.id}`,
|
|
75
|
+
};
|
|
76
|
+
return options.returnResult ? result : result.backedUp;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
60
81
|
const filesDir = path.join(backupDir, "files");
|
|
61
82
|
fs.mkdirSync(filesDir, { recursive: true, mode: 0o700 });
|
|
62
83
|
|
|
63
|
-
const preserved = [];
|
|
64
|
-
const backedUp = [];
|
|
65
84
|
for (const rel of PRESERVED_RUNTIME_FILES) {
|
|
85
|
+
const item = preserved.find((entry) => entry.path === rel);
|
|
86
|
+
if (!item?.exists) continue;
|
|
66
87
|
const source = path.join(ctx.agentDir, rel);
|
|
67
|
-
if (!fs.existsSync(source)) {
|
|
68
|
-
preserved.push({
|
|
69
|
-
path: rel,
|
|
70
|
-
exists: false,
|
|
71
|
-
});
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
88
|
const target = path.join(filesDir, rel.replace(/[\\/]/g, "__"));
|
|
75
89
|
fs.copyFileSync(source, target);
|
|
76
90
|
chmodPrivate(target);
|
|
77
|
-
const item = {
|
|
78
|
-
path: rel,
|
|
79
|
-
exists: true,
|
|
80
|
-
bytes: fs.statSync(source).size,
|
|
81
|
-
sha256: sha256File(source),
|
|
82
|
-
};
|
|
83
|
-
preserved.push(item);
|
|
84
|
-
backedUp.push(item);
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
const backedUp = preserved.filter((item) => item.exists !== false);
|
|
94
|
+
|
|
87
95
|
if (options.plan) {
|
|
88
96
|
fs.writeFileSync(
|
|
89
97
|
path.join(backupDir, "update-plan.json"),
|
|
@@ -96,14 +104,15 @@ export function createRuntimeBackup(ctx, backupDir, options = {}) {
|
|
|
96
104
|
`${JSON.stringify({
|
|
97
105
|
schema: "pi67.update-backup.v1",
|
|
98
106
|
createdAt: new Date().toISOString(),
|
|
99
|
-
operation
|
|
107
|
+
operation,
|
|
100
108
|
agentDir: ctx.agentDir,
|
|
101
109
|
repoRoot: ctx.repoRoot,
|
|
102
110
|
files: preserved,
|
|
103
111
|
}, null, 2)}\n`,
|
|
104
112
|
{ mode: 0o600 },
|
|
105
113
|
);
|
|
106
|
-
|
|
114
|
+
const result = { backupDir, backedUp, skipped: false, reusedBackupDir: "", reason: "" };
|
|
115
|
+
return options.returnResult ? result : backedUp;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
export function listRuntimeBackups(ctx) {
|
|
@@ -246,6 +255,39 @@ function isStaleLock(lockPath) {
|
|
|
246
255
|
return false;
|
|
247
256
|
}
|
|
248
257
|
|
|
258
|
+
function collectPreservedRuntimeFiles(ctx) {
|
|
259
|
+
return PRESERVED_RUNTIME_FILES.map((rel) => {
|
|
260
|
+
const source = path.join(ctx.agentDir, rel);
|
|
261
|
+
if (!fs.existsSync(source)) {
|
|
262
|
+
return { path: rel, exists: false };
|
|
263
|
+
}
|
|
264
|
+
const stat = fs.statSync(source);
|
|
265
|
+
return {
|
|
266
|
+
path: rel,
|
|
267
|
+
exists: true,
|
|
268
|
+
bytes: stat.size,
|
|
269
|
+
sha256: sha256File(source),
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function findEquivalentRuntimeBackup(ctx, preserved, operation) {
|
|
275
|
+
return listRuntimeBackups(ctx).find((backup) =>
|
|
276
|
+
backup.operation === operation && sameRuntimeFiles(backup.files, preserved));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function sameRuntimeFiles(left, right) {
|
|
280
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
281
|
+
const leftByPath = new Map(left.map((item) => [item.path, item]));
|
|
282
|
+
return right.every((rightItem) => {
|
|
283
|
+
const leftItem = leftByPath.get(rightItem.path);
|
|
284
|
+
if (!leftItem) return false;
|
|
285
|
+
if ((leftItem.exists !== false) !== (rightItem.exists !== false)) return false;
|
|
286
|
+
if (rightItem.exists === false) return true;
|
|
287
|
+
return leftItem.bytes === rightItem.bytes && leftItem.sha256 === rightItem.sha256;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
249
291
|
function sha256File(file) {
|
|
250
292
|
const hash = crypto.createHash("sha256");
|
|
251
293
|
hash.update(fs.readFileSync(file));
|