@bigking67/pi-67 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +182 -0
  3. package/bin/pi-67.mjs +13 -0
  4. package/package.json +47 -0
  5. package/schemas/pi67-distro-manifest.schema.json +130 -0
  6. package/schemas/pi67-extension-registry.schema.json +110 -0
  7. package/schemas/pi67-publish-check.schema.json +122 -0
  8. package/schemas/pi67-state.schema.json +31 -0
  9. package/schemas/pi67-update-plan.schema.json +141 -0
  10. package/scripts/check.mjs +401 -0
  11. package/src/cli.mjs +108 -0
  12. package/src/commands/backups.mjs +124 -0
  13. package/src/commands/doctor.mjs +21 -0
  14. package/src/commands/extensions.mjs +184 -0
  15. package/src/commands/external.mjs +63 -0
  16. package/src/commands/install.mjs +48 -0
  17. package/src/commands/manifest.mjs +74 -0
  18. package/src/commands/publish-check.mjs +366 -0
  19. package/src/commands/report.mjs +20 -0
  20. package/src/commands/self-update.mjs +16 -0
  21. package/src/commands/skills.mjs +49 -0
  22. package/src/commands/smoke.mjs +18 -0
  23. package/src/commands/status.mjs +21 -0
  24. package/src/commands/themes.mjs +69 -0
  25. package/src/commands/update.mjs +138 -0
  26. package/src/commands/version.mjs +51 -0
  27. package/src/commands/xtalpi.mjs +69 -0
  28. package/src/data/distro-manifest.json +85 -0
  29. package/src/data/extension-registry.json +147 -0
  30. package/src/lib/args.mjs +87 -0
  31. package/src/lib/config-json.mjs +20 -0
  32. package/src/lib/distro-manifest.mjs +131 -0
  33. package/src/lib/distro-scripts.mjs +27 -0
  34. package/src/lib/extension-registry.mjs +250 -0
  35. package/src/lib/external-repos.mjs +71 -0
  36. package/src/lib/git.mjs +55 -0
  37. package/src/lib/npm-registry.mjs +288 -0
  38. package/src/lib/output.mjs +35 -0
  39. package/src/lib/paths.mjs +79 -0
  40. package/src/lib/platform.mjs +27 -0
  41. package/src/lib/shell-runner.mjs +40 -0
  42. package/src/lib/skill-policy.mjs +85 -0
  43. package/src/lib/state-store.mjs +31 -0
  44. package/src/lib/theme-policy.mjs +34 -0
  45. package/src/lib/update-plan.mjs +312 -0
  46. package/src/lib/update-safety.mjs +310 -0
