@pify/cli 0.1.2 → 0.2.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 CHANGED
@@ -4,9 +4,9 @@
4
4
  import { parseArgs } from "util";
5
5
 
6
6
  // src/pi.ts
7
- import { readFileSync, realpathSync } from "fs";
7
+ import { accessSync as accessSync2, constants as constants2, readFileSync, realpathSync, statSync as statSync2 } from "fs";
8
8
  import { homedir } from "os";
9
- import { join as join2 } from "path";
9
+ import { dirname, join as join2 } from "path";
10
10
 
11
11
  // src/exec.ts
12
12
  import { spawn, spawnSync } from "child_process";
@@ -97,6 +97,17 @@ function runInherit(command, args) {
97
97
  child.on("close", (code) => resolve2(code ?? 1));
98
98
  });
99
99
  }
100
+ function runOfficialInstaller(kind) {
101
+ const [cmd, argv] = kind === "windows" ? [
102
+ "powershell",
103
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "irm https://pi.dev/install.ps1 | iex"]
104
+ ] : ["sh", ["-c", "curl -fsSL https://pi.dev/install.sh | sh"]];
105
+ return new Promise((resolve2, reject) => {
106
+ const child = spawn(cmd, argv, { stdio: "inherit", windowsHide: false });
107
+ child.on("error", reject);
108
+ child.on("close", (code) => resolve2(code ?? 1));
109
+ });
110
+ }
100
111
  function which(command) {
101
112
  const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
102
113
  if (isWindows) {
@@ -258,38 +269,107 @@ function guardNpmManaged(binPath) {
258
269
  );
259
270
  }
260
271
  }
272
+ function officialInstallerCommand() {
273
+ return isWindows ? 'powershell -c "irm https://pi.dev/install.ps1 | iex"' : "curl -fsSL https://pi.dev/install.sh | sh";
274
+ }
275
+ function writableOrCreatable(path) {
276
+ let current = path;
277
+ for (; ; ) {
278
+ try {
279
+ statSync2(current);
280
+ break;
281
+ } catch {
282
+ const parent = dirname(current);
283
+ if (parent === current) return false;
284
+ current = parent;
285
+ }
286
+ }
287
+ try {
288
+ if (!statSync2(current).isDirectory()) return false;
289
+ accessSync2(current, constants2.W_OK);
290
+ return true;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+ function npmInstallPiArgs(spec, npmMajor, prefix) {
296
+ const args = ["install", "-g", "--ignore-scripts"];
297
+ if (npmMajor >= 11) args.push("--min-release-age=0");
298
+ args.push("--no-fund", "--no-audit");
299
+ if (prefix) args.push("--prefix", prefix);
300
+ args.push(spec);
301
+ return args;
302
+ }
303
+ function npmMajorVersion() {
304
+ const res = runCapture("npm", ["--version"]);
305
+ const major = Number.parseInt(res.stdout.trim().split(".")[0] ?? "", 10);
306
+ return Number.isNaN(major) ? 0 : major;
307
+ }
261
308
  async function installPi(version) {
262
309
  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
310
  if (process.env.PI_MANAGED_INSTALL_ROOT && !piStatus().installed) {
270
311
  throw environmentError(
271
312
  "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)."
313
+ `Use the official installer: ${officialInstallerCommand()}`
273
314
  );
274
315
  }
275
316
  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]);
317
+ const npmPath = which("npm");
318
+ const bunPath = which("bun");
319
+ if (!npmPath && !bunPath) {
320
+ throw environmentError(
321
+ "Neither npm nor bun was found on PATH.",
322
+ `Run the official installer (it can install Node too): ${officialInstallerCommand()}`
323
+ );
324
+ }
325
+ let command;
326
+ let args;
327
+ let localPrefix = null;
328
+ if (npmPath) {
329
+ command = "npm";
330
+ if (!isWindows) {
331
+ const prefixRes = runCapture("npm", ["prefix", "-g"]);
332
+ const prefix = prefixRes.status === 0 ? prefixRes.stdout.trim() : "";
333
+ const prefixWritable = prefix !== "" && writableOrCreatable(join2(prefix, "lib", "node_modules")) && writableOrCreatable(join2(prefix, "bin"));
334
+ if (prefix && !prefixWritable) {
335
+ let staleGlobalPi = false;
336
+ try {
337
+ statSync2(join2(prefix, "bin", "pi"));
338
+ staleGlobalPi = true;
339
+ } catch {
340
+ }
341
+ if (staleGlobalPi) {
342
+ throw environmentError(
343
+ `npm's global directory is not writable (${prefix}) and pi is already installed there.`,
344
+ `Update or remove it first: sudo npm install -g --ignore-scripts ${PI_PACKAGE}`
345
+ );
346
+ }
347
+ localPrefix = join2(homedir(), ".local");
348
+ }
349
+ }
350
+ args = npmInstallPiArgs(spec, npmMajorVersion(), localPrefix ?? void 0);
351
+ } else {
352
+ command = "bun";
353
+ args = ["add", "-g", "--ignore-scripts", spec];
354
+ }
355
+ step(`${command} ${args.join(" ")}`);
356
+ const code = await runInherit(command, args);
278
357
  if (code !== 0) {
279
358
  throw new PifyError(
280
- `npm install -g ${spec} exited with code ${code}.`,
359
+ `${command} ${args[0]} exited with code ${code}.`,
281
360
  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."
361
+ isWindows ? "If this is a permissions or file-lock error, close running pi sessions and retry from an elevated terminal." : `If the problem persists, try the official installer: ${officialInstallerCommand()}`
283
362
  );
284
363
  }
285
364
  const st = piStatus();
286
365
  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
- );
366
+ const pathHint = localPrefix ? `Add ${join2(localPrefix, "bin")} to PATH and open a new terminal.` : "Add the global bin directory to PATH (npm prefix -g) and open a new terminal.";
367
+ throw environmentError("pi was installed but is not resolvable on PATH.", pathHint);
291
368
  }
