@bigking67/pi-67 0.10.1 → 0.10.2
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 +11 -1
- package/package.json +1 -1
- package/scripts/check.mjs +72 -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/update.mjs +2 -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 +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.2]
|
|
4
|
+
|
|
5
|
+
- Reads `Manager latest` through the npm registry HTTP API instead of spawning
|
|
6
|
+
local `npm` / `npm.cmd`, fixing Windows environments where command shims fail
|
|
7
|
+
with `spawnSync npm.cmd EINVAL`.
|
|
8
|
+
- Adds a final Windows `cmd.exe /d /s /c npm.cmd ...` fallback for explicit npm
|
|
9
|
+
operations such as `pi-67 self-update`.
|
|
10
|
+
- Adds read-only visibility for legacy PowerShell
|
|
11
|
+
`~/.pi/agent-backups/pre-update-*` conflict snapshots through
|
|
12
|
+
`pi-67 backups list --include-legacy` and
|
|
13
|
+
`pi-67 backups inspect --legacy <pre-update-id>`.
|
|
14
|
+
|
|
3
15
|
## [0.10.1]
|
|
4
16
|
|
|
5
17
|
- 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
|
|
@@ -80,7 +83,9 @@ Runtime backups are first-class CLI state:
|
|
|
80
83
|
|
|
81
84
|
```bash
|
|
82
85
|
pi-67 backups list
|
|
86
|
+
pi-67 backups list --include-legacy
|
|
83
87
|
pi-67 backups inspect <backup-id-or-path>
|
|
88
|
+
pi-67 backups inspect <pre-update-id> --legacy
|
|
84
89
|
pi-67 backups restore --from <backup-id-or-path> --dry-run
|
|
85
90
|
pi-67 backups restore --from <backup-id-or-path> --yes
|
|
86
91
|
```
|
|
@@ -88,6 +93,11 @@ pi-67 backups restore --from <backup-id-or-path> --yes
|
|
|
88
93
|
The restore command only restores preserved runtime files and writes a
|
|
89
94
|
pre-restore backup before overwriting current local config.
|
|
90
95
|
|
|
96
|
+
Legacy PowerShell `~/.pi/agent-backups/pre-update-*` directories are read-only
|
|
97
|
+
known-conflict snapshots from older migration paths. They are listed only with
|
|
98
|
+
`--include-legacy` and are intentionally separate from restorable runtime
|
|
99
|
+
backups under `~/.pi/pi67/backups/`.
|
|
100
|
+
|
|
91
101
|
Theme changes are explicit only:
|
|
92
102
|
|
|
93
103
|
```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"]),
|
|
@@ -348,6 +394,18 @@ function runUpdateSafetySelfTests() {
|
|
|
348
394
|
fs.readFileSync(path.join(agentDir, "settings.json"), "utf8").includes("\"dark\""),
|
|
349
395
|
"runtime backup restore must recover the backed up file content",
|
|
350
396
|
);
|
|
397
|
+
const legacyRoot = path.join(path.dirname(stateDir), "agent-backups", "pre-update-20260707-235901");
|
|
398
|
+
fs.mkdirSync(legacyRoot, { recursive: true });
|
|
399
|
+
fs.writeFileSync(path.join(legacyRoot, "local.diff"), "diff --git a/settings.json b/settings.json\n");
|
|
400
|
+
fs.writeFileSync(path.join(legacyRoot, "settings.json"), "{\"theme\":\"legacy\"}\n");
|
|
401
|
+
assert(
|
|
402
|
+
listLegacyConflictBackups(ctx).some((item) => item.id === "pre-update-20260707-235901" && item.hasLocalDiff),
|
|
403
|
+
"legacy PowerShell conflict backups must be listable",
|
|
404
|
+
);
|
|
405
|
+
assert(
|
|
406
|
+
inspectLegacyConflictBackup(ctx, "pre-update-20260707-235901").files.some((item) => item.name === "local.diff"),
|
|
407
|
+
"legacy PowerShell conflict backups must be inspectable",
|
|
408
|
+
);
|
|
351
409
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
352
410
|
}
|
|
353
411
|
|
|
@@ -413,6 +471,17 @@ function deepMerge(base, overrides) {
|
|
|
413
471
|
return next;
|
|
414
472
|
}
|
|
415
473
|
|
|
474
|
+
function jsonResponse(status, payload) {
|
|
475
|
+
return {
|
|
476
|
+
ok: status >= 200 && status < 300,
|
|
477
|
+
status,
|
|
478
|
+
statusText: status === 404 ? "Not Found" : "OK",
|
|
479
|
+
async json() {
|
|
480
|
+
return payload;
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
416
485
|
function assert(condition, message) {
|
|
417
486
|
if (!condition) throw new Error(message);
|
|
418
487
|
}
|
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/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
|
});
|
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
|
});
|
|
@@ -116,6 +116,16 @@ export function listRuntimeBackups(ctx) {
|
|
|
116
116
|
.sort((left, right) => String(right.createdAt || right.id).localeCompare(String(left.createdAt || left.id)));
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export function listLegacyConflictBackups(ctx) {
|
|
120
|
+
const root = legacyConflictBackupsDir(ctx);
|
|
121
|
+
if (!fs.existsSync(root)) return [];
|
|
122
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
123
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pre-update-"))
|
|
124
|
+
.map((entry) => legacyConflictBackupSummary(path.join(root, entry.name)))
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.sort((left, right) => String(right.createdAt || right.id).localeCompare(String(left.createdAt || left.id)));
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
export function inspectRuntimeBackup(ctx, input) {
|
|
120
130
|
const backupDir = resolveBackupDir(ctx, input);
|
|
121
131
|
const summary = backupSummary(backupDir);
|
|
@@ -125,6 +135,15 @@ export function inspectRuntimeBackup(ctx, input) {
|
|
|
125
135
|
return summary;
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
export function inspectLegacyConflictBackup(ctx, input) {
|
|
139
|
+
const backupDir = resolveLegacyConflictBackupDir(ctx, input);
|
|
140
|
+
const summary = legacyConflictBackupSummary(backupDir, { includeFiles: true });
|
|
141
|
+
if (!summary) {
|
|
142
|
+
throw new CliError(`legacy conflict backup not found or unreadable: ${backupDir}`);
|
|
143
|
+
}
|
|
144
|
+
return summary;
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
export function restoreRuntimeBackup(ctx, input, options = {}) {
|
|
129
148
|
const backup = inspectRuntimeBackup(ctx, input);
|
|
130
149
|
const dryRun = Boolean(options.dryRun);
|
|
@@ -267,6 +286,46 @@ function backupSummary(backupDir) {
|
|
|
267
286
|
}
|
|
268
287
|
}
|
|
269
288
|
|
|
289
|
+
function legacyConflictBackupsDir(ctx) {
|
|
290
|
+
return path.join(path.dirname(ctx.stateDir), "agent-backups");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function legacyConflictBackupSummary(backupDir, options = {}) {
|
|
294
|
+
try {
|
|
295
|
+
if (!fs.existsSync(backupDir) || !fs.statSync(backupDir).isDirectory()) return null;
|
|
296
|
+
const entries = fs.readdirSync(backupDir, { withFileTypes: true })
|
|
297
|
+
.filter((entry) => entry.isFile())
|
|
298
|
+
.map((entry) => {
|
|
299
|
+
const file = path.join(backupDir, entry.name);
|
|
300
|
+
const stat = fs.statSync(file);
|
|
301
|
+
return {
|
|
302
|
+
name: entry.name,
|
|
303
|
+
path: file,
|
|
304
|
+
bytes: stat.size,
|
|
305
|
+
mtime: stat.mtime.toISOString(),
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
const stat = fs.statSync(backupDir);
|
|
309
|
+
const totalBytes = entries.reduce((sum, item) => sum + item.bytes, 0);
|
|
310
|
+
const localDiff = entries.find((item) => item.name === "local.diff");
|
|
311
|
+
return {
|
|
312
|
+
id: path.basename(backupDir),
|
|
313
|
+
path: backupDir,
|
|
314
|
+
schema: "pi67.legacy-conflict-backup.v1",
|
|
315
|
+
createdAt: stat.mtime.toISOString(),
|
|
316
|
+
operation: "known-migration-conflict",
|
|
317
|
+
reason: "PowerShell updater preserved known migration conflict files before restoring them from HEAD",
|
|
318
|
+
fileCount: entries.length,
|
|
319
|
+
totalBytes,
|
|
320
|
+
hasLocalDiff: Boolean(localDiff),
|
|
321
|
+
localDiffPath: localDiff?.path || "",
|
|
322
|
+
files: options.includeFiles ? entries : undefined,
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
270
329
|
function resolveBackupDir(ctx, input) {
|
|
271
330
|
if (!input) throw new CliError("backup id/path is required", 2);
|
|
272
331
|
const expanded = expandHome(String(input));
|
|
@@ -276,6 +335,15 @@ function resolveBackupDir(ctx, input) {
|
|
|
276
335
|
return path.join(ctx.stateDir, "backups", expanded);
|
|
277
336
|
}
|
|
278
337
|
|
|
338
|
+
function resolveLegacyConflictBackupDir(ctx, input) {
|
|
339
|
+
if (!input) throw new CliError("legacy backup id/path is required", 2);
|
|
340
|
+
const expanded = expandHome(String(input));
|
|
341
|
+
if (path.isAbsolute(expanded) || expanded.includes("/") || expanded.includes("\\")) {
|
|
342
|
+
return path.resolve(expanded);
|
|
343
|
+
}
|
|
344
|
+
return path.join(legacyConflictBackupsDir(ctx), expanded);
|
|
345
|
+
}
|
|
346
|
+
|
|
279
347
|
function inferBackupOperation(id) {
|
|
280
348
|
if (id.endsWith("-update")) return "update";
|
|
281
349
|
if (id.endsWith("-repair")) return "repair";
|