@bigking67/pi-67 0.10.1 → 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 +24 -0
- package/README.md +15 -1
- package/package.json +1 -1
- package/scripts/check.mjs +86 -3
- package/src/commands/backups.mjs +32 -9
- package/src/commands/extensions.mjs +4 -4
- package/src/commands/publish-check.mjs +3 -3
- package/src/commands/status.mjs +1 -1
- package/src/commands/themes.mjs +4 -0
- package/src/commands/update.mjs +4 -2
- package/src/lib/npm-registry.mjs +63 -16
- package/src/lib/shell-runner.mjs +19 -6
- package/src/lib/update-plan.mjs +2 -2
- package/src/lib/update-safety.mjs +132 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.10.2]
|
|
16
|
+
|
|
17
|
+
- Reads `Manager latest` through the npm registry HTTP API instead of spawning
|
|
18
|
+
local `npm` / `npm.cmd`, fixing Windows environments where command shims fail
|
|
19
|
+
with `spawnSync npm.cmd EINVAL`.
|
|
20
|
+
- Adds a final Windows `cmd.exe /d /s /c npm.cmd ...` fallback for explicit npm
|
|
21
|
+
operations such as `pi-67 self-update`.
|
|
22
|
+
- Adds read-only visibility for legacy PowerShell
|
|
23
|
+
`~/.pi/agent-backups/pre-update-*` conflict snapshots through
|
|
24
|
+
`pi-67 backups list --include-legacy` and
|
|
25
|
+
`pi-67 backups inspect --legacy <pre-update-id>`.
|
|
26
|
+
|
|
3
27
|
## [0.10.1]
|
|
4
28
|
|
|
5
29
|
- Falls back from `npm` to `npm.cmd` on Windows when checking npm registry state
|
package/README.md
CHANGED
|
@@ -39,7 +39,10 @@ pi-67 update --repair
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
`pi-67 update --check` reports whether the npm manager is outdated. Updating the
|
|
42
|
-
manager itself is explicit
|
|
42
|
+
manager itself is explicit. The latest-version check reads the npm registry
|
|
43
|
+
HTTP API directly and does not depend on spawning local `npm` / `npm.cmd`.
|
|
44
|
+
Explicit npm operations such as `self-update` still use npm, with Windows
|
|
45
|
+
fallback through `cmd.exe /d /s /c npm.cmd ...`:
|
|
43
46
|
|
|
44
47
|
```bash
|
|
45
48
|
pi-67 self-update
|
|
@@ -75,12 +78,17 @@ Before a real `update` or `repair`, the npm manager acquires
|
|
|
75
78
|
`~/.pi/pi67/backups/<timestamp>-update/`. This makes the public `npx -y
|
|
76
79
|
@bigking67/pi-67@latest update --repair` path safe even for in-place checkouts
|
|
77
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.
|
|
78
84
|
|
|
79
85
|
Runtime backups are first-class CLI state:
|
|
80
86
|
|
|
81
87
|
```bash
|
|
82
88
|
pi-67 backups list
|
|
89
|
+
pi-67 backups list --include-legacy
|
|
83
90
|
pi-67 backups inspect <backup-id-or-path>
|
|
91
|
+
pi-67 backups inspect <pre-update-id> --legacy
|
|
84
92
|
pi-67 backups restore --from <backup-id-or-path> --dry-run
|
|
85
93
|
pi-67 backups restore --from <backup-id-or-path> --yes
|
|
86
94
|
```
|
|
@@ -88,6 +96,12 @@ pi-67 backups restore --from <backup-id-or-path> --yes
|
|
|
88
96
|
The restore command only restores preserved runtime files and writes a
|
|
89
97
|
pre-restore backup before overwriting current local config.
|
|
90
98
|
|
|
99
|
+
Legacy PowerShell `~/.pi/agent-backups/pre-update-*` directories are read-only
|
|
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/`.
|
|
104
|
+
|
|
91
105
|
Theme changes are explicit only:
|
|
92
106
|
|
|
93
107
|
```bash
|
package/package.json
CHANGED
package/scripts/check.mjs
CHANGED
|
@@ -3,13 +3,15 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { npmPublishTargetStatus } from "../src/lib/npm-registry.mjs";
|
|
6
|
+
import { npmLatestVersion, npmPublishTargetStatus, npmRegistryPackageUrl } from "../src/lib/npm-registry.mjs";
|
|
7
7
|
import { commandCandidatesForPlatform } from "../src/lib/shell-runner.mjs";
|
|
8
8
|
import { readExtensionRegistry, validateExtensionRegistry } from "../src/lib/extension-registry.mjs";
|
|
9
9
|
import { buildPlanDecisions, classifyGitShort } from "../src/lib/update-plan.mjs";
|
|
10
10
|
import {
|
|
11
11
|
beginUpdateLifecycle,
|
|
12
|
+
inspectLegacyConflictBackup,
|
|
12
13
|
inspectRuntimeBackup,
|
|
14
|
+
listLegacyConflictBackups,
|
|
13
15
|
listRuntimeBackups,
|
|
14
16
|
restoreRuntimeBackup,
|
|
15
17
|
} from "../src/lib/update-safety.mjs";
|
|
@@ -26,6 +28,7 @@ for (const file of files.filter((item) => item.endsWith(".mjs"))) {
|
|
|
26
28
|
for (const file of files.filter((item) => item.endsWith(".json"))) {
|
|
27
29
|
JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
30
|
}
|
|
31
|
+
await runNpmRegistrySelfTests();
|
|
29
32
|
runPublishTargetSelfTests();
|
|
30
33
|
runShellRunnerSelfTests();
|
|
31
34
|
runExtensionRegistrySelfTests();
|
|
@@ -94,10 +97,53 @@ function runPublishTargetSelfTests() {
|
|
|
94
97
|
);
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
async function runNpmRegistrySelfTests() {
|
|
101
|
+
assert(
|
|
102
|
+
npmRegistryPackageUrl("@bigking67/pi-67") === "https://registry.npmjs.org/@bigking67%2Fpi-67/latest",
|
|
103
|
+
"scoped npm package registry URL must use the direct registry endpoint",
|
|
104
|
+
);
|
|
105
|
+
const current = await npmLatestVersion("@example/pkg", {
|
|
106
|
+
currentVersion: "0.9.0",
|
|
107
|
+
fetchImpl: async () => jsonResponse(200, { version: "1.2.3" }),
|
|
108
|
+
});
|
|
109
|
+
assert(current.ok && current.latestVersion === "1.2.3" && current.outdated, "direct registry latest lookup must parse version payloads");
|
|
110
|
+
|
|
111
|
+
const missing = await npmLatestVersion("@example/missing", {
|
|
112
|
+
fetchImpl: async () => jsonResponse(404, { error: "not found" }),
|
|
113
|
+
});
|
|
114
|
+
assert(missing.message === "not published on npm registry yet", "direct registry 404 must classify unpublished packages");
|
|
115
|
+
|
|
116
|
+
const malformed = await npmLatestVersion("@example/bad", {
|
|
117
|
+
fetchImpl: async () => jsonResponse(200, { name: "@example/bad" }),
|
|
118
|
+
});
|
|
119
|
+
assert(!malformed.ok && malformed.message === "npm registry returned no version", "direct registry malformed payloads must fail closed");
|
|
120
|
+
|
|
121
|
+
const invalidJson = await npmLatestVersion("@example/invalid-json", {
|
|
122
|
+
fetchImpl: async () => ({
|
|
123
|
+
ok: true,
|
|
124
|
+
status: 200,
|
|
125
|
+
statusText: "OK",
|
|
126
|
+
async json() {
|
|
127
|
+
throw new SyntaxError("Unexpected token");
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
assert(invalidJson.message === "npm registry returned invalid JSON", "direct registry invalid JSON must be classified");
|
|
132
|
+
|
|
133
|
+
const timeout = await npmLatestVersion("@example/slow", {
|
|
134
|
+
fetchImpl: async () => {
|
|
135
|
+
const error = new Error("The operation was aborted");
|
|
136
|
+
error.name = "AbortError";
|
|
137
|
+
throw error;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
assert(timeout.message === "npm registry lookup timed out", "direct registry aborts must classify as timeouts");
|
|
141
|
+
}
|
|
142
|
+
|
|
97
143
|
function runShellRunnerSelfTests() {
|
|
98
144
|
assert(
|
|
99
|
-
JSON.stringify(commandCandidatesForPlatform("npm", "win32")) === JSON.stringify(["npm", "npm.cmd"]),
|
|
100
|
-
"Windows npm execution must fall back
|
|
145
|
+
JSON.stringify(commandCandidatesForPlatform("npm", "win32")) === JSON.stringify(["npm", "npm.cmd", "cmd.exe"]),
|
|
146
|
+
"Windows npm execution must fall back through npm.cmd and cmd.exe",
|
|
101
147
|
);
|
|
102
148
|
assert(
|
|
103
149
|
JSON.stringify(commandCandidatesForPlatform("git", "win32")) === JSON.stringify(["git"]),
|
|
@@ -324,6 +370,20 @@ function runUpdateSafetySelfTests() {
|
|
|
324
370
|
listRuntimeBackups(ctx).some((item) => item.path === lifecycle.backupDir),
|
|
325
371
|
"update lifecycle backups must be listable",
|
|
326
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
|
+
);
|
|
327
387
|
assert(
|
|
328
388
|
inspectRuntimeBackup(ctx, lifecycle.backupDir).fileCount >= 2,
|
|
329
389
|
"update lifecycle backups must be inspectable",
|
|
@@ -348,6 +408,18 @@ function runUpdateSafetySelfTests() {
|
|
|
348
408
|
fs.readFileSync(path.join(agentDir, "settings.json"), "utf8").includes("\"dark\""),
|
|
349
409
|
"runtime backup restore must recover the backed up file content",
|
|
350
410
|
);
|
|
411
|
+
const legacyRoot = path.join(path.dirname(stateDir), "agent-backups", "pre-update-20260707-235901");
|
|
412
|
+
fs.mkdirSync(legacyRoot, { recursive: true });
|
|
413
|
+
fs.writeFileSync(path.join(legacyRoot, "local.diff"), "diff --git a/settings.json b/settings.json\n");
|
|
414
|
+
fs.writeFileSync(path.join(legacyRoot, "settings.json"), "{\"theme\":\"legacy\"}\n");
|
|
415
|
+
assert(
|
|
416
|
+
listLegacyConflictBackups(ctx).some((item) => item.id === "pre-update-20260707-235901" && item.hasLocalDiff),
|
|
417
|
+
"legacy PowerShell conflict backups must be listable",
|
|
418
|
+
);
|
|
419
|
+
assert(
|
|
420
|
+
inspectLegacyConflictBackup(ctx, "pre-update-20260707-235901").files.some((item) => item.name === "local.diff"),
|
|
421
|
+
"legacy PowerShell conflict backups must be inspectable",
|
|
422
|
+
);
|
|
351
423
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
352
424
|
}
|
|
353
425
|
|
|
@@ -413,6 +485,17 @@ function deepMerge(base, overrides) {
|
|
|
413
485
|
return next;
|
|
414
486
|
}
|
|
415
487
|
|
|
488
|
+
function jsonResponse(status, payload) {
|
|
489
|
+
return {
|
|
490
|
+
ok: status >= 200 && status < 300,
|
|
491
|
+
status,
|
|
492
|
+
statusText: status === 404 ? "Not Found" : "OK",
|
|
493
|
+
async json() {
|
|
494
|
+
return payload;
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
416
499
|
function assert(condition, message) {
|
|
417
500
|
if (!condition) throw new Error(message);
|
|
418
501
|
}
|
package/src/commands/backups.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { parseCommandOptions } from "../lib/args.mjs";
|
|
2
3
|
import {
|
|
4
|
+
inspectLegacyConflictBackup,
|
|
3
5
|
inspectRuntimeBackup,
|
|
6
|
+
listLegacyConflictBackups,
|
|
4
7
|
listRuntimeBackups,
|
|
5
8
|
restoreRuntimeBackup,
|
|
6
9
|
} from "../lib/update-safety.mjs";
|
|
@@ -20,14 +23,16 @@ export async function backupsCommand(ctx, argv) {
|
|
|
20
23
|
|
|
21
24
|
function listCommand(ctx, argv) {
|
|
22
25
|
const { options } = parseCommandOptions(argv, {
|
|
23
|
-
bools: ["json"],
|
|
26
|
+
bools: ["json", "include-legacy"],
|
|
24
27
|
});
|
|
25
28
|
const backups = listRuntimeBackups(ctx);
|
|
29
|
+
const legacyConflictBackups = listLegacyConflictBackups(ctx);
|
|
26
30
|
if (ctx.json || options.json) {
|
|
27
31
|
printJson({
|
|
28
32
|
schema: "pi67.backups-list.v1",
|
|
29
33
|
createdAt: new Date().toISOString(),
|
|
30
34
|
backups,
|
|
35
|
+
legacyConflictBackups: options.includeLegacy ? legacyConflictBackups : undefined,
|
|
31
36
|
});
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
@@ -35,21 +40,32 @@ function listCommand(ctx, argv) {
|
|
|
35
40
|
keyValue("Backups dir", `${ctx.stateDir}/backups`);
|
|
36
41
|
if (backups.length === 0) {
|
|
37
42
|
warn("no runtime backups found");
|
|
38
|
-
|
|
43
|
+
} else {
|
|
44
|
+
for (const item of backups) {
|
|
45
|
+
info(`${item.id}: ${item.operation}; files=${item.fileCount}; created=${item.createdAt || "unknown"}`);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
if (legacyConflictBackups.length > 0) {
|
|
49
|
+
section("Legacy conflict backups");
|
|
50
|
+
keyValue("Backups dir", path.join(path.dirname(ctx.stateDir), "agent-backups"));
|
|
51
|
+
if (!options.includeLegacy) {
|
|
52
|
+
warn(`${legacyConflictBackups.length} legacy pre-update conflict backups found; rerun with --include-legacy to list them.`);
|
|
53
|
+
} else {
|
|
54
|
+
for (const item of legacyConflictBackups) {
|
|
55
|
+
info(`${item.id}: ${item.operation}; files=${item.fileCount}; bytes=${item.totalBytes}; created=${item.createdAt || "unknown"}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
function inspectCommand(ctx, argv) {
|
|
46
62
|
const { options, positionals } = parseCommandOptions(argv, {
|
|
47
|
-
bools: ["json"],
|
|
63
|
+
bools: ["json", "legacy"],
|
|
48
64
|
});
|
|
49
|
-
const backup = inspectRuntimeBackup(ctx, positionals[0]);
|
|
65
|
+
const backup = options.legacy ? inspectLegacyConflictBackup(ctx, positionals[0]) : inspectRuntimeBackup(ctx, positionals[0]);
|
|
50
66
|
if (ctx.json || options.json) {
|
|
51
67
|
printJson({
|
|
52
|
-
schema: "pi67.backup-inspect.v1",
|
|
68
|
+
schema: options.legacy ? "pi67.legacy-conflict-backup-inspect.v1" : "pi67.backup-inspect.v1",
|
|
53
69
|
createdAt: new Date().toISOString(),
|
|
54
70
|
backup,
|
|
55
71
|
});
|
|
@@ -60,8 +76,13 @@ function inspectCommand(ctx, argv) {
|
|
|
60
76
|
keyValue("Path", backup.path);
|
|
61
77
|
keyValue("Created", backup.createdAt || "unknown");
|
|
62
78
|
keyValue("Operation", backup.operation);
|
|
79
|
+
if (backup.reason) keyValue("Reason", backup.reason);
|
|
63
80
|
keyValue("Files", `${backup.fileCount}${backup.preservedCount === undefined ? "" : ` present / ${backup.preservedCount} preserved slots`}`);
|
|
64
81
|
for (const file of backup.files) {
|
|
82
|
+
if (file.name) {
|
|
83
|
+
info(`${file.name}${file.bytes === undefined ? "" : ` bytes=${file.bytes}`}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
65
86
|
if (file.exists === false) {
|
|
66
87
|
info(`${file.path} missing-at-backup-time`);
|
|
67
88
|
} else {
|
|
@@ -111,13 +132,15 @@ function printBackupsHelp() {
|
|
|
111
132
|
process.stdout.write(`pi-67 backups - inspect and restore preserved runtime backups
|
|
112
133
|
|
|
113
134
|
Usage:
|
|
114
|
-
pi-67 backups list [--json]
|
|
115
|
-
pi-67 backups inspect <backup-id-or-path> [--json]
|
|
135
|
+
pi-67 backups list [--include-legacy] [--json]
|
|
136
|
+
pi-67 backups inspect <backup-id-or-path> [--legacy] [--json]
|
|
116
137
|
pi-67 backups restore --from <backup-id-or-path> [--dry-run] [--yes] [--json]
|
|
117
138
|
|
|
118
139
|
Examples:
|
|
119
140
|
pi-67 backups list
|
|
141
|
+
pi-67 backups list --include-legacy
|
|
120
142
|
pi-67 backups inspect 20260707T120000Z-update
|
|
143
|
+
pi-67 backups inspect pre-update-20260707-235901 --legacy
|
|
121
144
|
pi-67 backups restore --from 20260707T120000Z-update --dry-run
|
|
122
145
|
pi-67 backups restore --from 20260707T120000Z-update --yes
|
|
123
146
|
`);
|
|
@@ -37,7 +37,7 @@ function list(ctx, argv) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function doctor(ctx, argv) {
|
|
40
|
+
async function doctor(ctx, argv) {
|
|
41
41
|
const { options } = parseCommandOptions(argv, {
|
|
42
42
|
bools: ["json", "strict-shared-skills", "no-remote"],
|
|
43
43
|
});
|
|
@@ -46,7 +46,7 @@ function doctor(ctx, argv) {
|
|
|
46
46
|
manifest,
|
|
47
47
|
requiredIds: REQUIRED_EXTENSION_REGISTRY_IDS,
|
|
48
48
|
});
|
|
49
|
-
const updatePlan = buildUpdatePlan(ctx, {
|
|
49
|
+
const updatePlan = await buildUpdatePlan(ctx, {
|
|
50
50
|
noRemote: ctx.noRemote || options.noRemote,
|
|
51
51
|
strictSharedSkills: options.strictSharedSkills,
|
|
52
52
|
});
|
|
@@ -120,11 +120,11 @@ function inspect(ctx, argv) {
|
|
|
120
120
|
for (const smoke of entry.smoke) info(smoke);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function plan(ctx, argv) {
|
|
123
|
+
async function plan(ctx, argv) {
|
|
124
124
|
const { options } = parseCommandOptions(argv, {
|
|
125
125
|
bools: ["json", "strict-shared-skills", "no-remote"],
|
|
126
126
|
});
|
|
127
|
-
const updatePlan = buildUpdatePlan(ctx, {
|
|
127
|
+
const updatePlan = await buildUpdatePlan(ctx, {
|
|
128
128
|
noRemote: ctx.noRemote || options.noRemote,
|
|
129
129
|
strictSharedSkills: options.strictSharedSkills,
|
|
130
130
|
});
|
|
@@ -18,7 +18,7 @@ export async function publishCheckCommand(ctx, argv) {
|
|
|
18
18
|
const noRemote = ctx.noRemote || options.noRemote;
|
|
19
19
|
const noPack = options.noPack;
|
|
20
20
|
const strict = options.strict;
|
|
21
|
-
const report = buildPublishCheck(ctx, {
|
|
21
|
+
const report = await buildPublishCheck(ctx, {
|
|
22
22
|
noRemote,
|
|
23
23
|
noPack,
|
|
24
24
|
allowFirstPublish: options.allowFirstPublish,
|
|
@@ -35,14 +35,14 @@ export async function publishCheckCommand(ctx, argv) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function buildPublishCheck(ctx, options) {
|
|
38
|
+
async function buildPublishCheck(ctx, options) {
|
|
39
39
|
const pkg = readCliPackageJson();
|
|
40
40
|
const rootPackage = readJsonIfExists(path.join(ctx.repoRoot, "package.json")) || {};
|
|
41
41
|
const versionFile = readTextIfExists(path.join(ctx.repoRoot, "VERSION")).trim();
|
|
42
42
|
const workflowFile = path.join(ctx.repoRoot, ".github", "workflows", "npm-publish.yml");
|
|
43
43
|
const workflow = workflowCheck(workflowFile);
|
|
44
44
|
const git = fs.existsSync(ctx.repoRoot) ? gitStatus(ctx.repoRoot) : { isRepo: false };
|
|
45
|
-
const registry = npmLatestVersion(pkg.name, {
|
|
45
|
+
const registry = await npmLatestVersion(pkg.name, {
|
|
46
46
|
currentVersion: pkg.version,
|
|
47
47
|
noRemote: options.noRemote,
|
|
48
48
|
timeoutMs: 10000,
|
package/src/commands/status.mjs
CHANGED
|
@@ -6,7 +6,7 @@ export async function statusCommand(ctx, argv) {
|
|
|
6
6
|
const { options } = parseCommandOptions(argv, {
|
|
7
7
|
bools: ["json", "no-remote"],
|
|
8
8
|
});
|
|
9
|
-
const plan = buildUpdatePlan(ctx, { noRemote: ctx.noRemote || options.noRemote });
|
|
9
|
+
const plan = await buildUpdatePlan(ctx, { noRemote: ctx.noRemote || options.noRemote });
|
|
10
10
|
if (ctx.json || options.json) {
|
|
11
11
|
printJson(plan);
|
|
12
12
|
return;
|
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
|
@@ -27,7 +27,7 @@ export async function updateCommand(ctx, argv) {
|
|
|
27
27
|
const dryRun = ctx.dryRun || options.dryRun;
|
|
28
28
|
const json = ctx.json || options.json;
|
|
29
29
|
if (options.check) {
|
|
30
|
-
const plan = buildUpdatePlan(ctx, {
|
|
30
|
+
const plan = await buildUpdatePlan(ctx, {
|
|
31
31
|
noRemote: ctx.noRemote || options.noRemote,
|
|
32
32
|
strictSharedSkills: options.strictSharedSkills,
|
|
33
33
|
});
|
|
@@ -39,7 +39,7 @@ export async function updateCommand(ctx, argv) {
|
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const plan = buildUpdatePlan(ctx, {
|
|
42
|
+
const plan = await buildUpdatePlan(ctx, {
|
|
43
43
|
noRemote: ctx.noRemote || options.noRemote,
|
|
44
44
|
strictSharedSkills: options.strictSharedSkills,
|
|
45
45
|
});
|
|
@@ -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()
|
package/src/lib/npm-registry.mjs
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { captureCommand } from "./shell-runner.mjs";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const DEFAULT_REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
4
|
+
|
|
5
|
+
export async function npmLatestVersion(packageName, options = {}) {
|
|
4
6
|
if (options.noRemote) {
|
|
5
7
|
return { skipped: true, ok: false, latestVersion: "", outdated: false, message: "remote checks skipped" };
|
|
6
8
|
}
|
|
7
|
-
const result =
|
|
8
|
-
timeoutMs: options.timeoutMs || 8000,
|
|
9
|
-
});
|
|
9
|
+
const result = await npmRegistryLatestPayload(packageName, options);
|
|
10
10
|
if (!result.ok) {
|
|
11
|
-
const rawMessage = result.stderr || result.error || "npm registry lookup failed";
|
|
12
11
|
return {
|
|
13
12
|
skipped: false,
|
|
14
13
|
ok: false,
|
|
15
14
|
latestVersion: "",
|
|
16
15
|
outdated: false,
|
|
17
|
-
message:
|
|
16
|
+
message: result.message,
|
|
18
17
|
};
|
|
19
18
|
}
|
|
20
|
-
const latestVersion =
|
|
19
|
+
const latestVersion = parseNpmLatestPayload(result.payload);
|
|
21
20
|
return {
|
|
22
21
|
skipped: false,
|
|
23
22
|
ok: Boolean(latestVersion),
|
|
@@ -27,6 +26,55 @@ export function npmLatestVersion(packageName, options = {}) {
|
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
export function npmRegistryPackageUrl(packageName, options = {}) {
|
|
30
|
+
const baseUrl = String(options.registryBaseUrl || DEFAULT_REGISTRY_BASE_URL).replace(/\/+$/, "");
|
|
31
|
+
const encodedPackage = encodeURIComponent(String(packageName || "")).replace(/^%40/, "@");
|
|
32
|
+
return `${baseUrl}/${encodedPackage}/latest`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function npmRegistryLatestPayload(packageName, options = {}) {
|
|
36
|
+
const timeoutMs = options.timeoutMs || 8000;
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
+
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
40
|
+
if (typeof fetchImpl !== "function") {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
message: "npm registry lookup failed: fetch unavailable in this Node runtime",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetchImpl(npmRegistryPackageUrl(packageName, options), {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
headers: {
|
|
51
|
+
accept: "application/json",
|
|
52
|
+
"user-agent": "pi-67-manager",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: response.status === 404
|
|
59
|
+
? "not published on npm registry yet"
|
|
60
|
+
: `npm registry lookup failed: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return { ok: true, payload: await response.json() };
|
|
65
|
+
} catch {
|
|
66
|
+
return { ok: false, message: "npm registry returned invalid JSON" };
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
message: registryErrorMessage(error),
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
30
78
|
export function npmPackageScopeStatus(packageName, options = {}) {
|
|
31
79
|
const scope = packageScope(packageName);
|
|
32
80
|
if (!scope) {
|
|
@@ -228,15 +276,14 @@ function packageScope(packageName) {
|
|
|
228
276
|
return match ? match[1] : "";
|
|
229
277
|
}
|
|
230
278
|
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
279
|
+
function parseNpmLatestPayload(payload) {
|
|
280
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return "";
|
|
281
|
+
return typeof payload.version === "string" ? payload.version.trim() : "";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function registryErrorMessage(error) {
|
|
285
|
+
if (error?.name === "AbortError") return "npm registry lookup timed out";
|
|
286
|
+
return `npm registry lookup failed: ${compactMessage(error?.message || error || "unknown error")}`;
|
|
240
287
|
}
|
|
241
288
|
|
|
242
289
|
function parseJsonObject(value) {
|
package/src/lib/shell-runner.mjs
CHANGED
|
@@ -41,21 +41,34 @@ export function captureCommand(command, args = [], options = {}) {
|
|
|
41
41
|
|
|
42
42
|
export function commandCandidatesForPlatform(command, platform = process.platform) {
|
|
43
43
|
if (platform === "win32" && command === "npm") {
|
|
44
|
-
return ["npm", "npm.cmd"];
|
|
44
|
+
return ["npm", "npm.cmd", "cmd.exe"];
|
|
45
45
|
}
|
|
46
46
|
return [command];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function spawnWithFallback(command, args, options) {
|
|
50
50
|
let lastResult;
|
|
51
|
-
for (const candidate of
|
|
52
|
-
const result = spawnSync(candidate, args, options);
|
|
51
|
+
for (const candidate of commandInvocationsForPlatform(command, args)) {
|
|
52
|
+
const result = spawnSync(candidate.command, candidate.args, options);
|
|
53
53
|
lastResult = result;
|
|
54
|
-
if (!
|
|
54
|
+
if (!isRetryableSpawnFailure(command, result)) return { command: candidate.command, result };
|
|
55
55
|
}
|
|
56
56
|
return { command, result: lastResult };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
60
|
-
|
|
59
|
+
function commandInvocationsForPlatform(command, args, platform = process.platform) {
|
|
60
|
+
if (platform === "win32" && command === "npm") {
|
|
61
|
+
return [
|
|
62
|
+
{ command: "npm", args },
|
|
63
|
+
{ command: "npm.cmd", args },
|
|
64
|
+
{ command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "npm.cmd", ...args] },
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
return [{ command, args }];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRetryableSpawnFailure(command, result, platform = process.platform) {
|
|
71
|
+
if (result?.error?.code === "ENOENT") return true;
|
|
72
|
+
if (platform === "win32" && command === "npm" && result?.error?.code === "EINVAL") return true;
|
|
73
|
+
return false;
|
|
61
74
|
}
|
package/src/lib/update-plan.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { npmLatestVersion } from "./npm-registry.mjs";
|
|
|
10
10
|
import { buildDistroManifest } from "./distro-manifest.mjs";
|
|
11
11
|
import { PRESERVED_RUNTIME_FILES } from "./update-safety.mjs";
|
|
12
12
|
|
|
13
|
-
export function buildUpdatePlan(ctx, options = {}) {
|
|
13
|
+
export async function buildUpdatePlan(ctx, options = {}) {
|
|
14
14
|
const versionFile = path.join(ctx.repoRoot, "VERSION");
|
|
15
15
|
const settings = readJsonFileIfExists(path.join(ctx.agentDir, "settings.json")) || {};
|
|
16
16
|
const theme = currentTheme(ctx);
|
|
@@ -35,7 +35,7 @@ export function buildUpdatePlan(ctx, options = {}) {
|
|
|
35
35
|
fs.existsSync(path.join(ctx.repoRoot, "scripts", name)),
|
|
36
36
|
]));
|
|
37
37
|
const pkg = readCliPackageJson();
|
|
38
|
-
const managerRegistry = npmLatestVersion(pkg.name, {
|
|
38
|
+
const managerRegistry = await npmLatestVersion(pkg.name, {
|
|
39
39
|
currentVersion: pkg.version,
|
|
40
40
|
noRemote: options.noRemote,
|
|
41
41
|
});
|
|
@@ -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) {
|
|
@@ -116,6 +125,16 @@ export function listRuntimeBackups(ctx) {
|
|
|
116
125
|
.sort((left, right) => String(right.createdAt || right.id).localeCompare(String(left.createdAt || left.id)));
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
export function listLegacyConflictBackups(ctx) {
|
|
129
|
+
const root = legacyConflictBackupsDir(ctx);
|
|
130
|
+
if (!fs.existsSync(root)) return [];
|
|
131
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
132
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pre-update-"))
|
|
133
|
+
.map((entry) => legacyConflictBackupSummary(path.join(root, entry.name)))
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.sort((left, right) => String(right.createdAt || right.id).localeCompare(String(left.createdAt || left.id)));
|
|
136
|
+
}
|
|
137
|
+
|
|
119
138
|
export function inspectRuntimeBackup(ctx, input) {
|
|
120
139
|
const backupDir = resolveBackupDir(ctx, input);
|
|
121
140
|
const summary = backupSummary(backupDir);
|
|
@@ -125,6 +144,15 @@ export function inspectRuntimeBackup(ctx, input) {
|
|
|
125
144
|
return summary;
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
export function inspectLegacyConflictBackup(ctx, input) {
|
|
148
|
+
const backupDir = resolveLegacyConflictBackupDir(ctx, input);
|
|
149
|
+
const summary = legacyConflictBackupSummary(backupDir, { includeFiles: true });
|
|
150
|
+
if (!summary) {
|
|
151
|
+
throw new CliError(`legacy conflict backup not found or unreadable: ${backupDir}`);
|
|
152
|
+
}
|
|
153
|
+
return summary;
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
export function restoreRuntimeBackup(ctx, input, options = {}) {
|
|
129
157
|
const backup = inspectRuntimeBackup(ctx, input);
|
|
130
158
|
const dryRun = Boolean(options.dryRun);
|
|
@@ -227,6 +255,39 @@ function isStaleLock(lockPath) {
|
|
|
227
255
|
return false;
|
|
228
256
|
}
|
|
229
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
|
+
|
|
230
291
|
function sha256File(file) {
|
|
231
292
|
const hash = crypto.createHash("sha256");
|
|
232
293
|
hash.update(fs.readFileSync(file));
|
|
@@ -267,6 +328,46 @@ function backupSummary(backupDir) {
|
|
|
267
328
|
}
|
|
268
329
|
}
|
|
269
330
|
|
|
331
|
+
function legacyConflictBackupsDir(ctx) {
|
|
332
|
+
return path.join(path.dirname(ctx.stateDir), "agent-backups");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function legacyConflictBackupSummary(backupDir, options = {}) {
|
|
336
|
+
try {
|
|
337
|
+
if (!fs.existsSync(backupDir) || !fs.statSync(backupDir).isDirectory()) return null;
|
|
338
|
+
const entries = fs.readdirSync(backupDir, { withFileTypes: true })
|
|
339
|
+
.filter((entry) => entry.isFile())
|
|
340
|
+
.map((entry) => {
|
|
341
|
+
const file = path.join(backupDir, entry.name);
|
|
342
|
+
const stat = fs.statSync(file);
|
|
343
|
+
return {
|
|
344
|
+
name: entry.name,
|
|
345
|
+
path: file,
|
|
346
|
+
bytes: stat.size,
|
|
347
|
+
mtime: stat.mtime.toISOString(),
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
const stat = fs.statSync(backupDir);
|
|
351
|
+
const totalBytes = entries.reduce((sum, item) => sum + item.bytes, 0);
|
|
352
|
+
const localDiff = entries.find((item) => item.name === "local.diff");
|
|
353
|
+
return {
|
|
354
|
+
id: path.basename(backupDir),
|
|
355
|
+
path: backupDir,
|
|
356
|
+
schema: "pi67.legacy-conflict-backup.v1",
|
|
357
|
+
createdAt: stat.mtime.toISOString(),
|
|
358
|
+
operation: "known-migration-conflict",
|
|
359
|
+
reason: "PowerShell updater preserved known migration conflict files before restoring them from HEAD",
|
|
360
|
+
fileCount: entries.length,
|
|
361
|
+
totalBytes,
|
|
362
|
+
hasLocalDiff: Boolean(localDiff),
|
|
363
|
+
localDiffPath: localDiff?.path || "",
|
|
364
|
+
files: options.includeFiles ? entries : undefined,
|
|
365
|
+
};
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
270
371
|
function resolveBackupDir(ctx, input) {
|
|
271
372
|
if (!input) throw new CliError("backup id/path is required", 2);
|
|
272
373
|
const expanded = expandHome(String(input));
|
|
@@ -276,6 +377,15 @@ function resolveBackupDir(ctx, input) {
|
|
|
276
377
|
return path.join(ctx.stateDir, "backups", expanded);
|
|
277
378
|
}
|
|
278
379
|
|
|
380
|
+
function resolveLegacyConflictBackupDir(ctx, input) {
|
|
381
|
+
if (!input) throw new CliError("legacy backup id/path is required", 2);
|
|
382
|
+
const expanded = expandHome(String(input));
|
|
383
|
+
if (path.isAbsolute(expanded) || expanded.includes("/") || expanded.includes("\\")) {
|
|
384
|
+
return path.resolve(expanded);
|
|
385
|
+
}
|
|
386
|
+
return path.join(legacyConflictBackupsDir(ctx), expanded);
|
|
387
|
+
}
|
|
388
|
+
|
|
279
389
|
function inferBackupOperation(id) {
|
|
280
390
|
if (id.endsWith("-update")) return "update";
|
|
281
391
|
if (id.endsWith("-repair")) return "repair";
|