@pify/cli 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,1419 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+
6
+ // src/pi.ts
7
+ import { readFileSync, realpathSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { join as join2 } from "path";
10
+
11
+ // src/exec.ts
12
+ import { spawn, spawnSync } from "child_process";
13
+ import { accessSync, constants, statSync } from "fs";
14
+ import { delimiter, join } from "path";
15
+
16
+ // src/errors.ts
17
+ var ExitCode = {
18
+ OK: 0,
19
+ /** Generic / unexpected failure. */
20
+ FAILURE: 1,
21
+ /** Bad arguments, unknown command, malformed package name. */
22
+ USAGE: 2,
23
+ /** A required external program (pi, npm, node) is missing or too old. */
24
+ ENVIRONMENT: 3,
25
+ /** The requested package is not in the catalog or not yet published. */
26
+ NOT_FOUND: 4,
27
+ /** A delegated `pi` or `npm` subprocess exited non-zero. */
28
+ SUBPROCESS: 5
29
+ };
30
+ var PifyError = class extends Error {
31
+ code;
32
+ /** Optional follow-up shown after the message, e.g. a command to run. */
33
+ hint;
34
+ constructor(message, code = ExitCode.FAILURE, hint2) {
35
+ super(message);
36
+ this.name = "PifyError";
37
+ this.code = code;
38
+ this.hint = hint2;
39
+ }
40
+ };
41
+ function usageError(message, hint2) {
42
+ return new PifyError(message, ExitCode.USAGE, hint2);
43
+ }
44
+ function environmentError(message, hint2) {
45
+ return new PifyError(message, ExitCode.ENVIRONMENT, hint2);
46
+ }
47
+ function notFoundError(message, hint2) {
48
+ return new PifyError(message, ExitCode.NOT_FOUND, hint2);
49
+ }
50
+
51
+ // src/exec.ts
52
+ var isWindows = process.platform === "win32";
53
+ function isOffline() {
54
+ for (const name of ["PIFY_OFFLINE", "PI_OFFLINE"]) {
55
+ const v = process.env[name];
56
+ if (v && /^(1|true|yes)$/i.test(v)) return true;
57
+ }
58
+ return false;
59
+ }
60
+ var SAFE_ARG = /^[A-Za-z0-9@._:/\\+=-]+$/;
61
+ function assertSafeArg(arg, what = "argument") {
62
+ if (!SAFE_ARG.test(arg)) {
63
+ throw new PifyError(
64
+ `Refusing to pass unsafe ${what} to a subprocess: ${JSON.stringify(arg)}`,
65
+ ExitCode.USAGE,
66
+ "Package names may contain letters, digits and @ . _ : / \\ + = - only."
67
+ );
68
+ }
69
+ }
70
+ function spawnPlan(command, args) {
71
+ assertSafeArg(command, "command");
72
+ args.forEach((a) => assertSafeArg(a));
73
+ return isWindows ? [`${command} ${args.join(" ")}`.trim(), [], { shell: true }] : [command, args, { shell: false }];
74
+ }
75
+ function runCapture(command, args) {
76
+ const [cmd, argv, opts] = spawnPlan(command, args);
77
+ const res = spawnSync(cmd, argv, {
78
+ ...opts,
79
+ encoding: "utf8",
80
+ windowsHide: true
81
+ });
82
+ return {
83
+ status: res.status ?? (res.error ? 127 : 1),
84
+ stdout: res.stdout ?? "",
85
+ stderr: res.stderr ?? ""
86
+ };
87
+ }
88
+ function runInherit(command, args) {
89
+ const [cmd, argv, opts] = spawnPlan(command, args);
90
+ return new Promise((resolve2, reject) => {
91
+ const child = spawn(cmd, argv, {
92
+ ...opts,
93
+ stdio: "inherit",
94
+ windowsHide: true
95
+ });
96
+ child.on("error", reject);
97
+ child.on("close", (code) => resolve2(code ?? 1));
98
+ });
99
+ }
100
+ function which(command) {
101
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
102
+ if (isWindows) {
103
+ const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
104
+ for (const dir of dirs) {
105
+ const candidates = command.includes(".") ? [join(dir, command)] : exts.map((ext) => join(dir, command + ext));
106
+ for (const candidate of candidates) {
107
+ try {
108
+ if (statSync(candidate).isFile()) return candidate;
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ for (const dir of dirs) {
116
+ const candidate = join(dir, command);
117
+ try {
118
+ if (!statSync(candidate).isFile()) continue;
119
+ accessSync(candidate, constants.X_OK);
120
+ return candidate;
121
+ } catch {
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ // src/ui.ts
128
+ function detectColor(stream) {
129
+ const { NO_COLOR, FORCE_COLOR, TERM } = process.env;
130
+ if (FORCE_COLOR !== void 0 && FORCE_COLOR !== "0") return true;
131
+ if (NO_COLOR !== void 0 && NO_COLOR !== "") return false;
132
+ if (TERM === "dumb") return false;
133
+ return Boolean(stream.isTTY);
134
+ }
135
+ var stdoutColor = detectColor(process.stdout);
136
+ var stderrColor = detectColor(process.stderr);
137
+ function setColor(enabled) {
138
+ stdoutColor = enabled;
139
+ stderrColor = enabled;
140
+ }
141
+ function palette(enabled) {
142
+ const wrap = (open, close) => (s) => enabled() ? `[${open}m${s}[${close}m` : s;
143
+ return {
144
+ bold: wrap("1", "22"),
145
+ dim: wrap("2", "22"),
146
+ red: wrap("31", "39"),
147
+ green: wrap("32", "39"),
148
+ yellow: wrap("33", "39"),
149
+ cyan: wrap("36", "39")
150
+ };
151
+ }
152
+ var style = palette(() => stdoutColor);
153
+ var estyle = palette(() => stderrColor);
154
+ function out(msg = "") {
155
+ process.stdout.write(`${msg}
156
+ `);
157
+ }
158
+ function step(command) {
159
+ process.stdout.write(`${style.dim(`$ ${command}`)}
160
+ `);
161
+ }
162
+ function success(msg) {
163
+ process.stdout.write(`${style.green(msg)}
164
+ `);
165
+ }
166
+ function hint(msg) {
167
+ process.stdout.write(`${style.dim(msg)}
168
+ `);
169
+ }
170
+ function warn(msg) {
171
+ process.stderr.write(`${estyle.yellow(`Warning: ${msg}`)}
172
+ `);
173
+ }
174
+ function error(msg) {
175
+ process.stderr.write(`${estyle.red(`Error: ${msg}`)}
176
+ `);
177
+ }
178
+ function errorHint(msg) {
179
+ process.stderr.write(`${estyle.dim(msg)}
180
+ `);
181
+ }
182
+
183
+ // src/version.ts
184
+ import { createRequire } from "module";
185
+ var require2 = createRequire(import.meta.url);
186
+ var VERSION = require2("../package.json").version;
187
+
188
+ // src/pi.ts
189
+ var PI_PACKAGE = "@earendil-works/pi-coding-agent";
190
+ var NODE_FLOOR = "22.19.0";
191
+ var TESTED_PI_VERSION = "0.84.4";
192
+ var LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
193
+ function piStatus() {
194
+ const binPath = which("pi");
195
+ if (!binPath) return { installed: false, binPath: null, version: null };
196
+ const res = runCapture("pi", ["--version"]);
197
+ const version = res.status === 0 ? res.stdout.trim() || null : null;
198
+ return { installed: true, binPath, version };
199
+ }
200
+ async function fetchLatestPiVersion() {
201
+ if (isOffline()) return null;
202
+ try {
203
+ const controller = new AbortController();
204
+ const timer = setTimeout(() => controller.abort(), 5e3);
205
+ const res = await fetch(LATEST_VERSION_URL, {
206
+ signal: controller.signal,
207
+ headers: { "user-agent": `pify/${VERSION}` }
208
+ });
209
+ clearTimeout(timer);
210
+ if (!res.ok) return null;
211
+ const data = await res.json();
212
+ return typeof data.version === "string" ? data.version : null;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ function compareSemver(a, b) {
218
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10));
219
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10));
220
+ for (let i = 0; i < 3; i++) {
221
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
222
+ if (d !== 0) return d < 0 ? -1 : 1;
223
+ }
224
+ return 0;
225
+ }
226
+ function assertNodeVersion() {
227
+ const current = process.versions.node;
228
+ if (compareSemver(current, NODE_FLOOR) < 0) {
229
+ throw environmentError(
230
+ `The pi coding agent requires Node >= ${NODE_FLOOR}; you are running ${current}.`,
231
+ "Upgrade Node (e.g. via nvm / nvm-windows) and try again."
232
+ );
233
+ }
234
+ }
235
+ function npmPrefixEvidence(binPath) {
236
+ const res = runCapture("npm", ["prefix", "-g"]);
237
+ const prefix = res.status === 0 ? res.stdout.trim() : "";
238
+ if (!prefix) return { managed: false, prefix: null };
239
+ const resolve2 = (p) => {
240
+ try {
241
+ return realpathSync(p);
242
+ } catch {
243
+ return p;
244
+ }
245
+ };
246
+ const normalize = (p) => isWindows ? p.toLowerCase() : p;
247
+ const managed = normalize(resolve2(binPath)).startsWith(normalize(resolve2(prefix)));
248
+ return { managed, prefix };
249
+ }
250
+ function guardNpmManaged(binPath) {
251
+ if (process.env.PI_MANAGED_INSTALL_ROOT) {
252
+ throw environmentError("pi is a managed install; pify will not overwrite it.");
253
+ }
254
+ if (!npmPrefixEvidence(binPath).managed) {
255
+ throw environmentError(
256
+ `pi at ${binPath} is not managed by npm; refusing to install over it.`,
257
+ "Update it with the package manager that installed it (pnpm/yarn/bun), or remove it first."
258
+ );
259
+ }
260
+ }
261
+ async function installPi(version) {
262
+ assertNodeVersion();
263
+ if (!which("npm")) {
264
+ throw environmentError(
265
+ "npm was not found on PATH.",
266
+ "Install Node.js (which bundles npm) from https://nodejs.org and retry."
267
+ );
268
+ }
269
+ if (process.env.PI_MANAGED_INSTALL_ROOT && !piStatus().installed) {
270
+ throw environmentError(
271
+ "PI_MANAGED_INSTALL_ROOT is set but pi is not installed.",
272
+ "Use the managed installer: curl -fsSL https://pi.dev/install.sh | sh (Linux/macOS)."
273
+ );
274
+ }
275
+ const spec = version ? `${PI_PACKAGE}@${version}` : PI_PACKAGE;
276
+ step(`npm install -g --ignore-scripts ${spec}`);
277
+ const code = await runInherit("npm", ["install", "-g", "--ignore-scripts", spec]);
278
+ if (code !== 0) {
279
+ throw new PifyError(
280
+ `npm install -g ${spec} exited with code ${code}.`,
281
+ ExitCode.SUBPROCESS,
282
+ isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." : "If this is an EACCES error, configure a user-writable npm prefix (https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally) or use a Node version manager."
283
+ );
284
+ }
285
+ const st = piStatus();
286
+ if (!st.installed) {
287
+ throw environmentError(
288
+ "pi was installed but is not resolvable on PATH.",
289
+ "Add npm's global prefix to PATH (npm prefix -g) and open a new terminal."
290
+ );
291
+ }
292
+ success(`pi ${st.version ?? ""} installed.`.replace(" ", " "));
293
+ }
294
+ function requirePi() {
295
+ if (!piStatus().installed) {
296
+ throw environmentError(
297
+ "The pi coding agent is not installed.",
298
+ "Run pify setup to install it."
299
+ );
300
+ }
301
+ }
302
+ function delegate(args) {
303
+ return runInherit("pi", args);
304
+ }
305
+ function agentDir() {
306
+ const override = process.env.PI_CODING_AGENT_DIR;
307
+ if (override) {
308
+ return override.startsWith("~") ? join2(homedir(), override.slice(1).replace(/^[/\\]/, "")) : override;
309
+ }
310
+ return join2(homedir(), ".pi", "agent");
311
+ }
312
+ function settingsPath(scope) {
313
+ return scope === "user" ? join2(agentDir(), "settings.json") : join2(process.cwd(), ".pi", "settings.json");
314
+ }
315
+ function readSettings(scope) {
316
+ try {
317
+ return JSON.parse(readFileSync(settingsPath(scope), "utf8"));
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+ var PIFY_SOURCE = /^npm:@pify\/([A-Za-z0-9._-]+?)(?:@(.+))?$/;
323
+ function installedPifyPackages() {
324
+ const result = /* @__PURE__ */ new Map();
325
+ for (const scope of ["user", "project"]) {
326
+ const settings = readSettings(scope);
327
+ for (const entry of settings?.packages ?? []) {
328
+ const source = typeof entry === "string" ? entry : entry.source;
329
+ if (typeof source !== "string") continue;
330
+ const match = PIFY_SOURCE.exec(source);
331
+ const name = match?.[1];
332
+ if (!name) continue;
333
+ result.set(name, { name, scope, source, pin: match?.[2] ?? null });
334
+ }
335
+ }
336
+ return result;
337
+ }
338
+ function installedVersionOnDisk(name, scope) {
339
+ const root = scope === "user" ? join2(agentDir(), "npm") : join2(process.cwd(), ".pi", "npm");
340
+ try {
341
+ const pkg = JSON.parse(
342
+ readFileSync(join2(root, "node_modules", "@pify", name, "package.json"), "utf8")
343
+ );
344
+ return { present: true, version: typeof pkg.version === "string" ? pkg.version : null };
345
+ } catch {
346
+ return { present: false, version: null };
347
+ }
348
+ }
349
+
350
+ // src/commands/setup.ts
351
+ async function setup(opts) {
352
+ assertNodeVersion();
353
+ if (!which("npm")) {
354
+ throw environmentError(
355
+ "npm was not found on PATH.",
356
+ "Install Node.js (which bundles npm) from https://nodejs.org and retry."
357
+ );
358
+ }
359
+ const st = piStatus();
360
+ if (st.installed && !opts.force && !opts.piVersion) {
361
+ success(`pi ${st.version ?? "(unknown version)"} is already installed at ${st.binPath}`);
362
+ const latest = await fetchLatestPiVersion();
363
+ if (latest && st.version && compareSemver(st.version, latest) < 0) {
364
+ out(style.yellow(`Update available: ${st.version} -> ${latest} - run pify update pi`));
365
+ }
366
+ hint("Next: pify list to see available packages.");
367
+ return 0;
368
+ }
369
+ if (st.installed && opts.piVersion && st.version === opts.piVersion) {
370
+ success(`pi ${opts.piVersion} is already installed.`);
371
+ return 0;
372
+ }
373
+ if (st.installed && st.binPath) {
374
+ guardNpmManaged(st.binPath);
375
+ }
376
+ await installPi(opts.piVersion);
377
+ hint("Next: pify list to see available packages.");
378
+ return 0;
379
+ }
380
+
381
+ // src/catalog.ts
382
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
383
+ import { homedir as homedir2 } from "os";
384
+ import { dirname, join as join3 } from "path";
385
+ import { fileURLToPath } from "url";
386
+ function bundledCatalogPath() {
387
+ return join3(dirname(fileURLToPath(import.meta.url)), "..", "catalog.json");
388
+ }
389
+ function cachePath() {
390
+ return join3(homedir2(), ".pify", "catalog.json");
391
+ }
392
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
393
+ function loadBundledCatalog() {
394
+ return JSON.parse(readFileSync2(bundledCatalogPath(), "utf8"));
395
+ }
396
+ function validateCatalog(data) {
397
+ if (typeof data !== "object" || data === null) return false;
398
+ const c = data;
399
+ if (!Number.isInteger(c.version)) return false;
400
+ if (!Array.isArray(c.packages)) return false;
401
+ for (const entry of c.packages) {
402
+ if (typeof entry !== "object" || entry === null) return false;
403
+ const p = entry;
404
+ if (typeof p.name !== "string" || !/^[a-z0-9-]+$/.test(p.name)) return false;
405
+ if (typeof p.npm !== "string" || !p.npm.startsWith("@pify/")) return false;
406
+ if (p.status !== "published" && p.status !== "planned") return false;
407
+ }
408
+ return true;
409
+ }
410
+ async function loadCatalog(opts = {}) {
411
+ const bundled = loadBundledCatalog();
412
+ const cached = readCache();
413
+ let remote = cached?.catalog ?? null;
414
+ const stale = !cached || Date.now() - cached.fetchedAt > CACHE_TTL_MS;
415
+ if (opts.refresh || stale) {
416
+ const fetched = await fetchRemoteCatalog(bundled.remote, opts.refresh ? 1e4 : 3e3);
417
+ if (fetched) {
418
+ remote = fetched;
419
+ writeCache(fetched);
420
+ }
421
+ }
422
+ if (!remote || !validateCatalog(remote)) return bundled;
423
+ return remote.version >= bundled.version ? remote : bundled;
424
+ }
425
+ async function refreshCatalog() {
426
+ const bundled = loadBundledCatalog();
427
+ const fetched = await fetchRemoteCatalog(bundled.remote, 1e4);
428
+ if (fetched) {
429
+ writeCache(fetched);
430
+ return { ok: true, catalog: fetched };
431
+ }
432
+ const cached = readCache();
433
+ const fallback = cached && validateCatalog(cached.catalog) && cached.catalog.version >= bundled.version ? cached.catalog : bundled;
434
+ return { ok: false, catalog: fallback };
435
+ }
436
+ async function fetchRemoteCatalog(defaultUrl, timeoutMs) {
437
+ if (isOffline()) return null;
438
+ const url = process.env.PIFY_CATALOG_URL || defaultUrl;
439
+ try {
440
+ const controller = new AbortController();
441
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
442
+ const res = await fetch(url, { signal: controller.signal });
443
+ clearTimeout(timer);
444
+ if (!res.ok) return null;
445
+ const data = await res.json();
446
+ return validateCatalog(data) ? data : null;
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+ function readCache() {
452
+ try {
453
+ const parsed = JSON.parse(readFileSync2(cachePath(), "utf8"));
454
+ if (typeof parsed.fetchedAt !== "number" || !validateCatalog(parsed.catalog)) return null;
455
+ return parsed;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ function writeCache(catalog) {
461
+ try {
462
+ mkdirSync(dirname(cachePath()), { recursive: true });
463
+ writeFileSync(cachePath(), JSON.stringify({ fetchedAt: Date.now(), catalog }, null, 2));
464
+ } catch {
465
+ }
466
+ }
467
+ var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
468
+ var DIST_TAG = /^[a-z][a-z0-9._-]*$/i;
469
+ function parseInstallSpec(input) {
470
+ const raw = input.startsWith("npm:") ? input.slice(4) : input;
471
+ const at = raw.indexOf("@", raw.startsWith("@") ? 1 : 0);
472
+ const base = at > 0 ? raw.slice(0, at) : raw;
473
+ const pin = at > 0 ? raw.slice(at + 1) : null;
474
+ if (pin !== null && !EXACT_VERSION.test(pin) && !(DIST_TAG.test(pin) && !/^\d/.test(pin))) {
475
+ throw usageError(
476
+ "Version pins must be exact versions or dist-tags.",
477
+ `Got ${JSON.stringify(pin)} - ranges (^, ~, >, <, *, x) are not supported.`
478
+ );
479
+ }
480
+ return { base, pin };
481
+ }
482
+ function resolveInstallTarget(catalog, input) {
483
+ const { base, pin } = parseInstallSpec(input);
484
+ if (base.startsWith("@")) {
485
+ if (!base.startsWith("@pify/")) {
486
+ throw usageError(
487
+ "pify installs @pify packages only.",
488
+ `For other sources use: pi install ${input}`
489
+ );
490
+ }
491
+ const shortName = base.slice("@pify/".length).toLowerCase();
492
+ if (!/^[a-z0-9._-]+$/.test(shortName)) {
493
+ throw usageError(`Invalid package name: ${JSON.stringify(base)}`);
494
+ }
495
+ const entry2 = catalog.packages.find((p) => p.name === shortName) ?? null;
496
+ return {
497
+ shortName,
498
+ npm: `@pify/${shortName}`,
499
+ pin,
500
+ explicit: true,
501
+ inCatalog: entry2 !== null,
502
+ status: entry2?.status ?? null,
503
+ repo: entry2?.repo ?? null
504
+ };
505
+ }
506
+ if (/[:/\\]/.test(base)) {
507
+ throw usageError(
508
+ "pify installs @pify packages only.",
509
+ `For other sources use: pi install ${input}`
510
+ );
511
+ }
512
+ const needle = base.toLowerCase();
513
+ const entry = catalog.packages.find((p) => p.name === needle || `pify-${p.name}` === needle) ?? null;
514
+ if (!entry) {
515
+ const names = catalog.packages.map((p) => p.name).join(", ");
516
+ throw notFoundError(
517
+ `Unknown pify package: ${JSON.stringify(input)}`,
518
+ `Available: ${names}`
519
+ );
520
+ }
521
+ if (entry.status !== "published") {
522
+ throw notFoundError(
523
+ `@pify/${entry.name} is not published yet.`,
524
+ `Track it at ${entry.repo}. Run "pify update --catalog" to refresh availability.`
525
+ );
526
+ }
527
+ return {
528
+ shortName: entry.name,
529
+ npm: entry.npm,
530
+ pin,
531
+ explicit: false,
532
+ inCatalog: true,
533
+ status: entry.status,
534
+ repo: entry.repo
535
+ };
536
+ }
537
+ function resolvePifyName(input) {
538
+ const { base } = parseInstallSpec(input);
539
+ if (base.startsWith("@")) {
540
+ if (!base.startsWith("@pify/")) {
541
+ throw usageError(
542
+ "pify manages @pify packages only.",
543
+ `For other sources use the pi CLI directly.`
544
+ );
545
+ }
546
+ const shortName = base.slice("@pify/".length).toLowerCase();
547
+ if (!/^[a-z0-9._-]+$/.test(shortName)) {
548
+ throw usageError(`Invalid package name: ${JSON.stringify(base)}`);
549
+ }
550
+ return shortName;
551
+ }
552
+ if (/[:/\\]/.test(base)) {
553
+ throw usageError(
554
+ "pify manages @pify packages only.",
555
+ `For other sources use the pi CLI directly.`
556
+ );
557
+ }
558
+ const name = base.toLowerCase();
559
+ return name.startsWith("pify-") ? name.slice("pify-".length) : name;
560
+ }
561
+
562
+ // src/commands/install.ts
563
+ function piArgs(target, opts) {
564
+ const spec = target.pin ? `npm:${target.npm}@${target.pin}` : `npm:${target.npm}`;
565
+ const args = ["install", spec];
566
+ if (opts.local) args.push("-l");
567
+ if (opts.approve) args.push("-a");
568
+ return args;
569
+ }
570
+ async function install(names, opts) {
571
+ if (names.length === 0) {
572
+ throw usageError("Missing package name.", "Usage: pify install <name...> [-l] [-a]");
573
+ }
574
+ const catalog = await loadCatalog();
575
+ const targets = names.map((name) => resolveInstallTarget(catalog, name));
576
+ for (const target of targets) {
577
+ if (target.explicit && (!target.inCatalog || target.status !== "published")) {
578
+ warn(`${target.npm} is not in the catalog (or not marked published); installing anyway.`);
579
+ }
580
+ }
581
+ requirePi();
582
+ if (opts.dryRun) {
583
+ for (const target of targets) out(`pi ${piArgs(target, opts).join(" ")}`);
584
+ return 0;
585
+ }
586
+ const failed = [];
587
+ for (const target of targets) {
588
+ const args = piArgs(target, opts);
589
+ step(`pi ${args.join(" ")}`);
590
+ const code = await delegate(args);
591
+ if (code !== 0) failed.push(target.shortName);
592
+ }
593
+ if (failed.length > 0) {
594
+ throw new PifyError(
595
+ `Installed ${targets.length - failed.length} of ${targets.length}; failed: ${failed.join(", ")}`,
596
+ ExitCode.SUBPROCESS
597
+ );
598
+ }
599
+ return 0;
600
+ }
601
+
602
+ // src/commands/remove.ts
603
+ var PIFY_SOURCE2 = /^npm:@pify\/([A-Za-z0-9._-]+?)(?:@.+)?$/;
604
+ function configuredNames(scope) {
605
+ const names = /* @__PURE__ */ new Set();
606
+ for (const entry of readSettings(scope)?.packages ?? []) {
607
+ const source = typeof entry === "string" ? entry : entry.source;
608
+ if (typeof source !== "string") continue;
609
+ const name = PIFY_SOURCE2.exec(source)?.[1];
610
+ if (name) names.add(name);
611
+ }
612
+ return names;
613
+ }
614
+ async function remove(names, opts) {
615
+ if (names.length === 0) {
616
+ throw usageError("Missing package name.", "Usage: pify remove <name...> [-l] [-a]");
617
+ }
618
+ const targets = names.map(resolvePifyName);
619
+ const scope = opts.local ? "project" : "user";
620
+ const installed = configuredNames(scope);
621
+ for (const name of targets) {
622
+ if (!installed.has(name)) {
623
+ const scopeNote = opts.local ? " in project scope" : "";
624
+ throw notFoundError(
625
+ `${name} is not installed${scopeNote}.`,
626
+ installed.size > 0 ? `Installed @pify packages: ${[...installed].sort().join(", ")}` : "Nothing from @pify is installed."
627
+ );
628
+ }
629
+ }
630
+ requirePi();
631
+ const argsFor = (name) => {
632
+ const args = ["remove", `npm:@pify/${name}`];
633
+ if (opts.local) args.push("-l");
634
+ if (opts.approve) args.push("-a");
635
+ return args;
636
+ };
637
+ if (opts.dryRun) {
638
+ for (const name of targets) out(`pi ${argsFor(name).join(" ")}`);
639
+ return 0;
640
+ }
641
+ const failed = [];
642
+ for (const name of targets) {
643
+ const args = argsFor(name);
644
+ step(`pi ${args.join(" ")}`);
645
+ const code = await delegate(args);
646
+ if (code !== 0) failed.push(name);
647
+ }
648
+ if (failed.length > 0) {
649
+ throw new PifyError(
650
+ `Removed ${targets.length - failed.length} of ${targets.length}; failed: ${failed.join(", ")}`,
651
+ ExitCode.SUBPROCESS
652
+ );
653
+ }
654
+ return 0;
655
+ }
656
+
657
+ // src/commands/update.ts
658
+ async function update(targets, opts) {
659
+ if (isOffline()) {
660
+ warn("PIFY_OFFLINE/PI_OFFLINE is set - updates skipped.");
661
+ return 0;
662
+ }
663
+ if (opts.catalogOnly) {
664
+ if (targets.length > 0) {
665
+ throw usageError("--catalog cannot be combined with update targets.");
666
+ }
667
+ const { ok, catalog } = await refreshCatalog();
668
+ if (ok) {
669
+ out(`Catalog refreshed (version ${catalog.version}, ${catalog.packages.length} packages).`);
670
+ } else {
671
+ warn("Could not refresh the catalog; using the existing copy.");
672
+ }
673
+ return 0;
674
+ }
675
+ const updateEverything = targets.length === 0;
676
+ const piOnly = targets.length === 1 && (targets[0] === "pi" || targets[0] === "self");
677
+ const installed = installedPifyPackages();
678
+ let packageNames = [];
679
+ if (updateEverything) {
680
+ packageNames = [...installed.keys()].sort();
681
+ } else if (!piOnly) {
682
+ packageNames = targets.map(resolvePifyName);
683
+ for (const name of packageNames) {
684
+ if (!installed.has(name)) {
685
+ throw notFoundError(`${name} is not installed.`, `pify install ${name}`);
686
+ }
687
+ }
688
+ }
689
+ if (opts.dryRun) {
690
+ if (updateEverything || piOnly) out("pi update");
691
+ for (const name of packageNames) out(`pi update npm:@pify/${name}`);
692
+ if (updateEverything) out("refresh catalog");
693
+ return 0;
694
+ }
695
+ const failed = [];
696
+ if (updateEverything || piOnly) {
697
+ if (!piStatus().installed) {
698
+ await installPi();
699
+ } else {
700
+ step("pi update");
701
+ const code = await delegate(["update"]);
702
+ if (code !== 0) failed.push("pi");
703
+ }
704
+ }
705
+ if (packageNames.length > 0 && !piStatus().installed) {
706
+ throw new PifyError(
707
+ "pi is required to update packages.",
708
+ ExitCode.ENVIRONMENT,
709
+ "Run pify setup first."
710
+ );
711
+ }
712
+ for (const name of packageNames) {
713
+ const args = ["update", `npm:@pify/${name}`];
714
+ step(`pi ${args.join(" ")}`);
715
+ const code = await delegate(args);
716
+ if (code !== 0) failed.push(name);
717
+ }
718
+ if (updateEverything && packageNames.length === 0) {
719
+ hint("No @pify packages installed; nothing more to update.");
720
+ }
721
+ if (updateEverything) {
722
+ await loadCatalog({ refresh: true });
723
+ }
724
+ if (failed.length > 0) {
725
+ throw new PifyError(`Update failed for: ${failed.join(", ")}`, ExitCode.SUBPROCESS);
726
+ }
727
+ success("Everything is up to date.");
728
+ return 0;
729
+ }
730
+
731
+ // src/commands/list.ts
732
+ async function list(opts) {
733
+ const catalog = await loadCatalog();
734
+ const installed = installedPifyPackages();
735
+ const rows = catalog.packages.map((pkg) => {
736
+ const entry = installed.get(pkg.name);
737
+ if (!entry) {
738
+ return { name: pkg.name, npm: pkg.npm, description: pkg.description, repo: pkg.repo, status: pkg.status, installed: null };
739
+ }
740
+ const disk = installedVersionOnDisk(pkg.name, entry.scope);
741
+ return {
742
+ name: pkg.name,
743
+ npm: pkg.npm,
744
+ description: pkg.description,
745
+ repo: pkg.repo,
746
+ status: pkg.status,
747
+ installed: {
748
+ scope: entry.scope,
749
+ version: disk.version,
750
+ pinned: entry.pin !== null,
751
+ missingOnDisk: !disk.present
752
+ }
753
+ };
754
+ });
755
+ for (const [name, entry] of installed) {
756
+ if (rows.some((r) => r.name === name)) continue;
757
+ const disk = installedVersionOnDisk(name, entry.scope);
758
+ rows.push({
759
+ name,
760
+ npm: `@pify/${name}`,
761
+ description: "(not in catalog)",
762
+ repo: null,
763
+ status: null,
764
+ installed: {
765
+ scope: entry.scope,
766
+ version: disk.version,
767
+ pinned: entry.pin !== null,
768
+ missingOnDisk: !disk.present
769
+ }
770
+ });
771
+ }
772
+ if (opts.json) {
773
+ out(JSON.stringify(rows, null, 2));
774
+ return 0;
775
+ }
776
+ out(style.bold(`@pify packages (${catalog.org})`));
777
+ out();
778
+ const nameWidth = rows.reduce((m, r) => Math.max(m, r.name.length), 0) + 2;
779
+ const stateOf = (r) => {
780
+ if (r.installed) {
781
+ if (r.installed.missingOnDisk) {
782
+ return { text: `missing on disk (${r.installed.scope})`, paint: style.red };
783
+ }
784
+ const ver = r.installed.version ?? "?";
785
+ const pin = r.installed.pinned ? " pinned" : "";
786
+ return { text: `installed ${ver} (${r.installed.scope})${pin}`, paint: style.green };
787
+ }
788
+ if (r.status === "published") return { text: "available", paint: (s) => s };
789
+ return { text: "planned", paint: style.dim };
790
+ };
791
+ const stateWidth = rows.reduce((m, r) => Math.max(m, stateOf(r).text.length), 0) + 2;
792
+ for (const r of rows) {
793
+ const { text, paint } = stateOf(r);
794
+ out(` ${r.name.padEnd(nameWidth)}${paint(text.padEnd(stateWidth))}${style.dim(r.description)}`);
795
+ }
796
+ out();
797
+ hint("Install with: pify install <name>");
798
+ return 0;
799
+ }
800
+
801
+ // src/commands/doctor.ts
802
+ import { existsSync } from "fs";
803
+ async function doctor(opts) {
804
+ const checks = [];
805
+ const push = (id, status, detail) => checks.push({ id, status, detail });
806
+ const nodeOk = compareSemver(process.versions.node, NODE_FLOOR) >= 0;
807
+ push(
808
+ "node",
809
+ nodeOk ? "ok" : "fail",
810
+ nodeOk ? `v${process.versions.node} (>= ${NODE_FLOOR} required)` : `v${process.versions.node} - pi requires >= ${NODE_FLOOR}; upgrade Node`
811
+ );
812
+ const npmPath = which("npm");
813
+ if (npmPath) {
814
+ const ver = runCapture("npm", ["--version"]);
815
+ push("npm", "ok", `${ver.status === 0 ? `v${ver.stdout.trim()} at ` : ""}${npmPath}`);
816
+ } else {
817
+ push("npm", "fail", "not found on PATH - install Node.js from https://nodejs.org");
818
+ }
819
+ const gitPath = which("git");
820
+ push(
821
+ "git",
822
+ gitPath ? "ok" : "warn",
823
+ gitPath ?? "git not found - pi's git: package sources will not work"
824
+ );
825
+ const pi = piStatus();
826
+ if (!pi.installed) {
827
+ push("pi", "fail", "not installed - run: pify setup");
828
+ } else if (!pi.version) {
829
+ push("pi", "fail", `pi found at ${pi.binPath} but "pi --version" failed (broken install)`);
830
+ } else {
831
+ push("pi", "ok", `pi ${pi.version} at ${pi.binPath}`);
832
+ }
833
+ if (isOffline()) {
834
+ push("pi-latest", "info", "skipped (offline)");
835
+ } else {
836
+ const latest = await fetchLatestPiVersion();
837
+ if (!latest) {
838
+ push("pi-latest", "warn", "could not reach pi.dev to check the latest version");
839
+ } else if (pi.version && compareSemver(pi.version, latest) < 0) {
840
+ push("pi-latest", "warn", `pi ${pi.version} installed, ${latest} available - run pify update pi`);
841
+ } else {
842
+ push("pi-latest", "ok", `pi ${latest} is current`);
843
+ }
844
+ }
845
+ if (process.env.PI_MANAGED_INSTALL_ROOT) {
846
+ push("pi-install-method", "info", "managed install (PI_MANAGED_INSTALL_ROOT)");
847
+ } else if (pi.binPath) {
848
+ const { managed, prefix } = npmPrefixEvidence(pi.binPath);
849
+ push(
850
+ "pi-install-method",
851
+ "info",
852
+ managed ? `npm global (${prefix})` : "unrecognized (self-update is delegated to pi either way)"
853
+ );
854
+ } else {
855
+ push("pi-install-method", "info", "n/a (pi not installed)");
856
+ }
857
+ for (const scope of ["user", "project"]) {
858
+ const id = scope === "user" ? "settings-user" : "settings-project";
859
+ const path = settingsPath(scope);
860
+ if (!existsSync(path)) {
861
+ push(id, "info", scope === "user" ? "not found (run pi once to create it)" : "none");
862
+ continue;
863
+ }
864
+ const settings = readSettings(scope);
865
+ if (settings === null) {
866
+ push(id, "warn", `${path} is unparseable JSON`);
867
+ } else {
868
+ push(id, "ok", `${path} (${settings.packages?.length ?? 0} packages configured)`);
869
+ }
870
+ }
871
+ const installed = installedPifyPackages();
872
+ if (installed.size === 0) {
873
+ push("pify-packages", "info", "no @pify packages configured yet");
874
+ } else {
875
+ const missing = [...installed.values()].filter(
876
+ (p) => !installedVersionOnDisk(p.name, p.scope).present
877
+ );
878
+ if (missing.length > 0) {
879
+ push(
880
+ "pify-packages",
881
+ "fail",
882
+ `missing on disk: ${missing.map((p) => p.name).join(", ")} - run pify install <name> to repair`
883
+ );
884
+ } else {
885
+ push("pify-packages", "ok", `${installed.size} configured, all present on disk`);
886
+ }
887
+ }
888
+ const envVars = ["PI_OFFLINE", "PIFY_OFFLINE", "PI_CODING_AGENT_DIR", "NO_COLOR", "PIFY_CATALOG_URL"].filter((name) => process.env[name] !== void 0).map((name) => `${name}=${process.env[name]}`);
889
+ push("env", "info", envVars.length > 0 ? envVars.join(", ") : "none set");
890
+ const problems = checks.filter((c) => c.status === "fail").length;
891
+ if (opts.json) {
892
+ out(JSON.stringify({ pify: VERSION, agentDir: agentDir(), checks, problems }, null, 2));
893
+ return problems > 0 ? 1 : 0;
894
+ }
895
+ out(style.bold("pify doctor"));
896
+ out();
897
+ const paint = {
898
+ ok: style.green,
899
+ warn: style.yellow,
900
+ fail: style.red,
901
+ info: style.dim
902
+ };
903
+ const idWidth = checks.reduce((m, c) => Math.max(m, c.id.length), 0) + 2;
904
+ for (const c of checks) {
905
+ out(` ${paint[c.status](c.status.padEnd(6))}${c.id.padEnd(idWidth)}${style.dim(c.detail)}`);
906
+ }
907
+ out();
908
+ if (problems > 0) {
909
+ out(style.red(`${problems} problem${problems > 1 ? "s" : ""} found.`));
910
+ return 1;
911
+ }
912
+ out(style.green("Everything looks good."));
913
+ return 0;
914
+ }
915
+
916
+ // src/commands/init.ts
917
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
918
+ import { basename, join as join4, resolve } from "path";
919
+
920
+ // src/templates.ts
921
+ function interpolate(template, vars) {
922
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
923
+ const value = vars[key];
924
+ if (value === void 0) throw new Error(`Unknown template variable: ${key}`);
925
+ return value;
926
+ });
927
+ }
928
+ function packageJsonTemplate(scoped) {
929
+ const publishConfig = scoped ? `
930
+ "publishConfig": { "access": "public" },` : "";
931
+ return `{
932
+ "name": "{{name}}",
933
+ "version": "0.1.0",
934
+ "description": "{{description}}",
935
+ "type": "module",
936
+ "keywords": ["pi-package", "pi-extension", "pi", "pify"],
937
+ "license": "MIT",${publishConfig}
938
+ "files": ["index.ts", "src"],
939
+ "pi": {
940
+ "extensions": ["./index.ts"]
941
+ },
942
+ "engines": { "node": ">=22.19.0" },
943
+ "scripts": { "typecheck": "tsc --noEmit" },
944
+ "peerDependencies": {
945
+ "@earendil-works/pi-ai": "*",
946
+ "@earendil-works/pi-agent-core": "*",
947
+ "@earendil-works/pi-coding-agent": "*",
948
+ "@earendil-works/pi-tui": "*",
949
+ "typebox": "*"
950
+ },
951
+ "peerDependenciesMeta": {
952
+ "@earendil-works/pi-ai": { "optional": true },
953
+ "@earendil-works/pi-agent-core": { "optional": true },
954
+ "@earendil-works/pi-coding-agent": { "optional": true },
955
+ "@earendil-works/pi-tui": { "optional": true },
956
+ "typebox": { "optional": true }
957
+ },
958
+ "devDependencies": {
959
+ "@earendil-works/pi-ai": "*",
960
+ "@earendil-works/pi-coding-agent": "*",
961
+ "typescript": "^5.7.2",
962
+ "@types/node": "^22.10.2"
963
+ }
964
+ }
965
+ `;
966
+ }
967
+ var INDEX_TS = `import { Type } from "@earendil-works/pi-ai";
968
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
969
+
970
+ // Rules this template already follows:
971
+ // - Signal tool errors by THROWING from execute(); returned values never set isError.
972
+ // - Truncate large output with truncateHead/truncateTail from
973
+ // @earendil-works/pi-coding-agent (built-in limits: 50 KB / 2000 lines).
974
+ // - Use StringEnum from @earendil-works/pi-ai for enum parameters;
975
+ // Type.Union/Type.Literal breaks on Google's API.
976
+ // - Use the CONFIG_DIR_NAME export instead of hardcoding ".pi" for
977
+ // project-local config paths.
978
+ const exampleTool = defineTool({
979
+ name: "{{snakeName}}_hello",
980
+ label: "{{shortName}} hello",
981
+ description: "Example tool for {{name}}. Replace with your own.",
982
+ parameters: Type.Object({
983
+ name: Type.String({ description: "Name to greet" }),
984
+ }),
985
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
986
+ return {
987
+ content: [{ type: "text", text: \`Hello, \${params.name}!\` }],
988
+ details: { greeted: params.name },
989
+ };
990
+ },
991
+ });
992
+
993
+ export default function (pi: ExtensionAPI) {
994
+ pi.registerTool(exampleTool);
995
+
996
+ pi.registerCommand("{{shortName}}-hello", {
997
+ description: "Example command for {{name}}. Replace with your own.",
998
+ handler: async (args, _ctx) => {
999
+ // \`args\` is the single raw string typed after the command name.
1000
+ // There is no argv parsing - split it yourself if you need arguments.
1001
+ },
1002
+ });
1003
+
1004
+ // Never start watchers, sockets, timers, or child processes here in the
1005
+ // factory: it can run in invocations that never start a session.
1006
+ pi.on("session_start", async (_event, _ctx) => {
1007
+ // Start background resources here.
1008
+ });
1009
+
1010
+ pi.on("session_shutdown", async (_event, _ctx) => {
1011
+ // Tear them down here. This must be idempotent: it fires on quit,
1012
+ // /reload, /new, /resume, and /fork.
1013
+ });
1014
+ }
1015
+ `;
1016
+ var TSCONFIG_JSON = `{
1017
+ "compilerOptions": {
1018
+ "target": "ES2023",
1019
+ "module": "NodeNext",
1020
+ "moduleResolution": "NodeNext",
1021
+ "strict": true,
1022
+ "noEmit": true,
1023
+ "skipLibCheck": true,
1024
+ "allowImportingTsExtensions": true,
1025
+ "types": ["node"]
1026
+ },
1027
+ "include": ["index.ts", "src/**/*.ts"]
1028
+ }
1029
+ `;
1030
+ var GITIGNORE = `node_modules/
1031
+ `;
1032
+ var README_MD = `# {{name}}
1033
+
1034
+ {{description}}
1035
+
1036
+ A Pi Package for the pi coding agent (https://pi.dev).
1037
+
1038
+ ## Develop
1039
+
1040
+ npm install # dev dependencies for typechecking
1041
+ pi install ./{{dir}} # register locally; hot-reloads with /reload
1042
+ pi -e ./index.ts # or load ad hoc for a single run
1043
+ npm run typecheck
1044
+
1045
+ ## Publish
1046
+
1047
+ npm publish --access public
1048
+
1049
+ Then install anywhere with: pi install npm:{{name}}
1050
+
1051
+ Tested with pi {{piVersion}}.
1052
+ `;
1053
+
1054
+ // src/commands/init.ts
1055
+ var NPM_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
1056
+ async function init(dirArg, opts) {
1057
+ const dir = dirArg ?? ".";
1058
+ const target = resolve(dir);
1059
+ if (existsSync2(target) && readdirSync(target).length > 0) {
1060
+ throw new PifyError(
1061
+ `Target directory ${JSON.stringify(dir)} is not empty.`,
1062
+ ExitCode.FAILURE,
1063
+ "Choose a new directory: pify init my-extension"
1064
+ );
1065
+ }
1066
+ const name = (opts.name ?? basename(target)).toLowerCase();
1067
+ if (!NPM_NAME.test(name)) {
1068
+ throw usageError(
1069
+ `Invalid npm package name: ${JSON.stringify(name)}`,
1070
+ "Pass a valid name explicitly: pify init <dir> --name my-pi-package"
1071
+ );
1072
+ }
1073
+ const shortName = name.includes("/") ? name.split("/")[1] : name;
1074
+ const vars = {
1075
+ name,
1076
+ description: opts.description ?? "A Pi Package for the pi coding agent.",
1077
+ shortName,
1078
+ snakeName: shortName.replace(/-/g, "_").replace(/[^a-z0-9_]/g, "_"),
1079
+ piVersion: TESTED_PI_VERSION,
1080
+ dir: basename(target)
1081
+ };
1082
+ mkdirSync2(target, { recursive: true });
1083
+ const files = [
1084
+ ["package.json", interpolate(packageJsonTemplate(name.startsWith("@")), vars)],
1085
+ ["index.ts", interpolate(INDEX_TS, vars)],
1086
+ ["tsconfig.json", TSCONFIG_JSON],
1087
+ [".gitignore", GITIGNORE],
1088
+ ["README.md", interpolate(README_MD, vars)]
1089
+ ];
1090
+ for (const [file, content] of files) {
1091
+ writeFileSync2(join4(target, file), content);
1092
+ }
1093
+ success(`Created ${files.length} files in ${basename(target)}/`);
1094
+ for (const [file] of files) out(` ${file}`);
1095
+ out();
1096
+ hint("Next steps:");
1097
+ hint(` cd ${basename(target)} && npm install`);
1098
+ hint(` pi install ./${basename(target)} # register locally; hot-reloads with /reload`);
1099
+ hint(` pi -e ./${basename(target)}/index.ts # or load ad hoc for a single run`);
1100
+ return 0;
1101
+ }
1102
+
1103
+ // src/help.ts
1104
+ function render(text) {
1105
+ return text.split("\n").map((line) => {
1106
+ if (/^(Usage|Commands|Short forms|Options|Examples|Environment Variables):$/.test(line)) {
1107
+ return style.bold(line);
1108
+ }
1109
+ if (/^\s+#/.test(line)) return style.dim(line);
1110
+ return line;
1111
+ }).join("\n");
1112
+ }
1113
+ function mainHelp() {
1114
+ return render(`${style.bold("pify")} - front door for the Pify suite for the pi coding agent
1115
+
1116
+ Usage:
1117
+ pify <command> [options]
1118
+
1119
+ Commands:
1120
+ setup Install the pi coding agent (safe to re-run)
1121
+ install <name...> Install @pify packages by short name
1122
+ remove <name...> Remove installed @pify packages
1123
+ update [pi|<name...>] Update pi and installed @pify packages
1124
+ list Show the @pify catalog with install state
1125
+ doctor Diagnose the local pi/pify environment
1126
+ init [dir] Scaffold a new Pi Package
1127
+
1128
+ Short forms:
1129
+ i = install, rm = remove, up = update, ls = list
1130
+
1131
+ Options:
1132
+ -h, --help Show help (pify <command> --help for details)
1133
+ -v, --version Print the pify version
1134
+ --no-color Disable colored output
1135
+
1136
+ Examples:
1137
+ # Bootstrap a bare machine (installs pi via npm)
1138
+ npx @pify/cli setup
1139
+
1140
+ # Install packages by short name
1141
+ pify install goal task
1142
+
1143
+ # Keep pi and every installed @pify package current
1144
+ pify update
1145
+
1146
+ # Scaffold a new extension package
1147
+ pify init my-extension
1148
+
1149
+ Environment Variables:
1150
+ PI_CODING_AGENT_DIR Override pi's agent directory (default: ~/.pi/agent)
1151
+ PIFY_CATALOG_URL Override the remote catalog URL
1152
+ PIFY_OFFLINE, PI_OFFLINE
1153
+ Set to 1/true/yes to skip catalog and version lookups
1154
+ NO_COLOR Disable colored output
1155
+
1156
+ Package operations are delegated to the pi CLI; pify never edits pi's
1157
+ settings. Anything beyond these commands is pi's job: run pi --help.
1158
+ `);
1159
+ }
1160
+ var COMMAND_HELP = {
1161
+ setup: `Usage:
1162
+ pify setup [--force] [--pi-version <version>]
1163
+
1164
+ Install the pi coding agent globally via npm. Safe to re-run; reports status
1165
+ when pi is already installed. Updates of an existing pi are pi's own job:
1166
+ run pify update pi.
1167
+
1168
+ Options:
1169
+ --force Reinstall pi even if it is already installed
1170
+ --pi-version <version> Install an exact pi version (may downgrade)
1171
+
1172
+ Examples:
1173
+ # Bootstrap a bare machine
1174
+ npx @pify/cli setup
1175
+
1176
+ # Pin the version your team has tested
1177
+ pify setup --pi-version 0.84.4
1178
+ `,
1179
+ install: `Usage:
1180
+ pify install <name...> [-l] [-a] [--dry-run]
1181
+
1182
+ Install @pify packages by short name. Names resolve against the Pify catalog
1183
+ (goal -> npm:@pify/goal); the install itself is delegated to pi install.
1184
+ Exact version pins are accepted (goal@0.3.0); range specifiers are refused.
1185
+
1186
+ Options:
1187
+ -l, --local Project scope (.pi/settings.json, .pi/npm)
1188
+ -a, --approve Forward --approve to pi (scripted -l installs)
1189
+ --dry-run Print the pi commands without running them
1190
+
1191
+ Examples:
1192
+ # Install two packages by short name
1193
+ pify install goal task
1194
+
1195
+ # Project-scoped install in CI (pre-approves project trust)
1196
+ pify install goal -l -a
1197
+ `,
1198
+ remove: `Usage:
1199
+ pify remove <name...> [-l] [-a] [--dry-run]
1200
+
1201
+ Remove installed @pify packages by short name. Delegates to pi remove.
1202
+
1203
+ Options:
1204
+ -l, --local Project scope
1205
+ -a, --approve Forward --approve to pi
1206
+ --dry-run Print the pi commands without running them
1207
+
1208
+ Examples:
1209
+ pify remove goal
1210
+ `,
1211
+ update: `Usage:
1212
+ pify update [pi | <name...>] [--catalog] [--dry-run]
1213
+
1214
+ With no target: update pi itself, then every installed @pify package, then
1215
+ refresh the catalog cache. "pify update pi" updates only the agent; named
1216
+ targets update only those packages. Exact version pins are never advanced;
1217
+ re-pin with pify install <name>@<version>.
1218
+
1219
+ Options:
1220
+ --catalog Refresh the remote catalog cache only
1221
+ --dry-run Print the update plan without executing it
1222
+
1223
+ Examples:
1224
+ # Everything current in one pass
1225
+ pify update
1226
+
1227
+ # Agent only
1228
+ pify update pi
1229
+ `,
1230
+ list: `Usage:
1231
+ pify list [--json]
1232
+
1233
+ Show the @pify catalog with local install state. pi list shows what is
1234
+ installed; pify list also shows what exists and what is coming.
1235
+
1236
+ Options:
1237
+ --json Machine-readable output
1238
+
1239
+ Examples:
1240
+ pify list
1241
+ `,
1242
+ doctor: `Usage:
1243
+ pify doctor [--json]
1244
+
1245
+ Diagnose the local pi/pify environment: Node and npm, the pi install,
1246
+ settings files, and installed @pify packages. Exit 0 means healthy; 1 means
1247
+ problems were found.
1248
+
1249
+ Options:
1250
+ --json Machine-readable output
1251
+
1252
+ Examples:
1253
+ pify doctor
1254
+ `,
1255
+ init: `Usage:
1256
+ pify init [dir] [--name <package-name>] [--description <text>]
1257
+
1258
+ Scaffold a new Pi Package: a raw-TypeScript extension entry (no build step),
1259
+ correct peerDependencies layout, pi manifest, and files allowlist. The
1260
+ target directory must be empty or not exist.
1261
+
1262
+ Options:
1263
+ --name <name> npm package name (default: the directory name)
1264
+ --description <text> Package description
1265
+
1266
+ Examples:
1267
+ # New extension package in ./my-extension
1268
+ pify init my-extension
1269
+
1270
+ # Scoped name
1271
+ pify init tools --name @myorg/pi-tools
1272
+ `
1273
+ };
1274
+ function commandHelp(command) {
1275
+ const text = COMMAND_HELP[command];
1276
+ return text ? render(text) : mainHelp();
1277
+ }
1278
+
1279
+ // src/cli.ts
1280
+ var COMMANDS = [
1281
+ {
1282
+ name: "setup",
1283
+ aliases: [],
1284
+ options: {
1285
+ force: { type: "boolean", default: false },
1286
+ "pi-version": { type: "string" }
1287
+ },
1288
+ allowPositionals: false,
1289
+ run: (_p, v) => setup({ force: Boolean(v.force), piVersion: v["pi-version"] })
1290
+ },
1291
+ {
1292
+ name: "install",
1293
+ aliases: ["i", "add"],
1294
+ options: {
1295
+ local: { type: "boolean", short: "l", default: false },
1296
+ approve: { type: "boolean", short: "a", default: false },
1297
+ "dry-run": { type: "boolean", default: false }
1298
+ },
1299
+ allowPositionals: true,
1300
+ run: (p, v) => install(p, {
1301
+ local: Boolean(v.local),
1302
+ approve: Boolean(v.approve),
1303
+ dryRun: Boolean(v["dry-run"])
1304
+ })
1305
+ },
1306
+ {
1307
+ name: "remove",
1308
+ aliases: ["rm", "uninstall"],
1309
+ options: {
1310
+ local: { type: "boolean", short: "l", default: false },
1311
+ approve: { type: "boolean", short: "a", default: false },
1312
+ "dry-run": { type: "boolean", default: false }
1313
+ },
1314
+ allowPositionals: true,
1315
+ run: (p, v) => remove(p, {
1316
+ local: Boolean(v.local),
1317
+ approve: Boolean(v.approve),
1318
+ dryRun: Boolean(v["dry-run"])
1319
+ })
1320
+ },
1321
+ {
1322
+ name: "update",
1323
+ aliases: ["up"],
1324
+ options: {
1325
+ catalog: { type: "boolean", default: false },
1326
+ "dry-run": { type: "boolean", default: false }
1327
+ },
1328
+ allowPositionals: true,
1329
+ run: (p, v) => update(p, { catalogOnly: Boolean(v.catalog), dryRun: Boolean(v["dry-run"]) })
1330
+ },
1331
+ {
1332
+ name: "list",
1333
+ aliases: ["ls"],
1334
+ options: { json: { type: "boolean", default: false } },
1335
+ allowPositionals: false,
1336
+ run: (_p, v) => list({ json: Boolean(v.json) })
1337
+ },
1338
+ {
1339
+ name: "doctor",
1340
+ aliases: [],
1341
+ options: { json: { type: "boolean", default: false } },
1342
+ allowPositionals: false,
1343
+ run: (_p, v) => doctor({ json: Boolean(v.json) })
1344
+ },
1345
+ {
1346
+ name: "init",
1347
+ aliases: [],
1348
+ options: {
1349
+ name: { type: "string" },
1350
+ description: { type: "string" }
1351
+ },
1352
+ allowPositionals: true,
1353
+ run: (p, v) => init(p[0], {
1354
+ name: v.name,
1355
+ description: v.description
1356
+ })
1357
+ }
1358
+ ];
1359
+ async function main(argv) {
1360
+ const args = argv.filter((a) => a !== "--no-color");
1361
+ if (args.length !== argv.length) setColor(false);
1362
+ if (args.length === 0) {
1363
+ out(mainHelp());
1364
+ return ExitCode.USAGE;
1365
+ }
1366
+ const first = args[0];
1367
+ if (first === "-h" || first === "--help") {
1368
+ out(mainHelp());
1369
+ return 0;
1370
+ }
1371
+ if (first === "-v" || first === "--version") {
1372
+ out(VERSION);
1373
+ return 0;
1374
+ }
1375
+ if (first.startsWith("-")) {
1376
+ throw usageError(`Unknown option ${first}.`, 'Use "pify --help".');
1377
+ }
1378
+ const command = COMMANDS.find((c) => c.name === first || c.aliases.includes(first));
1379
+ if (!command) {
1380
+ throw usageError(
1381
+ `Unknown command: ${JSON.stringify(first)}`,
1382
+ 'Run "pify --help" to see available commands.'
1383
+ );
1384
+ }
1385
+ const rest = args.slice(1);
1386
+ if (rest.includes("-h") || rest.includes("--help")) {
1387
+ out(commandHelp(command.name));
1388
+ return 0;
1389
+ }
1390
+ let parsed;
1391
+ try {
1392
+ parsed = parseArgs({
1393
+ args: rest,
1394
+ options: command.options,
1395
+ allowPositionals: command.allowPositionals,
1396
+ strict: true
1397
+ });
1398
+ } catch (err) {
1399
+ const message = err instanceof Error ? err.message : String(err);
1400
+ const unknown = /Unknown option '(--?[^']+)'/.exec(message)?.[1];
1401
+ throw usageError(
1402
+ unknown ? `Unknown option ${unknown} for "${command.name}".` : message,
1403
+ `Use "pify --help" or "pify ${command.name} --help".`
1404
+ );
1405
+ }
1406
+ return command.run(parsed.positionals, parsed.values);
1407
+ }
1408
+ main(process.argv.slice(2)).then((code) => {
1409
+ process.exitCode = code;
1410
+ }).catch((err) => {
1411
+ if (err instanceof PifyError) {
1412
+ error(err.message);
1413
+ if (err.hint) errorHint(err.hint);
1414
+ process.exitCode = err.code;
1415
+ } else {
1416
+ error(err instanceof Error ? err.stack ?? err.message : String(err));
1417
+ process.exitCode = ExitCode.FAILURE;
1418
+ }
1419
+ });