292
369
  success(`pi ${st.version ?? ""} installed.`.replace(" ", " "));
370
+ if (localPrefix) {
371
+ hint(`Installed under ${localPrefix} because npm's global prefix is not writable.`);
372
+ }
293
373
  }
294
374
  function requirePi() {
295
375
  if (!piStatus().installed) {
@@ -349,12 +429,19 @@ function installedVersionOnDisk(name, scope) {
349
429
 
350
430
  // src/commands/setup.ts
351
431
  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
- );
432
+ if (opts.installer) {
433
+ if (!process.stdin.isTTY || process.env.CI) {
434
+ throw environmentError(
435
+ "The official installer is interactive and needs a terminal.",
436
+ "In CI, use plain `pify setup` (non-interactive npm path) instead."
437
+ );
438
+ }
439
+ step(officialInstallerCommand());
440
+ const code = await runOfficialInstaller(isWindows ? "windows" : "posix");
441
+ if (code !== 0) {
442
+ throw new PifyError(`The official installer exited with code ${code}.`, ExitCode.SUBPROCESS);
443
+ }
444
+ return 0;
358
445
  }
359
446
  const st = piStatus();
360
447
  if (st.installed && !opts.force && !opts.piVersion) {
@@ -381,10 +468,10 @@ async function setup(opts) {
381
468
  // src/catalog.ts
382
469
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
383
470
  import { homedir as homedir2 } from "os";
384
- import { dirname, join as join3 } from "path";
471
+ import { dirname as dirname2, join as join3 } from "path";
385
472
  import { fileURLToPath } from "url";
386
473
  function bundledCatalogPath() {
387
- return join3(dirname(fileURLToPath(import.meta.url)), "..", "catalog.json");
474
+ return join3(dirname2(fileURLToPath(import.meta.url)), "..", "catalog.json");
388
475
  }
389
476
  function cachePath() {
390
477
  return join3(homedir2(), ".pify", "catalog.json");
@@ -459,7 +546,7 @@ function readCache() {
459
546
  }
460
547
  function writeCache(catalog) {
461
548
  try {
462
- mkdirSync(dirname(cachePath()), { recursive: true });
549
+ mkdirSync(dirname2(cachePath()), { recursive: true });
463
550
  writeFileSync(cachePath(), JSON.stringify({ fetchedAt: Date.now(), catalog }, null, 2));
464
551
  } catch {
465
552
  }
@@ -675,12 +762,12 @@ async function update(targets, opts) {
675
762
  const updateEverything = targets.length === 0;
676
763
  const piOnly = targets.length === 1 && (targets[0] === "pi" || targets[0] === "self");
677
764
  const installed = installedPifyPackages();
678
- let packageNames = [];
765
+ let packageNames2 = [];
679
766
  if (updateEverything) {
680
- packageNames = [...installed.keys()].sort();
767
+ packageNames2 = [...installed.keys()].sort();
681
768
  } else if (!piOnly) {
682
- packageNames = targets.map(resolvePifyName);
683
- for (const name of packageNames) {
769
+ packageNames2 = targets.map(resolvePifyName);
770
+ for (const name of packageNames2) {
684
771
  if (!installed.has(name)) {
685
772
  throw notFoundError(`${name} is not installed.`, `pify install ${name}`);
686
773
  }
@@ -688,7 +775,7 @@ async function update(targets, opts) {
688
775
  }
689
776
  if (opts.dryRun) {
690
777
  if (updateEverything || piOnly) out("pi update");
691
- for (const name of packageNames) out(`pi update npm:@pify/${name}`);
778
+ for (const name of packageNames2) out(`pi update npm:@pify/${name}`);
692
779
  if (updateEverything) out("refresh catalog");
693
780
  return 0;
694
781
  }
@@ -702,20 +789,20 @@ async function update(targets, opts) {
702
789
  if (code !== 0) failed.push("pi");
703
790
  }
704
791
  }
705
- if (packageNames.length > 0 && !piStatus().installed) {
792
+ if (packageNames2.length > 0 && !piStatus().installed) {
706
793
  throw new PifyError(
707
794
  "pi is required to update packages.",
708
795
  ExitCode.ENVIRONMENT,
709
796
  "Run pify setup first."
710
797
  );
711
798
  }
712
- for (const name of packageNames) {
799
+ for (const name of packageNames2) {
713
800
  const args = ["update", `npm:@pify/${name}`];
714
801
  step(`pi ${args.join(" ")}`);
715
802
  const code = await delegate(args);
716
803
  if (code !== 0) failed.push(name);
717
804
  }
718
- if (updateEverything && packageNames.length === 0) {
805
+ if (updateEverything && packageNames2.length === 0) {
719
806
  hint("No @pify packages installed; nothing more to update.");
720
807
  }
721
808
  if (updateEverything) {
@@ -822,6 +909,14 @@ async function doctor(opts) {
822
909
  gitPath ? "ok" : "warn",
823
910
  gitPath ?? "git not found - pi's git: package sources will not work"
824
911
  );
912
+ if (isWindows) {
913
+ const bashPath = which("bash");
914
+ push(
915
+ "bash",
916
+ bashPath ? "ok" : "warn",
917
+ bashPath ?? "bash not found - pi needs Git Bash on Windows; run pify setup --installer"
918
+ );
919
+ }
825
920
  const pi = piStatus();
826
921
  if (!pi.installed) {
827
922
  push("pi", "fail", "not installed - run: pify setup");
@@ -1100,6 +1195,244 @@ async function init(dirArg, opts) {
1100
1195
  return 0;
1101
1196
  }
1102
1197
 
1198
+ // src/registry.ts
1199
+ var GLOBAL_FLAGS = [
1200
+ { name: "help", short: "h", description: "Show help" },
1201
+ { name: "version", short: "v", description: "Print the pify version" },
1202
+ { name: "no-color", description: "Disable colored output" }
1203
+ ];
1204
+ var COMMAND_SPECS = [
1205
+ {
1206
+ name: "setup",
1207
+ aliases: [],
1208
+ description: "Install the pi coding agent (safe to re-run)",
1209
+ positional: "none",
1210
+ flags: [
1211
+ { name: "force", description: "Reinstall pi even if present" },
1212
+ { name: "pi-version", takesValue: true, description: "Install an exact pi version" },
1213
+ { name: "installer", description: "Run pi's official interactive installer" }
1214
+ ]
1215
+ },
1216
+ {
1217
+ name: "install",
1218
+ aliases: ["i", "add"],
1219
+ description: "Install @pify packages by short name",
1220
+ positional: "packages",
1221
+ flags: [
1222
+ { name: "local", short: "l", description: "Project scope" },
1223
+ { name: "approve", short: "a", description: "Forward --approve to pi" },
1224
+ { name: "dry-run", description: "Print the pi commands without running them" }
1225
+ ]
1226
+ },
1227
+ {
1228
+ name: "remove",
1229
+ aliases: ["rm", "uninstall"],
1230
+ description: "Remove installed @pify packages",
1231
+ positional: "packages",
1232
+ flags: [
1233
+ { name: "local", short: "l", description: "Project scope" },
1234
+ { name: "approve", short: "a", description: "Forward --approve to pi" },
1235
+ { name: "dry-run", description: "Print the pi commands without running them" }
1236
+ ]
1237
+ },
1238
+ {
1239
+ name: "update",
1240
+ aliases: ["up"],
1241
+ description: "Update pi and installed @pify packages",
1242
+ positional: "updateTargets",
1243
+ flags: [
1244
+ { name: "catalog", description: "Refresh the catalog cache only" },
1245
+ { name: "dry-run", description: "Print the update plan without executing" }
1246
+ ]
1247
+ },
1248
+ {
1249
+ name: "list",
1250
+ aliases: ["ls"],
1251
+ description: "Show the @pify catalog with install state",
1252
+ positional: "none",
1253
+ flags: [{ name: "json", description: "Machine-readable output" }]
1254
+ },
1255
+ {
1256
+ name: "doctor",
1257
+ aliases: [],
1258
+ description: "Diagnose the local pi/pify environment",
1259
+ positional: "none",
1260
+ flags: [{ name: "json", description: "Machine-readable output" }]
1261
+ },
1262
+ {
1263
+ name: "init",
1264
+ aliases: [],
1265
+ description: "Scaffold a new Pi Package",
1266
+ positional: "directory",
1267
+ flags: [
1268
+ { name: "name", takesValue: true, description: "npm package name" },
1269
+ { name: "description", takesValue: true, description: "Package description" }
1270
+ ]
1271
+ },
1272
+ {
1273
+ name: "completions",
1274
+ aliases: [],
1275
+ description: "Print a shell completion script",
1276
+ positional: "shell",
1277
+ flags: []
1278
+ }
1279
+ ];
1280
+ var COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell"];
1281
+ function parseArgsOptionsFor(spec) {
1282
+ const options = {};
1283
+ for (const flag of spec.flags) {
1284
+ options[flag.name] = flag.takesValue ? { type: "string", ...flag.short ? { short: flag.short } : {} } : { type: "boolean", default: false, ...flag.short ? { short: flag.short } : {} };
1285
+ }
1286
+ return options;
1287
+ }
1288
+
1289
+ // src/completions.ts
1290
+ function packageNames() {
1291
+ return loadBundledCatalog().packages.map((p) => p.name);
1292
+ }
1293
+ function allNames(spec) {
1294
+ return [spec.name, ...spec.aliases];
1295
+ }
1296
+ function flagTokens(spec) {
1297
+ return spec.flags.flatMap((f) => [`--${f.name}`, ...f.short ? [`-${f.short}`] : []]);
1298
+ }
1299
+ function positionalWords(spec) {
1300
+ switch (spec.positional) {
1301
+ case "packages":
1302
+ return packageNames();
1303
+ case "updateTargets":
1304
+ return ["pi", ...packageNames()];
1305
+ case "shell":
1306
+ return [...COMPLETION_SHELLS];
1307
+ default:
1308
+ return [];
1309
+ }
1310
+ }
1311
+ function bashScript() {
1312
+ const commands = COMMAND_SPECS.flatMap(allNames).join(" ");
1313
+ const cases = COMMAND_SPECS.map((spec) => {
1314
+ const words = [...positionalWords(spec), ...flagTokens(spec)].join(" ");
1315
+ return ` ${allNames(spec).join("|")}) words="${words}" ;;`;
1316
+ }).join("\n");
1317
+ return `# bash completion for pify \u2014 generated by \`pify completions bash\`
1318
+ # Install: eval "$(pify completions bash)" (or save to a bash-completion dir)
1319
+ _pify_completions() {
1320
+ local cur words
1321
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1322
+ if [ "$COMP_CWORD" -eq 1 ]; then
1323
+ COMPREPLY=($(compgen -W "${commands} ${GLOBAL_FLAGS.map((f) => `--${f.name}`).join(" ")}" -- "$cur"))
1324
+ return
1325
+ fi
1326
+ case "\${COMP_WORDS[1]}" in
1327
+ ${cases}
1328
+ *) words="" ;;
1329
+ esac
1330
+ COMPREPLY=($(compgen -W "$words" -- "$cur"))
1331
+ }
1332
+ complete -F _pify_completions pify
1333
+ `;
1334
+ }
1335
+ function zshScript() {
1336
+ const commandDescs = COMMAND_SPECS.flatMap(
1337
+ (spec) => allNames(spec).map((n) => ` '${n}:${spec.description.replace(/'/g, "")}'`)
1338
+ ).join("\n");
1339
+ const cases = COMMAND_SPECS.map((spec) => {
1340
+ const words = [...positionalWords(spec), ...flagTokens(spec)].map((w) => `'${w}'`).join(" ");
1341
+ return ` ${allNames(spec).join("|")}) _values 'value' ${words || "''"} ;;`;
1342
+ }).join("\n");
1343
+ return `#compdef pify
1344
+ # zsh completion for pify \u2014 generated by \`pify completions zsh\`
1345
+ # Install: eval "$(pify completions zsh)" (or save into a directory on $fpath)
1346
+ _pify() {
1347
+ local -a commands
1348
+ commands=(
1349
+ ${commandDescs}
1350
+ )
1351
+ if (( CURRENT == 2 )); then
1352
+ _describe 'command' commands
1353
+ return
1354
+ fi
1355
+ case "$words[2]" in
1356
+ ${cases}
1357
+ esac
1358
+ }
1359
+ _pify "$@"
1360
+ `;
1361
+ }
1362
+ function fishScript() {
1363
+ const lines = [
1364
+ `# fish completion for pify \u2014 generated by \`pify completions fish\``,
1365
+ `# Install: pify completions fish > ~/.config/fish/completions/pify.fish`,
1366
+ `complete -c pify -f`
1367
+ ];
1368
+ for (const spec of COMMAND_SPECS) {
1369
+ for (const name of allNames(spec)) {
1370
+ lines.push(
1371
+ `complete -c pify -n __fish_use_subcommand -a ${name} -d '${spec.description.replace(/'/g, "")}'`
1372
+ );
1373
+ }
1374
+ const condition = `__fish_seen_subcommand_from ${allNames(spec).join(" ")}`;
1375
+ const words = positionalWords(spec);
1376
+ if (words.length > 0) {
1377
+ lines.push(`complete -c pify -n '${condition}' -a '${words.join(" ")}'`);
1378
+ }
1379
+ for (const flag of spec.flags) {
1380
+ const short = flag.short ? ` -s ${flag.short}` : "";
1381
+ lines.push(
1382
+ `complete -c pify -n '${condition}' -l ${flag.name}${short} -d '${flag.description.replace(/'/g, "")}'`
1383
+ );
1384
+ }
1385
+ }
1386
+ for (const flag of GLOBAL_FLAGS) {
1387
+ const short = flag.short ? ` -s ${flag.short}` : "";
1388
+ lines.push(`complete -c pify -l ${flag.name}${short} -d '${flag.description.replace(/'/g, "")}'`);
1389
+ }
1390
+ return `${lines.join("\n")}
1391
+ `;
1392
+ }
1393
+ function powershellScript() {
1394
+ const commands = COMMAND_SPECS.flatMap(allNames);
1395
+ const caseBlocks = COMMAND_SPECS.map((spec) => {
1396
+ const words = [...positionalWords(spec), ...flagTokens(spec)].map((w) => `'${w}'`).join(", ");
1397
+ return ` ${allNames(spec).map((n) => `'${n}'`).join(" ")} { @(${words}) }`;
1398
+ }).join("\n");
1399
+ return `# PowerShell completion for pify \u2014 generated by \`pify completions powershell\`
1400
+ # Install: pify completions powershell | Out-String | Invoke-Expression (add to $PROFILE)
1401
+ Register-ArgumentCompleter -Native -CommandName pify -ScriptBlock {
1402
+ param($wordToComplete, $commandAst, $cursorPosition)
1403
+ $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
1404
+ $candidates = if ($tokens.Count -le 1 -or ($tokens.Count -eq 2 -and $wordToComplete)) {
1405
+ @(${commands.map((c) => `'${c}'`).join(", ")})
1406
+ } else {
1407
+ switch ($tokens[1]) {
1408
+ ${caseBlocks}
1409
+ default { @() }
1410
+ }
1411
+ }
1412
+ $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1413
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1414
+ }
1415
+ }
1416
+ `;
1417
+ }
1418
+ function generateCompletion(shell) {
1419
+ switch (shell) {
1420
+ case "bash":
1421
+ return bashScript();
1422
+ case "zsh":
1423
+ return zshScript();
1424
+ case "fish":
1425
+ return fishScript();
1426
+ case "powershell":
1427
+ return powershellScript();
1428
+ default:
1429
+ throw usageError(
1430
+ `Unknown shell ${JSON.stringify(shell)}.`,
1431
+ `Usage: pify completions <${COMPLETION_SHELLS.join("|")}>`
1432
+ );
1433
+ }
1434
+ }
1435
+
1103
1436
  // src/help.ts
1104
1437
  function render(text) {
1105
1438
  return text.split("\n").map((line) => {
@@ -1124,6 +1457,7 @@ Commands:
1124
1457
  list Show the @pify catalog with install state
1125
1458
  doctor Diagnose the local pi/pify environment
1126
1459
  init [dir] Scaffold a new Pi Package
1460
+ completions <shell> Print a completion script (bash|zsh|fish|powershell)
1127
1461
 
1128
1462
  Short forms:
1129
1463
  i = install, rm = remove, up = update, ls = list
@@ -1159,15 +1493,21 @@ settings. Anything beyond these commands is pi's job: run pi --help.
1159
1493
  }
1160
1494
  var COMMAND_HELP = {
1161
1495
  setup: `Usage:
1162
- pify setup [--force] [--pi-version <version>]
1496
+ pify setup [--force] [--pi-version <version>] [--installer]
1163
1497
 
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.
1498
+ Install the pi coding agent. Safe to re-run; reports status when pi is
1499
+ already installed. Uses the same npm invocation as pi's official installers
1500
+ (--ignore-scripts --min-release-age=0), falls back to bun when npm is
1501
+ absent, and to --prefix ~/.local on POSIX when npm's global prefix is not
1502
+ writable (never sudo). Updates of an existing pi are pi's own job: run
1503
+ pify update pi.
1167
1504
 
1168
1505
  Options:
1169
1506
  --force Reinstall pi even if it is already installed
1170
1507
  --pi-version <version> Install an exact pi version (may downgrade)
1508
+ --installer Run pi's official interactive installer for
1509
+ this OS (can bootstrap Node and Git Bash;
1510
+ needs a terminal, refuses in CI)
1171
1511
 
1172
1512
  Examples:
1173
1513
  # Bootstrap a bare machine
@@ -1175,6 +1515,9 @@ Examples:
1175
1515
 
1176
1516
  # Pin the version your team has tested
1177
1517
  pify setup --pi-version 0.84.4
1518
+
1519
+ # Full native experience (Windows: install.ps1, Unix: install.sh)
1520
+ pify setup --installer
1178
1521
  `,
1179
1522
  install: `Usage:
1180
1523
  pify install <name...> [-l] [-a] [--dry-run]
@@ -1251,6 +1594,23 @@ Options:
1251
1594
 
1252
1595
  Examples:
1253
1596
  pify doctor
1597
+ `,
1598
+ completions: `Usage:
1599
+ pify completions <bash|zsh|fish|powershell>
1600
+
1601
+ Print a shell completion script generated from the live command registry \u2014
1602
+ command names, aliases, flags, and package names all tab-complete.
1603
+
1604
+ Examples:
1605
+ # bash / zsh \u2014 evaluate at shell startup (add to your rc file)
1606
+ eval "$(pify completions bash)"
1607
+ eval "$(pify completions zsh)"
1608
+
1609
+ # fish
1610
+ pify completions fish > ~/.config/fish/completions/pify.fish
1611
+
1612
+ # PowerShell \u2014 add to $PROFILE
1613
+ pify completions powershell | Out-String | Invoke-Expression
1254
1614
  `,
1255
1615
  init: `Usage:
1256
1616
  pify init [dir] [--name <package-name>] [--description <text>]
@@ -1277,85 +1637,40 @@ function commandHelp(command) {
1277
1637
  }
1278
1638
 
1279
1639
  // 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
- })
1640
+ var RUNNERS = {
1641
+ setup: (_p, v) => setup({
1642
+ force: Boolean(v.force),
1643
+ piVersion: v["pi-version"],
1644
+ installer: Boolean(v.installer)
1645
+ }),
1646
+ install: (p, v) => install(p, {
1647
+ local: Boolean(v.local),
1648
+ approve: Boolean(v.approve),
1649
+ dryRun: Boolean(v["dry-run"])
1650
+ }),
1651
+ remove: (p, v) => remove(p, {
1652
+ local: Boolean(v.local),
1653
+ approve: Boolean(v.approve),
1654
+ dryRun: Boolean(v["dry-run"])
1655
+ }),
1656
+ update: (p, v) => update(p, { catalogOnly: Boolean(v.catalog), dryRun: Boolean(v["dry-run"]) }),
1657
+ list: (_p, v) => list({ json: Boolean(v.json) }),
1658
+ doctor: (_p, v) => doctor({ json: Boolean(v.json) }),
1659
+ init: (p, v) => init(p[0], {
1660
+ name: v.name,
1661
+ description: v.description
1662
+ }),
1663
+ completions: async (p) => {
1664
+ if (!p[0]) {
1665
+ throw usageError("Missing shell.", "Usage: pify completions <bash|zsh|fish|powershell>");
1666
+ }
1667
+ out(generateCompletion(p[0]));
1668
+ return 0;
1357
1669
  }
1358
- ];
1670
+ };
1671
+ function findSpec(name) {
1672
+ return COMMAND_SPECS.find((c) => c.name === name || c.aliases.includes(name));
1673
+ }
1359
1674
  async function main(argv) {
1360
1675
  const args = argv.filter((a) => a !== "--no-color");
1361
1676
  if (args.length !== argv.length) setColor(false);
@@ -1375,8 +1690,8 @@ async function main(argv) {
1375
1690
  if (first.startsWith("-")) {
1376
1691
  throw usageError(`Unknown option ${first}.`, 'Use "pify --help".');
1377
1692
  }
1378
- const command = COMMANDS.find((c) => c.name === first || c.aliases.includes(first));
1379
- if (!command) {
1693
+ const spec = findSpec(first);
1694
+ if (!spec) {
1380
1695
  throw usageError(
1381
1696
  `Unknown command: ${JSON.stringify(first)}`,
1382
1697
  'Run "pify --help" to see available commands.'
@@ -1384,26 +1699,26 @@ async function main(argv) {
1384
1699
  }
1385
1700
  const rest = args.slice(1);
1386
1701
  if (rest.includes("-h") || rest.includes("--help")) {
1387
- out(commandHelp(command.name));
1702
+ out(commandHelp(spec.name));
1388
1703
  return 0;
1389
1704
  }
1390
1705
  let parsed;
1391
1706
  try {
1392
1707
  parsed = parseArgs({
1393
1708
  args: rest,
1394
- options: command.options,
1395
- allowPositionals: command.allowPositionals,
1709
+ options: parseArgsOptionsFor(spec),
1710
+ allowPositionals: spec.positional !== "none",
1396
1711
  strict: true
1397
1712
  });
1398
1713
  } catch (err) {
1399
1714
  const message = err instanceof Error ? err.message : String(err);
1400
1715
  const unknown = /Unknown option '(--?[^']+)'/.exec(message)?.[1];
1401
1716
  throw usageError(
1402
- unknown ? `Unknown option ${unknown} for "${command.name}".` : message,
1403
- `Use "pify --help" or "pify ${command.name} --help".`
1717
+ unknown ? `Unknown option ${unknown} for "${spec.name}".` : message,
1718
+ `Use "pify --help" or "pify ${spec.name} --help".`
1404
1719
  );
1405
1720
  }
1406
- return command.run(parsed.positionals, parsed.values);
1721
+ return RUNNERS[spec.name](parsed.positionals, parsed.values);
1407
1722
  }
1408
1723
  main(process.argv.slice(2)).then((code) => {
1409
1724
  process.exitCode = code;