@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,141 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/bigKING67/pi-67/schemas/pi67-update-plan.schema.json",
4
+ "title": "pi-67 update plan",
5
+ "type": "object",
6
+ "required": ["schema", "createdAt", "manager", "paths", "actions", "blocked", "warnings", "recommendations"],
7
+ "properties": {
8
+ "schema": {
9
+ "const": "pi67.update-plan.v1"
10
+ },
11
+ "createdAt": {
12
+ "type": "string"
13
+ },
14
+ "manager": {
15
+ "type": "object",
16
+ "required": ["package", "version"],
17
+ "properties": {
18
+ "package": {
19
+ "type": "string"
20
+ },
21
+ "version": {
22
+ "type": "string"
23
+ },
24
+ "registry": {
25
+ "type": "object",
26
+ "description": "npm registry latest-version check; skipped when --no-remote is used."
27
+ }
28
+ },
29
+ "additionalProperties": true
30
+ },
31
+ "paths": {
32
+ "type": "object"
33
+ },
34
+ "policy": {
35
+ "type": "object",
36
+ "description": "Non-destructive update policy in force for user config, theme selection, shared skills, and external repos.",
37
+ "required": ["preservedRuntimeFiles", "themePolicy", "sharedSkillsPolicy", "externalDirtyPolicy"],
38
+ "properties": {
39
+ "userConfigPolicy": {
40
+ "type": "string"
41
+ },
42
+ "preservedRuntimeFiles": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "string"
46
+ }
47
+ },
48
+ "themePolicy": {
49
+ "type": "string"
50
+ },
51
+ "sharedSkillsPolicy": {
52
+ "type": "string"
53
+ },
54
+ "externalDirtyPolicy": {
55
+ "type": "string"
56
+ }
57
+ },
58
+ "additionalProperties": true
59
+ },
60
+ "actions": {
61
+ "type": "array",
62
+ "description": "Safe update/repair actions that preserve user-owned config and local choices.",
63
+ "items": {
64
+ "type": "object",
65
+ "required": ["id", "kind", "operation", "writes", "preserves", "risk", "reason"],
66
+ "properties": {
67
+ "id": {
68
+ "type": "string",
69
+ "minLength": 1
70
+ },
71
+ "kind": {
72
+ "type": "string",
73
+ "minLength": 1
74
+ },
75
+ "operation": {
76
+ "type": "string",
77
+ "minLength": 1
78
+ },
79
+ "writes": {
80
+ "type": "array",
81
+ "items": {
82
+ "type": "string"
83
+ }
84
+ },
85
+ "preserves": {
86
+ "type": "array",
87
+ "items": {
88
+ "type": "string"
89
+ }
90
+ },
91
+ "risk": {
92
+ "enum": ["low", "medium", "high"]
93
+ },
94
+ "reason": {
95
+ "type": "string"
96
+ },
97
+ "explicitCommand": {
98
+ "type": "string"
99
+ }
100
+ },
101
+ "additionalProperties": true
102
+ }
103
+ },
104
+ "blocked": {
105
+ "type": "array",
106
+ "description": "Actions blocked to preserve user state, dirty repos, detached repos, or strict-mode policy.",
107
+ "items": {
108
+ "type": "object",
109
+ "required": ["id", "kind", "reason"],
110
+ "properties": {
111
+ "id": {
112
+ "type": "string"
113
+ },
114
+ "kind": {
115
+ "type": "string"
116
+ },
117
+ "reason": {
118
+ "type": "string"
119
+ },
120
+ "recovery": {
121
+ "type": "string"
122
+ }
123
+ },
124
+ "additionalProperties": true
125
+ }
126
+ },
127
+ "warnings": {
128
+ "type": "array",
129
+ "items": {
130
+ "type": "string"
131
+ }
132
+ },
133
+ "recommendations": {
134
+ "type": "array",
135
+ "items": {
136
+ "type": "string"
137
+ }
138
+ }
139
+ },
140
+ "additionalProperties": true
141
+ }
@@ -0,0 +1,401 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { npmPublishTargetStatus } from "../src/lib/npm-registry.mjs";
7
+ import { readExtensionRegistry, validateExtensionRegistry } from "../src/lib/extension-registry.mjs";
8
+ import { buildPlanDecisions, classifyGitShort } from "../src/lib/update-plan.mjs";
9
+ import {
10
+ beginUpdateLifecycle,
11
+ inspectRuntimeBackup,
12
+ listRuntimeBackups,
13
+ restoreRuntimeBackup,
14
+ } from "../src/lib/update-safety.mjs";
15
+
16
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+ const files = [];
18
+ walk(path.join(root, "bin"), files);
19
+ walk(path.join(root, "src"), files);
20
+ walk(path.join(root, "schemas"), files);
21
+ for (const file of files.filter((item) => item.endsWith(".mjs"))) {
22
+ const result = spawnSync(process.execPath, ["--check", file], { stdio: "inherit" });
23
+ if (result.status !== 0) process.exit(result.status || 1);
24
+ }
25
+ for (const file of files.filter((item) => item.endsWith(".json"))) {
26
+ JSON.parse(fs.readFileSync(file, "utf8"));
27
+ }
28
+ runPublishTargetSelfTests();
29
+ runExtensionRegistrySelfTests();
30
+ runUpdatePlanSelfTests();
31
+ runUpdateSafetySelfTests();
32
+
33
+ function walk(dir, files) {
34
+ if (!fs.existsSync(dir)) return;
35
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
36
+ const full = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) walk(full, files);
38
+ else files.push(full);
39
+ }
40
+ }
41
+
42
+ function runPublishTargetSelfTests() {
43
+ const unpublished = {
44
+ skipped: false,
45
+ ok: false,
46
+ message: "not published on npm registry yet",
47
+ };
48
+ const visibleScope = {
49
+ skipped: false,
50
+ ok: true,
51
+ blocking: false,
52
+ scoped: true,
53
+ scope: "@example",
54
+ message: "visible",
55
+ };
56
+ const missingScope = {
57
+ skipped: false,
58
+ ok: false,
59
+ blocking: true,
60
+ scoped: true,
61
+ scope: "@example",
62
+ code: "scope_missing",
63
+ message: "missing",
64
+ };
65
+ assert(
66
+ npmPublishTargetStatus("@example/pkg", { registry: unpublished, scope: visibleScope }).code ===
67
+ "first_publish_requires_confirmation",
68
+ "first publish must require explicit confirmation",
69
+ );
70
+ assert(
71
+ npmPublishTargetStatus("@example/pkg", {
72
+ registry: unpublished,
73
+ scope: visibleScope,
74
+ allowFirstPublish: true,
75
+ }).ok,
76
+ "confirmed first publish with visible scope should pass the local target gate",
77
+ );
78
+ assert(
79
+ npmPublishTargetStatus("@example/pkg", {
80
+ registry: unpublished,
81
+ scope: missingScope,
82
+ }).blocking,
83
+ "missing scope must block unconfirmed first publish",
84
+ );
85
+ assert(
86
+ npmPublishTargetStatus("@example/pkg", {
87
+ registry: unpublished,
88
+ scope: missingScope,
89
+ allowFirstPublish: true,
90
+ }).code === "first_publish_scope_probe_confirmed",
91
+ "explicit first-publish confirmation should allow npm publish to be the authority for a new scope",
92
+ );
93
+ }
94
+
95
+ function runExtensionRegistrySelfTests() {
96
+ const registry = readExtensionRegistry(path.join(root, "src", "data", "extension-registry.json"));
97
+ const manifest = extensionRegistryTestManifest();
98
+ assert(
99
+ validateExtensionRegistry(registry).ok,
100
+ "current extension registry must pass standalone policy validation",
101
+ );
102
+ assert(
103
+ validateExtensionRegistry(registry, { manifest }).ok,
104
+ "current extension registry must pass policy validation",
105
+ );
106
+
107
+ const duplicate = clone(registry);
108
+ duplicate.extensions.push({ ...duplicate.extensions[0] });
109
+ assert(
110
+ validateExtensionRegistry(duplicate, { manifest }).problems.some((item) => item.includes("duplicate extension registry id")),
111
+ "duplicate extension registry ids must fail",
112
+ );
113
+
114
+ const missingSmoke = mutateExtension(registry, "xtalpi-pi-tools", (entry) => {
115
+ entry.smoke = [];
116
+ });
117
+ assert(
118
+ validateExtensionRegistry(missingSmoke, { manifest }).problems.some((item) => item.includes("smoke gate")),
119
+ "extensions without smoke gates must fail",
120
+ );
121
+
122
+ const forbiddenBehavior = mutateExtension(registry, "xtalpi-pi-tools", (entry) => {
123
+ entry.updateStrategy = "overwrite-user-config";
124
+ });
125
+ assert(
126
+ validateExtensionRegistry(forbiddenBehavior, { manifest }).problems.some((item) => item.includes("forbidden behavior")),
127
+ "forbidden update behavior must fail",
128
+ );
129
+
130
+ const unsupportedPatchMode = mutateExtension(registry, "xtalpi-pi-tools", (entry) => {
131
+ entry.configPatches[0].mode = "overwrite";
132
+ });
133
+ assert(
134
+ validateExtensionRegistry(unsupportedPatchMode, { manifest }).problems.some((item) => item.includes("unsupported config patch mode")),
135
+ "unsupported config patch modes must fail",
136
+ );
137
+
138
+ const themePolicyDriftManifest = {
139
+ ...manifest,
140
+ theme: { policy: "select-theme-during-update" },
141
+ };
142
+ assert(
143
+ validateExtensionRegistry(registry, { manifest: themePolicyDriftManifest }).problems.some((item) => item.includes("theme extension registry policy")),
144
+ "theme policy drift must fail",
145
+ );
146
+
147
+ const unsafeThemePatch = mutateExtension(registry, "pi-curated-themes", (entry) => {
148
+ entry.configPatches = [{ file: "settings.json", path: "theme", mode: "merge-preserve" }];
149
+ });
150
+ assert(
151
+ validateExtensionRegistry(unsafeThemePatch, { manifest }).problems.some((item) => item.includes("only report current theme")),
152
+ "theme update must not patch selected theme",
153
+ );
154
+
155
+ const safeThemeReportOnly = mutateExtension(registry, "pi-curated-themes", (entry) => {
156
+ entry.configPatches = [{ file: "settings.json", path: "theme", mode: "report-only" }];
157
+ });
158
+ assert(
159
+ validateExtensionRegistry(safeThemeReportOnly, { manifest }).ok,
160
+ "theme registry may report settings.json theme only when it does not patch it",
161
+ );
162
+
163
+ const sharedSkillDriftManifest = {
164
+ ...manifest,
165
+ sharedSkills: { policy: "overwrite-different-shared-skill" },
166
+ };
167
+ assert(
168
+ validateExtensionRegistry(registry, { manifest: sharedSkillDriftManifest }).problems.some((item) => item.includes("shared-skills extension registry policy")),
169
+ "shared skill policy drift must fail",
170
+ );
171
+
172
+ const externalDirtyDrift = mutateExtension(registry, "browser67", (entry) => {
173
+ entry.updateStrategy = "git-pull-even-when-dirty";
174
+ });
175
+ assert(
176
+ validateExtensionRegistry(externalDirtyDrift, { manifest }).problems.some((item) => item.includes("must block dirty updates")),
177
+ "dirty external repo update drift must fail",
178
+ );
179
+
180
+ const missingManagedManifest = {
181
+ ...manifest,
182
+ localExtensions: [...manifest.localExtensions, { name: "missing-managed-extension", owner: "pi67-managed" }],
183
+ };
184
+ assert(
185
+ validateExtensionRegistry(registry, { manifest: missingManagedManifest }).problems.some((item) => item.includes("managed local extension missing registry entry")),
186
+ "managed local extensions must be registered",
187
+ );
188
+ }
189
+
190
+ function runUpdatePlanSelfTests() {
191
+ assert(
192
+ classifyGitShort(" M settings.json\n?? tmp.txt").preservedRuntime.includes("settings.json"),
193
+ "git short classifier must identify preserved runtime config",
194
+ );
195
+ assert(
196
+ classifyGitShort("M settings.json").preservedRuntime.includes("settings.json"),
197
+ "git short classifier must tolerate status output whose first leading space was trimmed",
198
+ );
199
+ assert(
200
+ classifyGitShort(" M README.md").unsafeTracked.includes("README.md"),
201
+ "git short classifier must identify unsafe tracked edits",
202
+ );
203
+
204
+ const clean = decisionsFixture();
205
+ assert(
206
+ buildPlanDecisions(clean).blocked.length === 0,
207
+ "clean repo must not be blocked",
208
+ );
209
+
210
+ const dirtyRuntime = decisionsFixture({
211
+ git: { dirty: true, short: " M settings.json" },
212
+ });
213
+ const dirtyRuntimeDecisions = buildPlanDecisions(dirtyRuntime);
214
+ assert(
215
+ dirtyRuntimeDecisions.actions.some((item) => item.id === "user-runtime-config"),
216
+ "dirty runtime config must be planned for backup/restore instead of overwrite",
217
+ );
218
+ assert(
219
+ !dirtyRuntimeDecisions.blocked.some((item) => item.id === "repo-root"),
220
+ "dirty runtime config alone must not block the distro update plan",
221
+ );
222
+
223
+ const dirtyReadme = decisionsFixture({
224
+ git: { dirty: true, short: " M README.md" },
225
+ });
226
+ assert(
227
+ buildPlanDecisions(dirtyReadme).blocked.some((item) => item.id === "repo-root"),
228
+ "non-runtime dirty tracked files must block the distro update plan",
229
+ );
230
+
231
+ const missingExtension = decisionsFixture({
232
+ manifest: {
233
+ localExtensions: [{ name: "xtalpi-pi-tools", owner: "pi67-managed", exists: false, path: "extensions/xtalpi-pi-tools" }],
234
+ },
235
+ });
236
+ assert(
237
+ buildPlanDecisions(missingExtension).actions.some((item) => item.id === "xtalpi-pi-tools"),
238
+ "missing managed local extension must create a repair action",
239
+ );
240
+
241
+ const missingTheme = decisionsFixture({ theme: "current-theme", themeInstalled: false });
242
+ const missingThemeDecisions = buildPlanDecisions(missingTheme);
243
+ assert(
244
+ missingThemeDecisions.actions.some((item) => item.id === "pi-curated-themes" && item.preserves.includes("settings.json.theme")),
245
+ "missing theme assets must preserve selected theme while planning asset repair",
246
+ );
247
+
248
+ const sharedSkillConflict = decisionsFixture({
249
+ skills: { summary: { missing: 0, conflicts: 2 } },
250
+ });
251
+ assert(
252
+ buildPlanDecisions(sharedSkillConflict).warnings.some((item) => item.includes("preserves existing different skills")),
253
+ "shared skill conflicts must warn and preserve by default",
254
+ );
255
+ const strictSharedSkillConflict = decisionsFixture({
256
+ strictSharedSkills: true,
257
+ skills: { summary: { missing: 0, conflicts: 2 } },
258
+ });
259
+ assert(
260
+ buildPlanDecisions(strictSharedSkillConflict).blocked.some((item) => item.id === "shared-skills"),
261
+ "strict shared skill conflicts must block instead of overwriting",
262
+ );
263
+
264
+ const externalDirty = decisionsFixture({
265
+ external: [{ name: "browser67", exists: true, path: "/tmp/browser67", git: { isRepo: true, dirty: true, branch: "main" } }],
266
+ });
267
+ assert(
268
+ buildPlanDecisions(externalDirty).blocked.some((item) => item.id === "browser67"),
269
+ "dirty external repos must block destructive external updates",
270
+ );
271
+
272
+ const managerOutdated = decisionsFixture({
273
+ managerRegistry: { outdated: true },
274
+ });
275
+ assert(
276
+ buildPlanDecisions(managerOutdated).actions.some((item) => item.id === "pi67-manager"),
277
+ "outdated npm manager must produce an explicit self-update action",
278
+ );
279
+ }
280
+
281
+ function runUpdateSafetySelfTests() {
282
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi67-safety-"));
283
+ const agentDir = path.join(tmpRoot, "agent");
284
+ const stateDir = path.join(tmpRoot, "state");
285
+ fs.mkdirSync(agentDir, { recursive: true });
286
+ fs.writeFileSync(path.join(agentDir, "settings.json"), "{\"theme\":\"dark\"}\n");
287
+ fs.writeFileSync(path.join(agentDir, "auth.json"), "{\"apiKey\":\"redacted-test\"}\n");
288
+ const lifecycle = beginUpdateLifecycle({
289
+ agentDir,
290
+ repoRoot: agentDir,
291
+ stateDir,
292
+ }, {
293
+ operation: "test",
294
+ plan: { schema: "pi67.update-plan.v1", actions: [], blocked: [], warnings: [] },
295
+ });
296
+ assert(fs.existsSync(lifecycle.lockPath), "update lifecycle must acquire a lock");
297
+ assert(fs.existsSync(path.join(lifecycle.backupDir, "backup-manifest.json")), "update lifecycle must write backup manifest");
298
+ assert(
299
+ lifecycle.backedUp.some((item) => item.path === "settings.json") &&
300
+ lifecycle.backedUp.some((item) => item.path === "auth.json"),
301
+ "update lifecycle must snapshot preserved runtime files",
302
+ );
303
+ lifecycle.release();
304
+ assert(!fs.existsSync(lifecycle.lockPath), "update lifecycle must release lock");
305
+ const ctx = { agentDir, repoRoot: agentDir, stateDir };
306
+ assert(
307
+ listRuntimeBackups(ctx).some((item) => item.path === lifecycle.backupDir),
308
+ "update lifecycle backups must be listable",
309
+ );
310
+ assert(
311
+ inspectRuntimeBackup(ctx, lifecycle.backupDir).fileCount >= 2,
312
+ "update lifecycle backups must be inspectable",
313
+ );
314
+ assert(
315
+ inspectRuntimeBackup(ctx, lifecycle.backupDir).preservedCount >= 6,
316
+ "update lifecycle backups must record missing preserved runtime slots",
317
+ );
318
+ fs.writeFileSync(path.join(agentDir, "settings.json"), "{\"theme\":\"changed\"}\n");
319
+ fs.writeFileSync(path.join(agentDir, "models.json"), "{\"createdAfterBackup\":true}\n");
320
+ const restore = restoreRuntimeBackup(ctx, lifecycle.backupDir);
321
+ assert(
322
+ restore.restored.some((item) => item.path === "settings.json"),
323
+ "runtime backup restore must restore settings.json",
324
+ );
325
+ assert(
326
+ restore.removed.some((item) => item.path === "models.json") &&
327
+ !fs.existsSync(path.join(agentDir, "models.json")),
328
+ "runtime backup restore must remove preserved files that were missing at backup time",
329
+ );
330
+ assert(
331
+ fs.readFileSync(path.join(agentDir, "settings.json"), "utf8").includes("\"dark\""),
332
+ "runtime backup restore must recover the backed up file content",
333
+ );
334
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
335
+ }
336
+
337
+ function decisionsFixture(overrides = {}) {
338
+ const base = {
339
+ ctx: { skillsDir: "/tmp/pi67-skills" },
340
+ git: { isRepo: true, dirty: false, short: "" },
341
+ managerRegistry: { outdated: false },
342
+ manifest: {
343
+ runtimeFiles: {
344
+ preserve: ["settings.json", "models.json", "auth.json", "mcp.json", "image-gen.json"],
345
+ },
346
+ localExtensions: [
347
+ { name: "xtalpi-pi-tools", owner: "pi67-managed", exists: true, path: "extensions/xtalpi-pi-tools" },
348
+ ],
349
+ },
350
+ skills: { summary: { missing: 0, conflicts: 0 } },
351
+ external: [],
352
+ scriptStatus: {},
353
+ theme: "",
354
+ themeInstalled: false,
355
+ strictSharedSkills: false,
356
+ };
357
+ return deepMerge(base, overrides);
358
+ }
359
+
360
+ function extensionRegistryTestManifest() {
361
+ return {
362
+ theme: {
363
+ policy: "install-theme-package-only-never-select-theme-on-update",
364
+ },
365
+ sharedSkills: {
366
+ policy: "copy-by-default-preserve-different-existing-skills-unless-strict",
367
+ },
368
+ localExtensions: [
369
+ { name: "xtalpi-pi-tools", owner: "pi67-managed" },
370
+ { name: "pi-rules-loader", owner: "pi67-managed" },
371
+ ],
372
+ };
373
+ }
374
+
375
+ function mutateExtension(registry, id, mutate) {
376
+ const next = clone(registry);
377
+ const entry = next.extensions.find((item) => item.id === id);
378
+ assert(entry, `test registry missing ${id}`);
379
+ mutate(entry);
380
+ return next;
381
+ }
382
+
383
+ function clone(value) {
384
+ return JSON.parse(JSON.stringify(value));
385
+ }
386
+
387
+ function deepMerge(base, overrides) {
388
+ const next = clone(base);
389
+ for (const [key, value] of Object.entries(overrides)) {
390
+ if (value && typeof value === "object" && !Array.isArray(value) && next[key] && typeof next[key] === "object" && !Array.isArray(next[key])) {
391
+ next[key] = deepMerge(next[key], value);
392
+ } else {
393
+ next[key] = value;
394
+ }
395
+ }
396
+ return next;
397
+ }
398
+
399
+ function assert(condition, message) {
400
+ if (!condition) throw new Error(message);
401
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,108 @@
1
+ import { splitGlobalArgs } from "./lib/args.mjs";
2
+ import { resolveContext } from "./lib/paths.mjs";
3
+ import { CliError } from "./lib/output.mjs";
4
+ import { installCommand } from "./commands/install.mjs";
5
+ import { updateCommand } from "./commands/update.mjs";
6
+ import { doctorCommand } from "./commands/doctor.mjs";
7
+ import { smokeCommand } from "./commands/smoke.mjs";
8
+ import { statusCommand } from "./commands/status.mjs";
9
+ import { reportCommand } from "./commands/report.mjs";
10
+ import { versionCommand } from "./commands/version.mjs";
11
+ import { xtalpiCommand } from "./commands/xtalpi.mjs";
12
+ import { themesCommand } from "./commands/themes.mjs";
13
+ import { skillsCommand } from "./commands/skills.mjs";
14
+ import { extensionsCommand } from "./commands/extensions.mjs";
15
+ import { externalCommand } from "./commands/external.mjs";
16
+ import { selfUpdateCommand } from "./commands/self-update.mjs";
17
+ import { publishCheckCommand } from "./commands/publish-check.mjs";
18
+ import { manifestCommand } from "./commands/manifest.mjs";
19
+ import { backupsCommand } from "./commands/backups.mjs";
20
+
21
+ const COMMANDS = {
22
+ install: installCommand,
23
+ update: updateCommand,
24
+ doctor: doctorCommand,
25
+ smoke: smokeCommand,
26
+ status: statusCommand,
27
+ report: reportCommand,
28
+ version: versionCommand,
29
+ xtalpi: xtalpiCommand,
30
+ themes: themesCommand,
31
+ skills: skillsCommand,
32
+ extensions: extensionsCommand,
33
+ external: externalCommand,
34
+ "self-update": selfUpdateCommand,
35
+ "publish-check": publishCheckCommand,
36
+ manifest: manifestCommand,
37
+ backups: backupsCommand,
38
+ };
39
+
40
+ export async function main(argv) {
41
+ const { globals, rest } = splitGlobalArgs(argv);
42
+ if (globals.help || rest.length === 0 || rest[0] === "help") {
43
+ printHelp();
44
+ return;
45
+ }
46
+ if (rest[0] === "--version" || rest[0] === "-v") {
47
+ await versionCommand(resolveContext(globals), []);
48
+ return;
49
+ }
50
+ const command = rest[0];
51
+ const handler = COMMANDS[command];
52
+ if (!handler) {
53
+ throw new CliError(`unknown command: ${command}`, 2);
54
+ }
55
+ const ctx = resolveContext(globals);
56
+ await handler(ctx, rest.slice(1));
57
+ }
58
+
59
+ function printHelp() {
60
+ process.stdout.write(`pi-67 - pi-67 distribution manager
61
+
62
+ Usage:
63
+ pi-67 [global options] <command> [options]
64
+
65
+ Global options:
66
+ --agent-dir DIR Pi agent checkout. Default: ~/.pi/agent
67
+ --repo-root DIR pi-67 repo root. Default: same as --agent-dir
68
+ --skills-dir DIR Shared skills root. Default: ~/.agents/skills
69
+ --packages-dir DIR External package root. Default: ~/.agents/packages
70
+ --json Emit JSON when the command supports it
71
+ --dry-run Print planned writes without changing files
72
+ --no-remote Skip remote network checks where supported
73
+ --yes Confirm explicit opt-in actions where supported
74
+ -h, --help Show help
75
+
76
+ Commands:
77
+ install Clone/install pi-67 safely
78
+ update Update pi-67; use --check for read-only plan
79
+ doctor Run readiness diagnostics
80
+ smoke Run repository smoke gates
81
+ status Read-only status summary
82
+ report Generate pi67-report.json
83
+ version Print manager and distro versions
84
+ xtalpi xtalpi health/smoke/capability helpers
85
+ themes current/list/doctor/set without update-time overwrite
86
+ skills inventory/sync/migrate shared skills
87
+ extensions list/doctor/inspect/plan extension ownership policy
88
+ external list/install/update/doctor external repos
89
+ self-update Explicitly update the npm manager package
90
+ publish-check Verify npm publish readiness and trusted publishing
91
+ manifest Show managed package/config/theme ownership policy
92
+ backups list/inspect/restore preserved runtime backups
93
+
94
+ Examples:
95
+ pi-67 install
96
+ pi-67 update
97
+ pi-67 update --check
98
+ pi-67 update --repair
99
+ pi-67 self-update
100
+ pi-67 publish-check
101
+ pi-67 manifest
102
+ pi-67 manifest --validate
103
+ pi-67 backups list
104
+ pi-67 extensions doctor
105
+ pi-67 xtalpi smoke --quick
106
+ pi-67 themes current
107
+ `);
108
+ }