@@ -0,0 +1,124 @@
1
+ import { parseCommandOptions } from "../lib/args.mjs";
2
+ import {
3
+ inspectRuntimeBackup,
4
+ listRuntimeBackups,
5
+ restoreRuntimeBackup,
6
+ } from "../lib/update-safety.mjs";
7
+ import { CliError, info, keyValue, pass, printJson, section, warn } from "../lib/output.mjs";
8
+
9
+ export async function backupsCommand(ctx, argv) {
10
+ const subcommand = argv[0] || "list";
11
+ if (subcommand === "list") return listCommand(ctx, argv.slice(1));
12
+ if (subcommand === "inspect") return inspectCommand(ctx, argv.slice(1));
13
+ if (subcommand === "restore") return restoreCommand(ctx, argv.slice(1));
14
+ if (subcommand === "-h" || subcommand === "--help" || subcommand === "help") {
15
+ printBackupsHelp();
16
+ return;
17
+ }
18
+ throw new CliError(`unknown backups subcommand: ${subcommand}`, 2);
19
+ }
20
+
21
+ function listCommand(ctx, argv) {
22
+ const { options } = parseCommandOptions(argv, {
23
+ bools: ["json"],
24
+ });
25
+ const backups = listRuntimeBackups(ctx);
26
+ if (ctx.json || options.json) {
27
+ printJson({
28
+ schema: "pi67.backups-list.v1",
29
+ createdAt: new Date().toISOString(),
30
+ backups,
31
+ });
32
+ return;
33
+ }
34
+ section("pi-67 runtime backups");
35
+ keyValue("Backups dir", `${ctx.stateDir}/backups`);
36
+ if (backups.length === 0) {
37
+ warn("no runtime backups found");
38
+ return;
39
+ }
40
+ for (const item of backups) {
41
+ info(`${item.id}: ${item.operation}; files=${item.fileCount}; created=${item.createdAt || "unknown"}`);
42
+ }
43
+ }
44
+
45
+ function inspectCommand(ctx, argv) {
46
+ const { options, positionals } = parseCommandOptions(argv, {
47
+ bools: ["json"],
48
+ });
49
+ const backup = inspectRuntimeBackup(ctx, positionals[0]);
50
+ if (ctx.json || options.json) {
51
+ printJson({
52
+ schema: "pi67.backup-inspect.v1",
53
+ createdAt: new Date().toISOString(),
54
+ backup,
55
+ });
56
+ return;
57
+ }
58
+ section("pi-67 runtime backup");
59
+ keyValue("ID", backup.id);
60
+ keyValue("Path", backup.path);
61
+ keyValue("Created", backup.createdAt || "unknown");
62
+ keyValue("Operation", backup.operation);
63
+ keyValue("Files", `${backup.fileCount}${backup.preservedCount === undefined ? "" : ` present / ${backup.preservedCount} preserved slots`}`);
64
+ for (const file of backup.files) {
65
+ if (file.exists === false) {
66
+ info(`${file.path} missing-at-backup-time`);
67
+ } else {
68
+ info(`${file.path}${file.bytes === undefined ? "" : ` bytes=${file.bytes}`}${file.sha256 ? ` sha256=${file.sha256}` : ""}`);
69
+ }
70
+ }
71
+ }
72
+
73
+ function restoreCommand(ctx, argv) {
74
+ const { options } = parseCommandOptions(argv, {
75
+ strings: ["from"],
76
+ bools: ["json", "dry-run", "yes"],
77
+ });
78
+ const dryRun = ctx.dryRun || options.dryRun;
79
+ if (!options.from) {
80
+ throw new CliError("backups restore requires --from <backup-id-or-path>", 2);
81
+ }
82
+ if (!dryRun && !(ctx.yes || options.yes)) {
83
+ throw new CliError("backups restore overwrites runtime config; rerun with --yes or use --dry-run", 2);
84
+ }
85
+ const result = restoreRuntimeBackup(ctx, options.from, { dryRun });
86
+ if (ctx.json || options.json) {
87
+ printJson(result);
88
+ return;
89
+ }
90
+ section("pi-67 runtime restore");
91
+ keyValue("Source", result.backup.path);
92
+ keyValue("Dry run", result.dryRun ? "yes" : "no");
93
+ if (result.preRestoreBackupDir) {
94
+ keyValue("Pre-restore backup", result.preRestoreBackupDir);
95
+ }
96
+ for (const item of result.restored) {
97
+ pass(`${item.path} restored`);
98
+ }
99
+ for (const item of result.removed) {
100
+ pass(`${item.path} removed (${item.reason})`);
101
+ }
102
+ for (const item of result.missing) {
103
+ warn(`${item.path} missing from backup files: ${item.source}`);
104
+ }
105
+ for (const item of result.skipped) {
106
+ warn(`${item.path} skipped: ${item.reason}`);
107
+ }
108
+ }
109
+
110
+ function printBackupsHelp() {
111
+ process.stdout.write(`pi-67 backups - inspect and restore preserved runtime backups
112
+
113
+ Usage:
114
+ pi-67 backups list [--json]
115
+ pi-67 backups inspect <backup-id-or-path> [--json]
116
+ pi-67 backups restore --from <backup-id-or-path> [--dry-run] [--yes] [--json]
117
+
118
+ Examples:
119
+ pi-67 backups list
120
+ pi-67 backups inspect 20260707T120000Z-update
121
+ pi-67 backups restore --from 20260707T120000Z-update --dry-run
122
+ pi-67 backups restore --from 20260707T120000Z-update --yes
123
+ `);
124
+ }
@@ -0,0 +1,21 @@
1
+ import { parseCommandOptions } from "../lib/args.mjs";
2
+ import { runDistroScript } from "../lib/distro-scripts.mjs";
3
+ import { isWindows } from "../lib/platform.mjs";
4
+
5
+ export async function doctorCommand(ctx, argv) {
6
+ const { options } = parseCommandOptions(argv, {
7
+ bools: ["json", "quiet", "dry-run", "deep-mcp", "strict-shared-skills"],
8
+ strings: ["mcp-timeout-ms"],
9
+ });
10
+ const args = isWindows()
11
+ ? ["-AgentDir", ctx.agentDir, "-RepoRoot", ctx.repoRoot, "-SkillsDir", ctx.skillsDir]
12
+ : ["--agent-dir", ctx.agentDir, "--repo-root", ctx.repoRoot, "--skills-dir", ctx.skillsDir];
13
+ if (ctx.json || options.json) args.push(isWindows() ? "-Json" : "--json");
14
+ if (options.quiet) args.push(isWindows() ? "-Quiet" : "--quiet");
15
+ if (options.strictSharedSkills) args.push(isWindows() ? "-StrictSharedSkills" : "--strict-shared-skills");
16
+ if (!isWindows() && options.deepMcp) args.push("--deep-mcp");
17
+ if (!isWindows() && options.mcpTimeoutMs) args.push("--mcp-timeout-ms", options.mcpTimeoutMs);
18
+ runDistroScript(ctx, { sh: "pi67-doctor.sh", ps1: "pi67-doctor.ps1" }, args, {
19
+ dryRun: ctx.dryRun || options.dryRun,
20
+ });
21
+ }
@@ -0,0 +1,184 @@
1
+ import { parseCommandOptions } from "../lib/args.mjs";
2
+ import { buildDistroManifest } from "../lib/distro-manifest.mjs";
3
+ import { REQUIRED_EXTENSION_REGISTRY_IDS, validateExtensionRegistry } from "../lib/extension-registry.mjs";
4
+ import { buildUpdatePlan } from "../lib/update-plan.mjs";
5
+ import { CliError, fail, info, keyValue, pass, printJson, section, warn } from "../lib/output.mjs";
6
+
7
+ export async function extensionsCommand(ctx, argv) {
8
+ const [sub = "list", ...rest] = argv;
9
+ if (sub === "list") return list(ctx, rest);
10
+ if (sub === "doctor") return doctor(ctx, rest);
11
+ if (sub === "inspect") return inspect(ctx, rest);
12
+ if (sub === "plan") return plan(ctx, rest);
13
+ if (sub === "update") return updateHint(ctx, rest);
14
+ if (sub === "-h" || sub === "--help" || sub === "help") {
15
+ printExtensionsHelp();
16
+ return;
17
+ }
18
+ throw new CliError(`unknown extensions command: ${sub}`, 2);
19
+ }
20
+
21
+ function list(ctx, argv) {
22
+ const { options } = parseCommandOptions(argv, { bools: ["json"] });
23
+ const manifest = buildDistroManifest(ctx);
24
+ const data = {
25
+ schema: "pi67.extensions-list.v1",
26
+ createdAt: new Date().toISOString(),
27
+ extensions: manifest.extensionRegistry.extensions,
28
+ localExtensions: manifest.localExtensions,
29
+ summary: manifest.summary,
30
+ };
31
+ if (ctx.json || options.json) return printJson(data);
32
+ section("pi-67 extension registry");
33
+ keyValue("Registered", data.extensions.length);
34
+ keyValue("Local", `${manifest.summary.localExtensions - manifest.summary.missingLocalExtensions}/${manifest.summary.localExtensions} present`);
35
+ for (const item of data.extensions) {
36
+ info(`${item.id}: ${item.kind}; owner=${item.owner}; update=${item.updateStrategy}`);
37
+ }
38
+ }
39
+
40
+ function doctor(ctx, argv) {
41
+ const { options } = parseCommandOptions(argv, {
42
+ bools: ["json", "strict-shared-skills", "no-remote"],
43
+ });
44
+ const manifest = buildDistroManifest(ctx);
45
+ const validation = validateExtensionRegistry(manifest.extensionRegistry, {
46
+ manifest,
47
+ requiredIds: REQUIRED_EXTENSION_REGISTRY_IDS,
48
+ });
49
+ const updatePlan = buildUpdatePlan(ctx, {
50
+ noRemote: ctx.noRemote || options.noRemote,
51
+ strictSharedSkills: options.strictSharedSkills,
52
+ });
53
+ const data = {
54
+ schema: "pi67.extensions-doctor.v1",
55
+ createdAt: new Date().toISOString(),
56
+ validation,
57
+ localExtensions: manifest.localExtensions,
58
+ policy: {
59
+ theme: manifest.theme.policy,
60
+ sharedSkills: manifest.sharedSkills.policy,
61
+ externalRepos: manifest.externalReposPolicy,
62
+ },
63
+ updatePlan: {
64
+ actions: updatePlan.actions.filter((item) => extensionActionKinds().has(item.kind)),
65
+ blocked: updatePlan.blocked.filter((item) => extensionBlockKinds().has(item.kind)),
66
+ warnings: updatePlan.warnings,
67
+ },
68
+ };
69
+ if (ctx.json || options.json) {
70
+ printJson(data);
71
+ if (!validation.ok) process.exitCode = 1;
72
+ return;
73
+ }
74
+ section("pi-67 extensions doctor");
75
+ if (validation.ok) pass(validation.message);
76
+ else {
77
+ for (const problem of validation.problems) fail(problem);
78
+ process.exitCode = 1;
79
+ }
80
+ for (const warning of validation.warnings) warn(warning);
81
+ section("Local extensions");
82
+ for (const item of manifest.localExtensions) {
83
+ if (item.exists) pass(`${item.name}: ${item.path} (${item.owner})`);
84
+ else warn(`${item.name}: missing ${item.path}`);
85
+ }
86
+ section("Extension-related update plan");
87
+ for (const action of data.updatePlan.actions) {
88
+ info(`${action.id}: ${action.operation}; preserves=${action.preserves.join(", ")}`);
89
+ }
90
+ for (const blocked of data.updatePlan.blocked) {
91
+ warn(`${blocked.id}: ${blocked.reason}`);
92
+ }
93
+ }
94
+
95
+ function inspect(ctx, argv) {
96
+ const { options, positionals } = parseCommandOptions(argv, { bools: ["json"] });
97
+ const id = positionals[0];
98
+ if (!id) throw new CliError("extensions inspect requires an extension id", 2);
99
+ const manifest = buildDistroManifest(ctx);
100
+ const entry = manifest.extensionRegistry.extensions.find((item) => item.id === id);
101
+ if (!entry) throw new CliError(`unknown registered extension: ${id}`, 2);
102
+ const local = manifest.localExtensions.find((item) => item.name === id) || null;
103
+ const data = {
104
+ schema: "pi67.extensions-inspect.v1",
105
+ createdAt: new Date().toISOString(),
106
+ extension: entry,
107
+ local,
108
+ };
109
+ if (ctx.json || options.json) return printJson(data);
110
+ section(`pi-67 extension: ${id}`);
111
+ keyValue("Kind", entry.kind);
112
+ keyValue("Owner", entry.owner);
113
+ keyValue("Install", entry.installStrategy);
114
+ keyValue("Update", entry.updateStrategy);
115
+ keyValue("Repair", entry.repairStrategy);
116
+ if (entry.path) keyValue("Path", entry.path);
117
+ if (entry.target) keyValue("Target", entry.target);
118
+ if (local) keyValue("Local exists", local.exists ? "yes" : "no");
119
+ section("Smoke gates");
120
+ for (const smoke of entry.smoke) info(smoke);
121
+ }
122
+
123
+ function plan(ctx, argv) {
124
+ const { options } = parseCommandOptions(argv, {
125
+ bools: ["json", "strict-shared-skills", "no-remote"],
126
+ });
127
+ const updatePlan = buildUpdatePlan(ctx, {
128
+ noRemote: ctx.noRemote || options.noRemote,
129
+ strictSharedSkills: options.strictSharedSkills,
130
+ });
131
+ const data = {
132
+ schema: "pi67.extensions-plan.v1",
133
+ createdAt: new Date().toISOString(),
134
+ actions: updatePlan.actions.filter((item) => extensionActionKinds().has(item.kind)),
135
+ blocked: updatePlan.blocked.filter((item) => extensionBlockKinds().has(item.kind)),
136
+ warnings: updatePlan.warnings,
137
+ };
138
+ if (ctx.json || options.json) return printJson(data);
139
+ section("pi-67 extension update plan");
140
+ for (const action of data.actions) info(`${action.id}: ${action.operation}`);
141
+ for (const blocked of data.blocked) warn(`${blocked.id}: ${blocked.reason}`);
142
+ for (const warning of data.warnings) warn(warning);
143
+ if (data.actions.length === 0 && data.blocked.length === 0) pass("no extension-related action is required");
144
+ }
145
+
146
+ function updateHint(_ctx, argv) {
147
+ const { positionals } = parseCommandOptions(argv, { bools: ["dry-run"] });
148
+ const id = positionals[0] || "<id>";
149
+ throw new CliError(
150
+ [
151
+ "pi-67 extensions update is intentionally not a generic overwrite entrypoint.",
152
+ `For first-party bundled extensions, run: pi-67 update --repair`,
153
+ `For external repos, run: pi-67 external update ${id}`,
154
+ "For shared skills, run: pi-67 skills sync",
155
+ "For themes, run: pi-67 themes list/current/set explicitly.",
156
+ ].join("\n"),
157
+ 2,
158
+ );
159
+ }
160
+
161
+ function extensionActionKinds() {
162
+ return new Set(["local-extension", "theme-package", "skill-pack"]);
163
+ }
164
+
165
+ function extensionBlockKinds() {
166
+ return new Set(["external-repo", "skill-pack"]);
167
+ }
168
+
169
+ function printExtensionsHelp() {
170
+ process.stdout.write(`pi-67 extensions - inspect pi-67 extension ownership policy
171
+
172
+ Usage:
173
+ pi-67 extensions list [--json]
174
+ pi-67 extensions doctor [--json] [--strict-shared-skills]
175
+ pi-67 extensions inspect <id> [--json]
176
+ pi-67 extensions plan [--json] [--strict-shared-skills]
177
+
178
+ Notes:
179
+ Generic extension overwrite updates are intentionally not supported.
180
+ Use pi-67 update --repair for first-party bundled extensions, pi-67 external
181
+ update <name> for external repos, pi-67 skills sync for shared skills, and
182
+ pi-67 themes set <name> for explicit theme selection changes.
183
+ `);
184
+ }
@@ -0,0 +1,63 @@
1
+ import { parseCommandOptions } from "../lib/args.mjs";
2
+ import { EXTERNAL_REPOS, externalStatus, installExternal, listExternal, updateExternal } from "../lib/external-repos.mjs";
3
+ import { CliError, info, keyValue, printJson, section, warn } from "../lib/output.mjs";
4
+
5
+ export async function externalCommand(ctx, argv) {
6
+ const [sub = "list", ...rest] = argv;
7
+ if (sub === "list") return list(ctx, rest);
8
+ if (sub === "install") return install(ctx, rest);
9
+ if (sub === "update") return update(ctx, rest);
10
+ if (sub === "doctor") return doctor(ctx, rest);
11
+ throw new CliError(`unknown external command: ${sub}`, 2);
12
+ }
13
+
14
+ function list(ctx, argv) {
15
+ const { options } = parseCommandOptions(argv, { bools: ["json"] });
16
+ const data = { schema: "pi67.external-list.v1", repos: listExternal(ctx) };
17
+ if (ctx.json || options.json) return printJson(data);
18
+ section("pi-67 external repos");
19
+ for (const repo of data.repos) {
20
+ keyValue(repo.name, repo.exists ? `${repo.path} ${repo.git?.dirty ? "dirty" : "clean"}` : `missing at ${repo.path}`);
21
+ }
22
+ }
23
+
24
+ function install(ctx, argv) {
25
+ const { options, positionals } = parseCommandOptions(argv, { bools: ["json", "dry-run"] });
26
+ const name = positionals[0];
27
+ assertExternalName(name);
28
+ const data = installExternal(ctx, name, { dryRun: ctx.dryRun || options.dryRun });
29
+ if (ctx.json || options.json) return printJson(data);
30
+ info(`${name}: ${data.action}`);
31
+ }
32
+
33
+ function update(ctx, argv) {
34
+ const { options, positionals } = parseCommandOptions(argv, { bools: ["json", "dry-run"] });
35
+ const name = positionals[0];
36
+ assertExternalName(name);
37
+ const data = updateExternal(ctx, name, { dryRun: ctx.dryRun || options.dryRun });
38
+ if (ctx.json || options.json) return printJson(data);
39
+ info(`${name}: ${data.action}`);
40
+ }
41
+
42
+ function doctor(ctx, argv) {
43
+ const { options, positionals } = parseCommandOptions(argv, { bools: ["json"] });
44
+ const name = positionals[0];
45
+ assertExternalName(name);
46
+ const data = externalStatus(ctx, name);
47
+ if (ctx.json || options.json) return printJson(data);
48
+ section(`external doctor: ${name}`);
49
+ keyValue("Path", data.path);
50
+ keyValue("Exists", data.exists ? "yes" : "no");
51
+ if (!data.exists) {
52
+ warn(`Run: pi-67 external install ${name}`);
53
+ return;
54
+ }
55
+ keyValue("Git", data.git?.isRepo ? "yes" : "no");
56
+ keyValue("Dirty", data.git?.dirty ? "yes" : "no");
57
+ keyValue("Commit", data.git?.commit || "");
58
+ }
59
+
60
+ function assertExternalName(name) {
61
+ if (!name) throw new CliError(`external repo name required. Available: ${Object.keys(EXTERNAL_REPOS).join(", ")}`, 2);
62
+ if (!EXTERNAL_REPOS[name]) throw new CliError(`unknown external repo: ${name}`, 2);
63
+ }
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parseCommandOptions } from "../lib/args.mjs";
4
+ import { DEFAULT_REPO_URL } from "../lib/paths.mjs";
5
+ import { runCommand } from "../lib/shell-runner.mjs";
6
+ import { runDistroScript } from "../lib/distro-scripts.mjs";
7
+ import { isWindows } from "../lib/platform.mjs";
8
+ import { CliError, info, warn } from "../lib/output.mjs";
9
+ import { writeState } from "../lib/state-store.mjs";
10
+
11
+ export async function installCommand(ctx, argv) {
12
+ const { options } = parseCommandOptions(argv, {
13
+ strings: ["repo", "branch"],
14
+ bools: ["dry-run", "yes", "repair"],
15
+ });
16
+ const dryRun = ctx.dryRun || options.dryRun;
17
+ const repo = options.repo || DEFAULT_REPO_URL;
18
+
19
+ const missingAgent = !fs.existsSync(ctx.agentDir);
20
+ if (missingAgent) {
21
+ fs.mkdirSync(path.dirname(ctx.agentDir), { recursive: true });
22
+ const cloneArgs = ["clone"];
23
+ if (options.branch) cloneArgs.push("--branch", options.branch);
24
+ cloneArgs.push(repo, ctx.agentDir);
25
+ runCommand("git", cloneArgs, { dryRun });
26
+ if (dryRun) {
27
+ info(`DRY-RUN would run installer from ${ctx.agentDir}`);
28
+ return;
29
+ }
30
+ } else if (!fs.existsSync(path.join(ctx.agentDir, ".git"))) {
31
+ throw new CliError(`agent dir exists but is not a git checkout: ${ctx.agentDir}`);
32
+ } else {
33
+ warn(`agent checkout already exists: ${ctx.agentDir}`);
34
+ }
35
+
36
+ if (isWindows()) {
37
+ const args = ["-AgentDir", ctx.agentDir, "-RepoRoot", ctx.repoRoot, "-SkillsDir", ctx.skillsDir];
38
+ if (dryRun) args.push("-DryRun");
39
+ if (options.repair) args.push("-ForceNpm");
40
+ runDistroScript(ctx, { sh: "pi67-update.sh", ps1: "pi67-update.ps1" }, args, { dryRun: false });
41
+ } else {
42
+ const args = ["--agent-dir", ctx.agentDir, "--skills-dir", ctx.skillsDir, "--yes"];
43
+ if (dryRun) args.push("--dry-run");
44
+ runCommand("bash", [path.join(ctx.repoRoot, "install.sh"), ...args], { cwd: ctx.repoRoot, dryRun: false });
45
+ }
46
+ if (!dryRun) writeState(ctx, "install");
47
+ info("Install finished. Run `pi-67 doctor` next.");
48
+ }
@@ -0,0 +1,74 @@
1
+ import { parseCommandOptions } from "../lib/args.mjs";
2
+ import { buildDistroManifest } from "../lib/distro-manifest.mjs";
3
+ import { validateExtensionRegistry } from "../lib/extension-registry.mjs";
4
+ import { fail, info, keyValue, pass, printJson, section, warn } from "../lib/output.mjs";
5
+
6
+ export async function manifestCommand(ctx, argv) {
7
+ const { options } = parseCommandOptions(argv, {
8
+ bools: ["json", "validate"],
9
+ });
10
+ const manifest = buildDistroManifest(ctx);
11
+ const validation = validateExtensionRegistry(manifest.extensionRegistry, { manifest });
12
+ if (ctx.json || options.json) {
13
+ printJson(options.validate ? {
14
+ schema: "pi67.manifest-validation.v1",
15
+ createdAt: manifest.createdAt,
16
+ extensionRegistry: validation,
17
+ } : manifest);
18
+ if (options.validate && !validation.ok) process.exitCode = 1;
19
+ return;
20
+ }
21
+ if (options.validate) {
22
+ printValidation(validation);
23
+ return;
24
+ }
25
+ section("pi-67 distro manifest");
26
+ keyValue("Dependencies", manifest.summary.dependencies);
27
+ keyValue("Runtime packages", `${manifest.summary.pi67ManagedRuntimePackages} pi67-managed, ${manifest.summary.userManagedRuntimePackages} user-managed`);
28
+ keyValue("Local extensions", `${manifest.summary.localExtensions - manifest.summary.missingLocalExtensions}/${manifest.summary.localExtensions} present`);
29
+ keyValue("Registered extensions", manifest.summary.registeredExtensions);
30
+ keyValue("Shared skills", `${manifest.sharedSkills.sourceDir} -> ${manifest.sharedSkills.activeDir}`);
31
+ keyValue("Theme policy", `${manifest.theme.packageName}: ${manifest.theme.policy}`);
32
+ keyValue("Preserved runtime files", manifest.runtimeFiles.preserve.join(", "));
33
+
34
+ section("Update boundary");
35
+ info(`pi-67 update command: ${manifest.commands.update}`);
36
+ info(`Repair command: ${manifest.commands.repair}`);
37
+ info(`Always-fresh repair: ${manifest.commands.alwaysFreshRepair}`);
38
+ warn(`${manifest.commands.upstreamPiExtensions} is upstream Pi only; it is not the pi-67 distribution updater.`);
39
+
40
+ section("Local extensions");
41
+ for (const item of manifest.localExtensions) {
42
+ if (item.exists && item.owner === "pi67-managed") pass(`${item.name}: ${item.path}`);
43
+ else if (item.exists) info(`${item.name}: ${item.path} (${item.owner}; ${item.policy})`);
44
+ else warn(`${item.name}: missing ${item.path}`);
45
+ }
46
+
47
+ section("Extension registry");
48
+ for (const item of manifest.extensionRegistry.extensions) {
49
+ info(`${item.id}: ${item.kind}; update=${item.updateStrategy}; repair=${item.repairStrategy}`);
50
+ }
51
+ if (validation.ok) pass("extension registry policy ready");
52
+ else warn(`extension registry policy drift: ${validation.message}`);
53
+
54
+ if (manifest.userManagedPackages.length > 0) {
55
+ section("User-managed runtime packages");
56
+ for (const item of manifest.userManagedPackages) {
57
+ warn(`${item.spec} is report-only; pi-67 will not overwrite it by default.`);
58
+ }
59
+ }
60
+ }
61
+
62
+ function printValidation(validation) {
63
+ section("pi-67 manifest validation");
64
+ keyValue("Registry entries", validation.summary.entries);
65
+ keyValue("Smoke gates", validation.summary.smokeGates);
66
+ keyValue("Config patches", validation.summary.configPatches);
67
+ if (validation.ok) {
68
+ pass(validation.message);
69
+ } else {
70
+ for (const problem of validation.problems) fail(problem);
71
+ process.exitCode = 1;
72
+ }
73
+ for (const warning of validation.warnings) warn(warning);
74
+ }