@bigking67/pi-67 0.10.2 → 0.10.4
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 +34 -0
- package/README.md +17 -8
- package/package.json +1 -1
- package/scripts/check.mjs +98 -0
- package/src/cli.mjs +6 -3
- package/src/commands/backups.mjs +187 -1
- package/src/commands/doctor.mjs +24 -0
- package/src/commands/extensions.mjs +6 -1
- package/src/commands/external.mjs +28 -0
- package/src/commands/install.mjs +22 -0
- package/src/commands/manifest.mjs +21 -0
- package/src/commands/publish-check.mjs +24 -0
- package/src/commands/report.mjs +22 -0
- package/src/commands/self-update.mjs +19 -0
- package/src/commands/skills.mjs +83 -4
- package/src/commands/smoke.mjs +21 -0
- package/src/commands/status.mjs +20 -0
- package/src/commands/themes.mjs +33 -0
- package/src/commands/update.mjs +49 -1
- package/src/commands/version.mjs +22 -1
- package/src/commands/xtalpi.mjs +81 -2
- package/src/lib/args.mjs +7 -1
- package/src/lib/skill-policy.mjs +130 -3
- package/src/lib/update-plan.mjs +52 -5
- package/src/lib/update-safety.mjs +85 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.4]
|
|
4
|
+
|
|
5
|
+
- Makes all command-level `--help` paths side-effect free, including
|
|
6
|
+
`pi-67 update --help`, with self-tests proving help output does not create
|
|
7
|
+
runtime backup directories.
|
|
8
|
+
- Blocks unsafe dirty update plans before the update lifecycle starts, so a
|
|
9
|
+
blocked `pi-67 update` does not first write a runtime backup.
|
|
10
|
+
- Adds `pi-67 backups prune` and `pi-67 backups archive` for dry-run-first
|
|
11
|
+
backup retention, with per-kind `--keep-last` semantics.
|
|
12
|
+
- Adds `pi-67 skills plan` and `pi-67 skills diff <name>` for explicit
|
|
13
|
+
shared-skill drift review without overwriting existing global skills.
|
|
14
|
+
- Adds `pi-67 xtalpi trend`, `pi-67 xtalpi drift`, and `pi-67 xtalpi stress
|
|
15
|
+
--until-done`; drift defaults to full-suite artifacts to avoid targeted smoke
|
|
16
|
+
noise.
|
|
17
|
+
- Aligns runtime backup ownership around script-level
|
|
18
|
+
`pre-update-runtime-*` snapshots while the npm manager owns update planning,
|
|
19
|
+
locking, and orchestration.
|
|
20
|
+
- Reuses equivalent runtime backups across both `manifest.json` and
|
|
21
|
+
`backup-manifest.json` formats.
|
|
22
|
+
- Expands Windows CI coverage to Node 22 and Node 24 plus npm manager CLI
|
|
23
|
+
contract smoke commands.
|
|
24
|
+
|
|
25
|
+
## [0.10.3]
|
|
26
|
+
|
|
27
|
+
- Deduplicates real update/repair runtime backups when preserved config files
|
|
28
|
+
are unchanged from the latest same-operation backup.
|
|
29
|
+
- Deduplicates direct PowerShell `scripts/pi67-update.ps1` dirty runtime-config
|
|
30
|
+
preservation backups under `~/.pi/pi67/backups/`.
|
|
31
|
+
- Removes the legacy PowerShell updater path that wrote new
|
|
32
|
+
`~/.pi/agent-backups/pre-update-*` snapshots; runtime preservation now stays
|
|
33
|
+
under `~/.pi/pi67/backups/`.
|
|
34
|
+
- Avoids writing a theme backup when `pi-67 themes set <name>` is already the
|
|
35
|
+
active theme.
|
|
36
|
+
|
|
3
37
|
## [0.10.2]
|
|
4
38
|
|
|
5
39
|
- Reads `Manager latest` through the npm registry HTTP API instead of spawning
|
package/README.md
CHANGED
|
@@ -73,11 +73,17 @@ check or repair before trusting the globally installed manager.
|
|
|
73
73
|
- user-added global skills
|
|
74
74
|
- dirty external repos such as browser67 or design-craft
|
|
75
75
|
|
|
76
|
-
Before a real `update` or `repair`, the npm manager
|
|
77
|
-
|
|
78
|
-
`~/.pi/pi67/
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
Before a real `update` or `repair`, the npm manager builds the update plan,
|
|
77
|
+
blocks unsafe non-runtime dirty worktrees, and acquires
|
|
78
|
+
`~/.pi/pi67/locks/update.lock`. Runtime config backup/restore is delegated to
|
|
79
|
+
the Bash or PowerShell updater script only when an in-place checkout needs to
|
|
80
|
+
temporarily clear dirty preserved runtime files for `git pull --ff-only`. Those
|
|
81
|
+
script-level snapshots live under `~/.pi/pi67/backups/pre-update-runtime-*`.
|
|
82
|
+
This keeps `--help`, blocked update plans, and the public `npx -y
|
|
83
|
+
@bigking67/pi-67@latest update --repair` orchestration path free of duplicate
|
|
84
|
+
manager-owned runtime backups. If an identical runtime snapshot already exists,
|
|
85
|
+
the script-level updater reuses it instead of writing another timestamped
|
|
86
|
+
directory.
|
|
81
87
|
|
|
82
88
|
Runtime backups are first-class CLI state:
|
|
83
89
|
|
|
@@ -88,15 +94,18 @@ pi-67 backups inspect <backup-id-or-path>
|
|
|
88
94
|
pi-67 backups inspect <pre-update-id> --legacy
|
|
89
95
|
pi-67 backups restore --from <backup-id-or-path> --dry-run
|
|
90
96
|
pi-67 backups restore --from <backup-id-or-path> --yes
|
|
97
|
+
pi-67 backups prune --keep-last 10 --dry-run
|
|
98
|
+
pi-67 backups archive --keep-last 10 --older-than 30d --dry-run
|
|
91
99
|
```
|
|
92
100
|
|
|
93
101
|
The restore command only restores preserved runtime files and writes a
|
|
94
102
|
pre-restore backup before overwriting current local config.
|
|
95
103
|
|
|
96
104
|
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
|
|
105
|
+
known-conflict snapshots from older migration paths; the normal updater no
|
|
106
|
+
longer writes new ones. They are listed only with `--include-legacy` and are
|
|
107
|
+
intentionally separate from restorable runtime backups under
|
|
108
|
+
`~/.pi/pi67/backups/`.
|
|
100
109
|
|
|
101
110
|
Theme changes are explicit only:
|
|
102
111
|
|
package/package.json
CHANGED
package/scripts/check.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { npmLatestVersion, npmPublishTargetStatus, npmRegistryPackageUrl } from "../src/lib/npm-registry.mjs";
|
|
7
7
|
import { commandCandidatesForPlatform } from "../src/lib/shell-runner.mjs";
|
|
8
|
+
import { parseCommandOptions, splitGlobalArgs } from "../src/lib/args.mjs";
|
|
8
9
|
import { readExtensionRegistry, validateExtensionRegistry } from "../src/lib/extension-registry.mjs";
|
|
9
10
|
import { buildPlanDecisions, classifyGitShort } from "../src/lib/update-plan.mjs";
|
|
10
11
|
import {
|
|
@@ -29,6 +30,8 @@ for (const file of files.filter((item) => item.endsWith(".json"))) {
|
|
|
29
30
|
JSON.parse(fs.readFileSync(file, "utf8"));
|
|
30
31
|
}
|
|
31
32
|
await runNpmRegistrySelfTests();
|
|
33
|
+
runArgsSelfTests();
|
|
34
|
+
runCliHelpContractSelfTests();
|
|
32
35
|
runPublishTargetSelfTests();
|
|
33
36
|
runShellRunnerSelfTests();
|
|
34
37
|
runExtensionRegistrySelfTests();
|
|
@@ -44,6 +47,67 @@ function walk(dir, files) {
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
function runArgsSelfTests() {
|
|
51
|
+
const afterCommandHelp = splitGlobalArgs(["skills", "--help"]);
|
|
52
|
+
assert(
|
|
53
|
+
!afterCommandHelp.globals.help && afterCommandHelp.rest.join(" ") === "skills --help",
|
|
54
|
+
"command-level --help must not be consumed as global help after a command is seen",
|
|
55
|
+
);
|
|
56
|
+
const globalHelp = splitGlobalArgs(["--help"]);
|
|
57
|
+
assert(globalHelp.globals.help && globalHelp.rest.length === 0, "global --help must still be parsed globally");
|
|
58
|
+
assert(
|
|
59
|
+
parseCommandOptions(["--help"], { bools: [] }).options.help,
|
|
60
|
+
"command option parser must accept --help for every command",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runCliHelpContractSelfTests() {
|
|
65
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-help-contract-"));
|
|
66
|
+
const home = path.join(tmpRoot, "home");
|
|
67
|
+
const agentDir = path.join(tmpRoot, "agent");
|
|
68
|
+
const skillsDir = path.join(tmpRoot, "skills");
|
|
69
|
+
fs.mkdirSync(home, { recursive: true });
|
|
70
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
71
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
72
|
+
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
|
73
|
+
const globalArgs = ["--agent-dir", agentDir, "--repo-root", root, "--skills-dir", skillsDir];
|
|
74
|
+
const commands = [
|
|
75
|
+
["--help"],
|
|
76
|
+
["install", "--help"],
|
|
77
|
+
["update", "--help"],
|
|
78
|
+
["doctor", "--help"],
|
|
79
|
+
["smoke", "--help"],
|
|
80
|
+
["status", "--help"],
|
|
81
|
+
["report", "--help"],
|
|
82
|
+
["version", "--help"],
|
|
83
|
+
["xtalpi", "--help"],
|
|
84
|
+
["themes", "--help"],
|
|
85
|
+
["skills", "--help"],
|
|
86
|
+
["extensions", "--help"],
|
|
87
|
+
["external", "--help"],
|
|
88
|
+
["self-update", "--help"],
|
|
89
|
+
["publish-check", "--help"],
|
|
90
|
+
["manifest", "--help"],
|
|
91
|
+
["backups", "--help"],
|
|
92
|
+
];
|
|
93
|
+
const backupRoot = path.join(home, ".pi", "pi67", "backups");
|
|
94
|
+
for (const command of commands) {
|
|
95
|
+
const result = spawnSync(process.execPath, [path.join(root, "bin", "pi-67.mjs"), ...globalArgs, ...command], {
|
|
96
|
+
cwd: root,
|
|
97
|
+
env,
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
});
|
|
100
|
+
assert(result.status === 0, `help command failed: pi-67 ${command.join(" ")}\n${result.stderr || result.stdout}`);
|
|
101
|
+
assert(!result.stderr.trim(), `help command wrote stderr: pi-67 ${command.join(" ")}\n${result.stderr}`);
|
|
102
|
+
assert(result.stdout.includes("Usage:"), `help command must print Usage: pi-67 ${command.join(" ")}`);
|
|
103
|
+
}
|
|
104
|
+
assert(
|
|
105
|
+
!fs.existsSync(backupRoot) || fs.readdirSync(backupRoot).length === 0,
|
|
106
|
+
"help commands must not create runtime backups",
|
|
107
|
+
);
|
|
108
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
47
111
|
function runPublishTargetSelfTests() {
|
|
48
112
|
const unpublished = {
|
|
49
113
|
skipped: false,
|
|
@@ -370,6 +434,40 @@ function runUpdateSafetySelfTests() {
|
|
|
370
434
|
listRuntimeBackups(ctx).some((item) => item.path === lifecycle.backupDir),
|
|
371
435
|
"update lifecycle backups must be listable",
|
|
372
436
|
);
|
|
437
|
+
const backupCountBeforeDedupe = listRuntimeBackups(ctx).length;
|
|
438
|
+
const duplicateLifecycle = beginUpdateLifecycle({
|
|
439
|
+
agentDir,
|
|
440
|
+
repoRoot: agentDir,
|
|
441
|
+
stateDir,
|
|
442
|
+
}, {
|
|
443
|
+
operation: "test",
|
|
444
|
+
});
|
|
445
|
+
assert(duplicateLifecycle.backupSkipped, "unchanged update lifecycle snapshots must be deduplicated");
|
|
446
|
+
duplicateLifecycle.release();
|
|
447
|
+
assert(
|
|
448
|
+
listRuntimeBackups(ctx).length === backupCountBeforeDedupe,
|
|
449
|
+
"deduplicated update lifecycle snapshots must not create new backup directories",
|
|
450
|
+
);
|
|
451
|
+
const backupCountBeforeDelegated = listRuntimeBackups(ctx).length;
|
|
452
|
+
const delegatedLifecycle = beginUpdateLifecycle({
|
|
453
|
+
agentDir,
|
|
454
|
+
repoRoot: agentDir,
|
|
455
|
+
stateDir,
|
|
456
|
+
}, {
|
|
457
|
+
operation: "test-delegated",
|
|
458
|
+
backupRuntime: false,
|
|
459
|
+
});
|
|
460
|
+
assert(fs.existsSync(delegatedLifecycle.lockPath), "delegated update lifecycle must still acquire a lock");
|
|
461
|
+
assert(
|
|
462
|
+
delegatedLifecycle.backupSkipped && !delegatedLifecycle.backupDir && delegatedLifecycle.backedUp.length === 0,
|
|
463
|
+
"delegated update lifecycle must skip manager-owned runtime backups",
|
|
464
|
+
);
|
|
465
|
+
delegatedLifecycle.release();
|
|
466
|
+
assert(!fs.existsSync(delegatedLifecycle.lockPath), "delegated update lifecycle must release lock");
|
|
467
|
+
assert(
|
|
468
|
+
listRuntimeBackups(ctx).length === backupCountBeforeDelegated,
|
|
469
|
+
"delegated update lifecycle must not create runtime backup directories",
|
|
470
|
+
);
|
|
373
471
|
assert(
|
|
374
472
|
inspectRuntimeBackup(ctx, lifecycle.backupDir).fileCount >= 2,
|
|
375
473
|
"update lifecycle backups must be inspectable",
|
package/src/cli.mjs
CHANGED
|
@@ -81,15 +81,15 @@ Commands:
|
|
|
81
81
|
status Read-only status summary
|
|
82
82
|
report Generate pi67-report.json
|
|
83
83
|
version Print manager and distro versions
|
|
84
|
-
xtalpi xtalpi health/smoke/capability helpers
|
|
84
|
+
xtalpi xtalpi health/smoke/capability/trend/drift/stress helpers
|
|
85
85
|
themes current/list/doctor/set without update-time overwrite
|
|
86
|
-
skills inventory/sync/migrate shared skills
|
|
86
|
+
skills inventory/plan/diff/sync/migrate shared skills
|
|
87
87
|
extensions list/doctor/inspect/plan extension ownership policy
|
|
88
88
|
external list/install/update/doctor external repos
|
|
89
89
|
self-update Explicitly update the npm manager package
|
|
90
90
|
publish-check Verify npm publish readiness and trusted publishing
|
|
91
91
|
manifest Show managed package/config/theme ownership policy
|
|
92
|
-
backups list/inspect/restore
|
|
92
|
+
backups list/inspect/restore/prune/archive runtime backups
|
|
93
93
|
|
|
94
94
|
Examples:
|
|
95
95
|
pi-67 install
|
|
@@ -101,8 +101,11 @@ Examples:
|
|
|
101
101
|
pi-67 manifest
|
|
102
102
|
pi-67 manifest --validate
|
|
103
103
|
pi-67 backups list
|
|
104
|
+
pi-67 backups prune --keep-last 10 --dry-run
|
|
104
105
|
pi-67 extensions doctor
|
|
106
|
+
pi-67 skills plan
|
|
105
107
|
pi-67 xtalpi smoke --quick
|
|
108
|
+
pi-67 xtalpi trend
|
|
106
109
|
pi-67 themes current
|
|
107
110
|
`);
|
|
108
111
|
}
|
package/src/commands/backups.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import { parseCommandOptions } from "../lib/args.mjs";
|
|
3
5
|
import {
|
|
4
6
|
inspectLegacyConflictBackup,
|
|
@@ -14,6 +16,8 @@ export async function backupsCommand(ctx, argv) {
|
|
|
14
16
|
if (subcommand === "list") return listCommand(ctx, argv.slice(1));
|
|
15
17
|
if (subcommand === "inspect") return inspectCommand(ctx, argv.slice(1));
|
|
16
18
|
if (subcommand === "restore") return restoreCommand(ctx, argv.slice(1));
|
|
19
|
+
if (subcommand === "prune") return pruneCommand(ctx, argv.slice(1));
|
|
20
|
+
if (subcommand === "archive") return archiveCommand(ctx, argv.slice(1));
|
|
17
21
|
if (subcommand === "-h" || subcommand === "--help" || subcommand === "help") {
|
|
18
22
|
printBackupsHelp();
|
|
19
23
|
return;
|
|
@@ -25,6 +29,7 @@ function listCommand(ctx, argv) {
|
|
|
25
29
|
const { options } = parseCommandOptions(argv, {
|
|
26
30
|
bools: ["json", "include-legacy"],
|
|
27
31
|
});
|
|
32
|
+
if (options.help) return printBackupsHelp();
|
|
28
33
|
const backups = listRuntimeBackups(ctx);
|
|
29
34
|
const legacyConflictBackups = listLegacyConflictBackups(ctx);
|
|
30
35
|
if (ctx.json || options.json) {
|
|
@@ -62,6 +67,7 @@ function inspectCommand(ctx, argv) {
|
|
|
62
67
|
const { options, positionals } = parseCommandOptions(argv, {
|
|
63
68
|
bools: ["json", "legacy"],
|
|
64
69
|
});
|
|
70
|
+
if (options.help) return printBackupsHelp();
|
|
65
71
|
const backup = options.legacy ? inspectLegacyConflictBackup(ctx, positionals[0]) : inspectRuntimeBackup(ctx, positionals[0]);
|
|
66
72
|
if (ctx.json || options.json) {
|
|
67
73
|
printJson({
|
|
@@ -96,6 +102,7 @@ function restoreCommand(ctx, argv) {
|
|
|
96
102
|
strings: ["from"],
|
|
97
103
|
bools: ["json", "dry-run", "yes"],
|
|
98
104
|
});
|
|
105
|
+
if (options.help) return printBackupsHelp();
|
|
99
106
|
const dryRun = ctx.dryRun || options.dryRun;
|
|
100
107
|
if (!options.from) {
|
|
101
108
|
throw new CliError("backups restore requires --from <backup-id-or-path>", 2);
|
|
@@ -128,13 +135,186 @@ function restoreCommand(ctx, argv) {
|
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
function pruneCommand(ctx, argv) {
|
|
139
|
+
const { options } = parseCommandOptions(argv, {
|
|
140
|
+
strings: ["keep-last", "older-than"],
|
|
141
|
+
bools: ["json", "dry-run", "yes", "include-legacy"],
|
|
142
|
+
});
|
|
143
|
+
if (options.help) return printBackupsHelp();
|
|
144
|
+
const dryRun = ctx.dryRun || options.dryRun;
|
|
145
|
+
const plan = buildRetentionPlan(ctx, options);
|
|
146
|
+
if (!dryRun && !(ctx.yes || options.yes)) {
|
|
147
|
+
throw new CliError("backups prune deletes backup directories; rerun with --dry-run or --yes", 2);
|
|
148
|
+
}
|
|
149
|
+
if (!dryRun) {
|
|
150
|
+
for (const item of plan.selected) {
|
|
151
|
+
fs.rmSync(item.path, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const result = {
|
|
155
|
+
schema: "pi67.backups-prune.v1",
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
dryRun,
|
|
158
|
+
selector: plan.selector,
|
|
159
|
+
selected: plan.selected,
|
|
160
|
+
kept: plan.kept,
|
|
161
|
+
deleted: dryRun ? [] : plan.selected,
|
|
162
|
+
};
|
|
163
|
+
if (ctx.json || options.json) return printJson(result);
|
|
164
|
+
section("pi-67 backups prune");
|
|
165
|
+
keyValue("Dry run", dryRun ? "yes" : "no");
|
|
166
|
+
keyValue("Selected", result.selected.length);
|
|
167
|
+
keyValue("Kept", result.kept.length);
|
|
168
|
+
for (const item of result.selected) {
|
|
169
|
+
warn(`${dryRun ? "would delete" : "deleted"} ${item.kind}: ${item.id}`);
|
|
170
|
+
}
|
|
171
|
+
if (result.selected.length === 0) pass("no backups matched the retention selector");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function archiveCommand(ctx, argv) {
|
|
175
|
+
const { options } = parseCommandOptions(argv, {
|
|
176
|
+
strings: ["keep-last", "older-than", "to"],
|
|
177
|
+
bools: ["json", "dry-run", "yes", "include-legacy"],
|
|
178
|
+
});
|
|
179
|
+
if (options.help) return printBackupsHelp();
|
|
180
|
+
const dryRun = ctx.dryRun || options.dryRun;
|
|
181
|
+
const plan = buildRetentionPlan(ctx, options);
|
|
182
|
+
const archiveRoot = path.resolve(expandTilde(options.to || path.join(ctx.stateDir, "backups-archive")));
|
|
183
|
+
if (!dryRun && !(ctx.yes || options.yes)) {
|
|
184
|
+
throw new CliError("backups archive moves backup directories; rerun with --dry-run or --yes", 2);
|
|
185
|
+
}
|
|
186
|
+
const archived = [];
|
|
187
|
+
if (!dryRun) {
|
|
188
|
+
fs.mkdirSync(archiveRoot, { recursive: true, mode: 0o700 });
|
|
189
|
+
for (const item of plan.selected) {
|
|
190
|
+
const targetRoot = path.join(archiveRoot, item.kind);
|
|
191
|
+
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o700 });
|
|
192
|
+
const target = uniqueArchivePath(path.join(targetRoot, item.id));
|
|
193
|
+
moveDirectorySync(item.path, target);
|
|
194
|
+
archived.push({ ...item, archivedPath: target });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const result = {
|
|
198
|
+
schema: "pi67.backups-archive.v1",
|
|
199
|
+
createdAt: new Date().toISOString(),
|
|
200
|
+
dryRun,
|
|
201
|
+
archiveRoot,
|
|
202
|
+
selector: plan.selector,
|
|
203
|
+
selected: plan.selected,
|
|
204
|
+
kept: plan.kept,
|
|
205
|
+
archived: dryRun ? [] : archived,
|
|
206
|
+
};
|
|
207
|
+
if (ctx.json || options.json) return printJson(result);
|
|
208
|
+
section("pi-67 backups archive");
|
|
209
|
+
keyValue("Dry run", dryRun ? "yes" : "no");
|
|
210
|
+
keyValue("Archive root", archiveRoot);
|
|
211
|
+
keyValue("Selected", result.selected.length);
|
|
212
|
+
for (const item of result.selected) {
|
|
213
|
+
info(`${dryRun ? "would archive" : "archived"} ${item.kind}: ${item.id}`);
|
|
214
|
+
}
|
|
215
|
+
if (result.selected.length === 0) pass("no backups matched the retention selector");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildRetentionPlan(ctx, options) {
|
|
219
|
+
const keepLast = parseOptionalNonNegativeInt(options.keepLast, "--keep-last");
|
|
220
|
+
const olderThanMs = parseDurationMs(options.olderThan, "--older-than");
|
|
221
|
+
if (keepLast === undefined && olderThanMs === undefined) {
|
|
222
|
+
throw new CliError("backups retention requires --keep-last N and/or --older-than Nd|Nh|Nm", 2);
|
|
223
|
+
}
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const runtime = listRuntimeBackups(ctx).map((item) => ({ ...item, kind: "runtime" }));
|
|
226
|
+
const legacy = options.includeLegacy
|
|
227
|
+
? listLegacyConflictBackups(ctx).map((item) => ({ ...item, kind: "legacy" }))
|
|
228
|
+
: [];
|
|
229
|
+
const all = [...runtime, ...legacy].sort((left, right) => backupTime(right) - backupTime(left));
|
|
230
|
+
const rankByKind = new Map();
|
|
231
|
+
const selected = [];
|
|
232
|
+
const kept = [];
|
|
233
|
+
for (const item of all) {
|
|
234
|
+
const rank = rankByKind.get(item.kind) || 0;
|
|
235
|
+
rankByKind.set(item.kind, rank + 1);
|
|
236
|
+
const keptByCount = keepLast !== undefined && rank < keepLast;
|
|
237
|
+
const oldEnough = olderThanMs === undefined || now - backupTime(item) >= olderThanMs;
|
|
238
|
+
if (!keptByCount && oldEnough) selected.push(item);
|
|
239
|
+
else kept.push(item);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
selector: {
|
|
243
|
+
keepLast: keepLast ?? null,
|
|
244
|
+
olderThan: options.olderThan || "",
|
|
245
|
+
includeLegacy: Boolean(options.includeLegacy),
|
|
246
|
+
},
|
|
247
|
+
selected,
|
|
248
|
+
kept,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseOptionalNonNegativeInt(value, name) {
|
|
253
|
+
if (value === undefined) return undefined;
|
|
254
|
+
const number = Number(value);
|
|
255
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
256
|
+
throw new CliError(`${name} must be a non-negative integer`, 2);
|
|
257
|
+
}
|
|
258
|
+
return number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseDurationMs(value, name) {
|
|
262
|
+
if (value === undefined) return undefined;
|
|
263
|
+
const match = /^(\d+)(m|h|d)$/i.exec(String(value).trim());
|
|
264
|
+
if (!match) throw new CliError(`${name} must look like 30d, 12h, or 90m`, 2);
|
|
265
|
+
const amount = Number(match[1]);
|
|
266
|
+
const unit = match[2].toLowerCase();
|
|
267
|
+
const multiplier = unit === "d" ? 24 * 60 * 60 * 1000 : unit === "h" ? 60 * 60 * 1000 : 60 * 1000;
|
|
268
|
+
return amount * multiplier;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function backupTime(item) {
|
|
272
|
+
const parsed = Date.parse(item.createdAt || "");
|
|
273
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
274
|
+
try {
|
|
275
|
+
return fs.statSync(item.path).mtimeMs;
|
|
276
|
+
} catch {
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function uniqueArchivePath(candidate) {
|
|
282
|
+
if (!fs.existsSync(candidate)) return candidate;
|
|
283
|
+
for (let index = 1; index < 1000; index += 1) {
|
|
284
|
+
const next = `${candidate}.${index}`;
|
|
285
|
+
if (!fs.existsSync(next)) return next;
|
|
286
|
+
}
|
|
287
|
+
throw new CliError(`could not allocate archive path for ${candidate}`, 1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function expandTilde(input) {
|
|
291
|
+
if (input === "~") return os.homedir();
|
|
292
|
+
if (input.startsWith("~/") || input.startsWith("~\\")) {
|
|
293
|
+
return path.join(os.homedir(), input.slice(2));
|
|
294
|
+
}
|
|
295
|
+
return input;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function moveDirectorySync(source, target) {
|
|
299
|
+
try {
|
|
300
|
+
fs.renameSync(source, target);
|
|
301
|
+
return;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error.code !== "EXDEV") throw error;
|
|
304
|
+
}
|
|
305
|
+
fs.cpSync(source, target, { recursive: true, force: false, errorOnExist: true });
|
|
306
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
131
309
|
function printBackupsHelp() {
|
|
132
|
-
process.stdout.write(`pi-67 backups - inspect and
|
|
310
|
+
process.stdout.write(`pi-67 backups - inspect, restore, prune, and archive runtime backups
|
|
133
311
|
|
|
134
312
|
Usage:
|
|
135
313
|
pi-67 backups list [--include-legacy] [--json]
|
|
136
314
|
pi-67 backups inspect <backup-id-or-path> [--legacy] [--json]
|
|
137
315
|
pi-67 backups restore --from <backup-id-or-path> [--dry-run] [--yes] [--json]
|
|
316
|
+
pi-67 backups prune --keep-last N [--older-than 30d] [--include-legacy] [--dry-run|--yes] [--json]
|
|
317
|
+
pi-67 backups archive --keep-last N [--older-than 30d] [--to DIR] [--include-legacy] [--dry-run|--yes] [--json]
|
|
138
318
|
|
|
139
319
|
Examples:
|
|
140
320
|
pi-67 backups list
|
|
@@ -143,5 +323,11 @@ Examples:
|
|
|
143
323
|
pi-67 backups inspect pre-update-20260707-235901 --legacy
|
|
144
324
|
pi-67 backups restore --from 20260707T120000Z-update --dry-run
|
|
145
325
|
pi-67 backups restore --from 20260707T120000Z-update --yes
|
|
326
|
+
pi-67 backups prune --keep-last 10 --dry-run
|
|
327
|
+
pi-67 backups archive --keep-last 10 --older-than 30d --dry-run
|
|
328
|
+
|
|
329
|
+
Retention:
|
|
330
|
+
--keep-last is applied per backup kind. Legacy backups are ignored unless
|
|
331
|
+
--include-legacy is provided. prune/archive require --dry-run or --yes.
|
|
146
332
|
`);
|
|
147
333
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -7,6 +7,10 @@ export async function doctorCommand(ctx, argv) {
|
|
|
7
7
|
bools: ["json", "quiet", "dry-run", "deep-mcp", "strict-shared-skills"],
|
|
8
8
|
strings: ["mcp-timeout-ms"],
|
|
9
9
|
});
|
|
10
|
+
if (options.help) {
|
|
11
|
+
printDoctorHelp();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
10
14
|
const args = isWindows()
|
|
11
15
|
? ["-AgentDir", ctx.agentDir, "-RepoRoot", ctx.repoRoot, "-SkillsDir", ctx.skillsDir]
|
|
12
16
|
: ["--agent-dir", ctx.agentDir, "--repo-root", ctx.repoRoot, "--skills-dir", ctx.skillsDir];
|
|
@@ -19,3 +23,23 @@ export async function doctorCommand(ctx, argv) {
|
|
|
19
23
|
dryRun: ctx.dryRun || options.dryRun,
|
|
20
24
|
});
|
|
21
25
|
}
|
|
26
|
+
|
|
27
|
+
function printDoctorHelp() {
|
|
28
|
+
process.stdout.write(`pi-67 doctor - run readiness diagnostics
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
pi-67 doctor [--json] [--quiet] [--deep-mcp] [--strict-shared-skills]
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--json Emit machine-readable diagnostics.
|
|
35
|
+
--quiet Reduce human output where supported.
|
|
36
|
+
--deep-mcp Run deeper MCP probes on POSIX platforms.
|
|
37
|
+
--mcp-timeout-ms N Timeout for deep MCP probes on POSIX platforms.
|
|
38
|
+
--strict-shared-skills Treat differing shared skills as blocking.
|
|
39
|
+
--dry-run Print the script invocation without running it.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
pi-67 doctor
|
|
43
|
+
pi-67 doctor --json
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
@@ -20,6 +20,7 @@ export async function extensionsCommand(ctx, argv) {
|
|
|
20
20
|
|
|
21
21
|
function list(ctx, argv) {
|
|
22
22
|
const { options } = parseCommandOptions(argv, { bools: ["json"] });
|
|
23
|
+
if (options.help) return printExtensionsHelp();
|
|
23
24
|
const manifest = buildDistroManifest(ctx);
|
|
24
25
|
const data = {
|
|
25
26
|
schema: "pi67.extensions-list.v1",
|
|
@@ -41,6 +42,7 @@ async function doctor(ctx, argv) {
|
|
|
41
42
|
const { options } = parseCommandOptions(argv, {
|
|
42
43
|
bools: ["json", "strict-shared-skills", "no-remote"],
|
|
43
44
|
});
|
|
45
|
+
if (options.help) return printExtensionsHelp();
|
|
44
46
|
const manifest = buildDistroManifest(ctx);
|
|
45
47
|
const validation = validateExtensionRegistry(manifest.extensionRegistry, {
|
|
46
48
|
manifest,
|
|
@@ -94,6 +96,7 @@ async function doctor(ctx, argv) {
|
|
|
94
96
|
|
|
95
97
|
function inspect(ctx, argv) {
|
|
96
98
|
const { options, positionals } = parseCommandOptions(argv, { bools: ["json"] });
|
|
99
|
+
if (options.help) return printExtensionsHelp();
|
|
97
100
|
const id = positionals[0];
|
|
98
101
|
if (!id) throw new CliError("extensions inspect requires an extension id", 2);
|
|
99
102
|
const manifest = buildDistroManifest(ctx);
|
|
@@ -124,6 +127,7 @@ async function plan(ctx, argv) {
|
|
|
124
127
|
const { options } = parseCommandOptions(argv, {
|
|
125
128
|
bools: ["json", "strict-shared-skills", "no-remote"],
|
|
126
129
|
});
|
|
130
|
+
if (options.help) return printExtensionsHelp();
|
|
127
131
|
const updatePlan = await buildUpdatePlan(ctx, {
|
|
128
132
|
noRemote: ctx.noRemote || options.noRemote,
|
|
129
133
|
strictSharedSkills: options.strictSharedSkills,
|
|
@@ -144,7 +148,8 @@ async function plan(ctx, argv) {
|
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
function updateHint(_ctx, argv) {
|
|
147
|
-
const { positionals } = parseCommandOptions(argv, { bools: ["dry-run"] });
|
|
151
|
+
const { options, positionals } = parseCommandOptions(argv, { bools: ["dry-run"] });
|
|
152
|
+
if (options.help) return printExtensionsHelp();
|
|
148
153
|
const id = positionals[0] || "<id>";
|
|
149
154
|
throw new CliError(
|
|
150
155
|
[
|
|
@@ -4,6 +4,10 @@ import { CliError, info, keyValue, printJson, section, warn } from "../lib/outpu
|
|
|
4
4
|
|
|
5
5
|
export async function externalCommand(ctx, argv) {
|
|
6
6
|
const [sub = "list", ...rest] = argv;
|
|
7
|
+
if (sub === "-h" || sub === "--help" || sub === "help") {
|
|
8
|
+
printExternalHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
7
11
|
if (sub === "list") return list(ctx, rest);
|
|
8
12
|
if (sub === "install") return install(ctx, rest);
|
|
9
13
|
if (sub === "update") return update(ctx, rest);
|
|
@@ -13,6 +17,7 @@ export async function externalCommand(ctx, argv) {
|
|
|
13
17
|
|
|
14
18
|
function list(ctx, argv) {
|
|
15
19
|
const { options } = parseCommandOptions(argv, { bools: ["json"] });
|
|
20
|
+
if (options.help) return printExternalHelp();
|
|
16
21
|
const data = { schema: "pi67.external-list.v1", repos: listExternal(ctx) };
|
|
17
22
|
if (ctx.json || options.json) return printJson(data);
|
|
18
23
|
section("pi-67 external repos");
|
|
@@ -23,6 +28,7 @@ function list(ctx, argv) {
|
|
|
23
28
|
|
|
24
29
|
function install(ctx, argv) {
|
|
25
30
|
const { options, positionals } = parseCommandOptions(argv, { bools: ["json", "dry-run"] });
|
|
31
|
+
if (options.help) return printExternalHelp();
|
|
26
32
|
const name = positionals[0];
|
|
27
33
|
assertExternalName(name);
|
|
28
34
|
const data = installExternal(ctx, name, { dryRun: ctx.dryRun || options.dryRun });
|
|
@@ -32,6 +38,7 @@ function install(ctx, argv) {
|
|
|
32
38
|
|
|
33
39
|
function update(ctx, argv) {
|
|
34
40
|
const { options, positionals } = parseCommandOptions(argv, { bools: ["json", "dry-run"] });
|
|
41
|
+
if (options.help) return printExternalHelp();
|
|
35
42
|
const name = positionals[0];
|
|
36
43
|
assertExternalName(name);
|
|
37
44
|
const data = updateExternal(ctx, name, { dryRun: ctx.dryRun || options.dryRun });
|
|
@@ -41,6 +48,7 @@ function update(ctx, argv) {
|
|
|
41
48
|
|
|
42
49
|
function doctor(ctx, argv) {
|
|
43
50
|
const { options, positionals } = parseCommandOptions(argv, { bools: ["json"] });
|
|
51
|
+
if (options.help) return printExternalHelp();
|
|
44
52
|
const name = positionals[0];
|
|
45
53
|
assertExternalName(name);
|
|
46
54
|
const data = externalStatus(ctx, name);
|
|
@@ -61,3 +69,23 @@ function assertExternalName(name) {
|
|
|
61
69
|
if (!name) throw new CliError(`external repo name required. Available: ${Object.keys(EXTERNAL_REPOS).join(", ")}`, 2);
|
|
62
70
|
if (!EXTERNAL_REPOS[name]) throw new CliError(`unknown external repo: ${name}`, 2);
|
|
63
71
|
}
|
|
72
|
+
|
|
73
|
+
function printExternalHelp() {
|
|
74
|
+
process.stdout.write(`pi-67 external - manage external companion repositories
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
pi-67 external list [--json]
|
|
78
|
+
pi-67 external install <browser67|design-craft> [--dry-run] [--json]
|
|
79
|
+
pi-67 external update <browser67|design-craft> [--dry-run] [--json]
|
|
80
|
+
pi-67 external doctor <browser67|design-craft> [--json]
|
|
81
|
+
|
|
82
|
+
Safety:
|
|
83
|
+
External repos are explicit opt-in. Dirty external repos block update instead
|
|
84
|
+
of being overwritten.
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
pi-67 external list
|
|
88
|
+
pi-67 external install browser67 --dry-run
|
|
89
|
+
pi-67 external doctor design-craft
|
|
90
|
+
`);
|
|
91
|
+
}
|
package/src/commands/install.mjs
CHANGED
|
@@ -13,6 +13,10 @@ export async function installCommand(ctx, argv) {
|
|
|
13
13
|
strings: ["repo", "branch"],
|
|
14
14
|
bools: ["dry-run", "yes", "repair"],
|
|
15
15
|
});
|
|
16
|
+
if (options.help) {
|
|
17
|
+
printInstallHelp();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
16
20
|
const dryRun = ctx.dryRun || options.dryRun;
|
|
17
21
|
const repo = options.repo || DEFAULT_REPO_URL;
|
|
18
22
|
|
|
@@ -46,3 +50,21 @@ export async function installCommand(ctx, argv) {
|
|
|
46
50
|
if (!dryRun) writeState(ctx, "install");
|
|
47
51
|
info("Install finished. Run `pi-67 doctor` next.");
|
|
48
52
|
}
|
|
53
|
+
|
|
54
|
+
function printInstallHelp() {
|
|
55
|
+
process.stdout.write(`pi-67 install - clone/install pi-67 safely
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
pi-67 install [--repo URL] [--branch NAME] [--dry-run] [--repair]
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--repo URL Git repository URL. Defaults to the pi-67 upstream repo.
|
|
62
|
+
--branch NAME Clone a specific branch.
|
|
63
|
+
--dry-run Print planned writes without changing files.
|
|
64
|
+
--repair Force owned asset repair during the installer update phase.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
pi-67 install
|
|
68
|
+
pi-67 install --dry-run
|
|
69
|
+
`);
|
|
70
|
+
}
|