@pify/cli 0.1.3 → 0.2.1

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/README.md CHANGED
@@ -34,6 +34,15 @@ pify init my-extension # scaffold a new Pi Package
34
34
 
35
35
  Every command supports `--help`; `install`/`remove`/`update` support `--dry-run`; `list`/`doctor` support `--json`.
36
36
 
37
+ **Tab completion** — generated from the live command registry (commands, aliases, flags, and package names all complete):
38
+
39
+ ```bash
40
+ eval "$(pify completions bash)" # bash — add to ~/.bashrc
41
+ eval "$(pify completions zsh)" # zsh — add to ~/.zshrc
42
+ pify completions fish > ~/.config/fish/completions/pify.fish
43
+ pify completions powershell | Out-String | Invoke-Expression # add to $PROFILE
44
+ ```
45
+
37
46
  ## How it relates to `pi`
38
47
 
39
48
  `pify` deliberately does **not** replace pi's package manager. Every package operation delegates to `pi install` / `pi remove` / `pi update`, which own `~/.pi/agent/settings.json` and the extension npm workspace — pify never edits pi's settings. What `pify` adds:
@@ -48,16 +57,19 @@ Every command supports `--help`; `install`/`remove`/`update` support `--dry-run`
48
57
 
49
58
  | Package | Description |
50
59
  |---|---|
51
- | `@pify/btw` | By-the-way notes: capture asides without derailing the agent |
60
+ | `@pify/ask-question` | Structured questions on built-in dialogs: 1-4 questions, multi-select, Other free-text |
61
+ | `@pify/btw` | By-the-way side conversations: a read-only, codebase-aware side agent |
52
62
  | `@pify/goal` | Pin a session goal and keep the agent anchored to it |
53
63
  | `@pify/memory` | Persistent memory across pi sessions |
54
64
  | `@pify/plan-mode` | Read-only planning mode with approve-then-execute gate |
55
65
  | `@pify/pretty` | Prettier TUI rendering for tool calls, diffs, markdown |
56
66
  | `@pify/subagent` | Spawn scoped subagents from within a pi session |
57
67
  | `@pify/swarm` | Coordinate multiple pi agents working in parallel |
58
- | `@pify/task` | Track tasks and progress inside pi sessions |
59
- | `@pify/usage` | Token and cost usage reporting |
60
- | `@pify/workflow` | Deterministic multi-step agent workflows |
68
+ | `@pify/task` | Task tracking: dependency graph, evidence-gated completion, reminders |
69
+ | `@pify/todo` | Agent working-memory checklist: TodoWrite-style, next-item surfacing |
70
+ | `@pify/usage` | Token and cost reporting: live footer, local-history dashboard |
71
+ | `@pify/workflow` | Deterministic agent orchestration: CC-style workflow scripts |
72
+ | `@pify/worktree` | Safe git-worktree management with safety rails and merge-back |
61
73
  | `@pify/yolo` | Auto-approve everything, with an undo trail |
62
74
 
63
75
  The catalog ships inside the CLI and refreshes (at most daily) from [`catalog.json` on `main`](https://github.com/pifydev/cli/blob/main/catalog.json), so newly published packages appear without a CLI update. A fetched catalog is validated before use — every entry must stay inside the `@pify` scope — and any invalid document is discarded entirely. Overrides: `PIFY_CATALOG_URL` (remote URL), `PIFY_OFFLINE=1` or `PI_OFFLINE=1` (skip all network lookups).
package/catalog.json CHANGED
@@ -1,85 +1,106 @@
1
1
  {
2
- "version": 1,
2
+ "version": 15,
3
3
  "org": "pifydev",
4
4
  "scope": "@pify",
5
5
  "remote": "https://raw.githubusercontent.com/pifydev/cli/main/catalog.json",
6
6
  "packages": [
7
+ {
8
+ "name": "ask-question",
9
+ "npm": "@pify/ask-question",
10
+ "repo": "https://github.com/pifydev/ask-question",
11
+ "description": "CC AskUserQuestion-shaped tool on built-in dialogs: 1-4 questions, multi-select, Other free-text",
12
+ "status": "published"
13
+ },
7
14
  {
8
15
  "name": "btw",
9
16
  "npm": "@pify/btw",
10
17
  "repo": "https://github.com/pifydev/btw",
11
- "description": "By-the-way notes: capture asides during a session without derailing the agent",
12
- "status": "planned"
18
+ "description": "By-the-way side conversations: a read-only, codebase-aware side agent in a widget",
19
+ "status": "published"
13
20
  },
14
21
  {
15
22
  "name": "goal",
16
23
  "npm": "@pify/goal",
17
24
  "repo": "https://github.com/pifydev/goal",
18
- "description": "Pin a session goal and keep the agent anchored to it",
19
- "status": "planned"
25
+ "description": "Pin a session goal and keep the agent anchored to it: settled-idle continuation with safety limits",
26
+ "status": "published"
20
27
  },
21
28
  {
22
29
  "name": "memory",
23
30
  "npm": "@pify/memory",
24
31
  "repo": "https://github.com/pifydev/memory",
25
- "description": "Persistent memory across pi sessions",
26
- "status": "planned"
32
+ "description": "Persistent memory: two-tier markdown storage, FTS5 search, secret-scanned writes",
33
+ "status": "published"
27
34
  },
28
35
  {
29
36
  "name": "plan-mode",
30
37
  "npm": "@pify/plan-mode",
31
38
  "repo": "https://github.com/pifydev/plan-mode",
32
- "description": "Read-only planning mode with an explicit approve-then-execute gate",
33
- "status": "planned"
39
+ "description": "Read-only planning mode with an enforced approve-then-execute gate: tool policy, plan files, fresh-session handoff",
40
+ "status": "published"
34
41
  },
35
42
  {
36
43
  "name": "pretty",
37
44
  "npm": "@pify/pretty",
38
45
  "repo": "https://github.com/pifydev/pretty",
39
- "description": "Prettier TUI rendering for tool calls, diffs, and markdown",
40
- "status": "planned"
46
+ "description": "Compact theme-aware rendering for built-in tools: summaries, highlighted reads, colorized diffs",
47
+ "status": "published"
41
48
  },
42
49
  {
43
50
  "name": "subagent",
44
51
  "npm": "@pify/subagent",
45
52
  "repo": "https://github.com/pifydev/subagent",
46
- "description": "Spawn scoped subagents from within a pi session",
47
- "status": "planned"
53
+ "description": "Spawn scoped subagents: agent_run/agent_result, custom agent types, turn caps and tool allowlists",
54
+ "status": "published"
48
55
  },
49
56
  {
50
57
  "name": "swarm",
51
58
  "npm": "@pify/swarm",
52
59
  "repo": "https://github.com/pifydev/swarm",
53
- "description": "Coordinate multiple pi agents working in parallel",
54
- "status": "planned"
60
+ "description": "Coordinate multiple pi agents in parallel: fan-out with auto-routing, concurrency queue, aggregated reports",
61
+ "status": "published"
55
62
  },
56
63
  {
57
64
  "name": "task",
58
65
  "npm": "@pify/task",
59
66
  "repo": "https://github.com/pifydev/task",
60
- "description": "Track tasks and progress inside pi sessions",
61
- "status": "planned"
67
+ "description": "Task tracking with dependency graph, evidence-gated completion, live widget, CC-style reminders",
68
+ "status": "published"
69
+ },
70
+ {
71
+ "name": "todo",
72
+ "npm": "@pify/todo",
73
+ "repo": "https://github.com/pifydev/todo",
74
+ "description": "Agent working-memory checklist: CC TodoWrite-style complete-replacement, next-item surfacing, live widget",
75
+ "status": "published"
62
76
  },
63
77
  {
64
78
  "name": "usage",
65
79
  "npm": "@pify/usage",
66
80
  "repo": "https://github.com/pifydev/usage",
67
- "description": "Token and cost usage reporting for pi sessions",
68
- "status": "planned"
81
+ "description": "Token and cost reporting: live footer, /usage dashboard over local session history, zero network",
82
+ "status": "published"
69
83
  },
70
84
  {
71
85
  "name": "workflow",
72
86
  "npm": "@pify/workflow",
73
87
  "repo": "https://github.com/pifydev/workflow",
74
- "description": "Deterministic multi-step agent workflows for pi",
75
- "status": "planned"
88
+ "description": "Deterministic agent orchestration: CC-style workflow scripts with agent()/parallel()/pipeline()",
89
+ "status": "published"
90
+ },
91
+ {
92
+ "name": "worktree",
93
+ "npm": "@pify/worktree",
94
+ "repo": "https://github.com/pifydev/worktree",
95
+ "description": "Safe git-worktree management: create/list/merge/remove with safety rails, no shell interpolation",
96
+ "status": "published"
76
97
  },
77
98
  {
78
99
  "name": "yolo",
79
100
  "npm": "@pify/yolo",
80
101
  "repo": "https://github.com/pifydev/yolo",
81
- "description": "One toggle to auto-approve everything, with an undo trail",
82
- "status": "planned"
102
+ "description": "One toggle to auto-approve everything, with an undo trail: three-tier bash guard, file pre-images",
103
+ "status": "published"
83
104
  }
84
105
  ]
85
106
  }
package/dist/cli.js CHANGED
@@ -762,12 +762,12 @@ async function update(targets, opts) {
762
762
  const updateEverything = targets.length === 0;
763
763
  const piOnly = targets.length === 1 && (targets[0] === "pi" || targets[0] === "self");
764
764
  const installed = installedPifyPackages();
765
- let packageNames = [];
765
+ let packageNames2 = [];
766
766
  if (updateEverything) {
767
- packageNames = [...installed.keys()].sort();
767
+ packageNames2 = [...installed.keys()].sort();
768
768
  } else if (!piOnly) {
769
- packageNames = targets.map(resolvePifyName);
770
- for (const name of packageNames) {
769
+ packageNames2 = targets.map(resolvePifyName);
770
+ for (const name of packageNames2) {
771
771
  if (!installed.has(name)) {
772
772
  throw notFoundError(`${name} is not installed.`, `pify install ${name}`);
773
773
  }
@@ -775,7 +775,7 @@ async function update(targets, opts) {
775
775
  }
776
776
  if (opts.dryRun) {
777
777
  if (updateEverything || piOnly) out("pi update");
778
- for (const name of packageNames) out(`pi update npm:@pify/${name}`);
778
+ for (const name of packageNames2) out(`pi update npm:@pify/${name}`);
779
779
  if (updateEverything) out("refresh catalog");
780
780
  return 0;
781
781
  }
@@ -789,20 +789,20 @@ async function update(targets, opts) {
789
789
  if (code !== 0) failed.push("pi");
790
790
  }
791
791
  }
792
- if (packageNames.length > 0 && !piStatus().installed) {
792
+ if (packageNames2.length > 0 && !piStatus().installed) {
793
793
  throw new PifyError(
794
794
  "pi is required to update packages.",
795
795
  ExitCode.ENVIRONMENT,
796
796
  "Run pify setup first."
797
797
  );
798
798
  }
799
- for (const name of packageNames) {
799
+ for (const name of packageNames2) {
800
800
  const args = ["update", `npm:@pify/${name}`];
801
801
  step(`pi ${args.join(" ")}`);
802
802
  const code = await delegate(args);
803
803
  if (code !== 0) failed.push(name);
804
804
  }
805
- if (updateEverything && packageNames.length === 0) {
805
+ if (updateEverything && packageNames2.length === 0) {
806
806
  hint("No @pify packages installed; nothing more to update.");
807
807
  }
808
808
  if (updateEverything) {
@@ -1195,6 +1195,244 @@ async function init(dirArg, opts) {
1195
1195
  return 0;
1196
1196
  }
1197
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
+
1198
1436
  // src/help.ts
1199
1437
  function render(text) {
1200
1438
  return text.split("\n").map((line) => {
@@ -1219,6 +1457,7 @@ Commands:
1219
1457
  list Show the @pify catalog with install state
1220
1458
  doctor Diagnose the local pi/pify environment
1221
1459
  init [dir] Scaffold a new Pi Package
1460
+ completions <shell> Print a completion script (bash|zsh|fish|powershell)
1222
1461
 
1223
1462
  Short forms:
1224
1463
  i = install, rm = remove, up = update, ls = list
@@ -1355,6 +1594,23 @@ Options:
1355
1594
 
1356
1595
  Examples:
1357
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
1358
1614
  `,
1359
1615
  init: `Usage:
1360
1616
  pify init [dir] [--name <package-name>] [--description <text>]
@@ -1381,90 +1637,40 @@ function commandHelp(command) {
1381
1637
  }
1382
1638
 
1383
1639
  // src/cli.ts
1384
- var COMMANDS = [
1385
- {
1386
- name: "setup",
1387
- aliases: [],
1388
- options: {
1389
- force: { type: "boolean", default: false },
1390
- "pi-version": { type: "string" },
1391
- installer: { type: "boolean", default: false }
1392
- },
1393
- allowPositionals: false,
1394
- run: (_p, v) => setup({
1395
- force: Boolean(v.force),
1396
- piVersion: v["pi-version"],
1397
- installer: Boolean(v.installer)
1398
- })
1399
- },
1400
- {
1401
- name: "install",
1402
- aliases: ["i", "add"],
1403
- options: {
1404
- local: { type: "boolean", short: "l", default: false },
1405
- approve: { type: "boolean", short: "a", default: false },
1406
- "dry-run": { type: "boolean", default: false }
1407
- },
1408
- allowPositionals: true,
1409
- run: (p, v) => install(p, {
1410
- local: Boolean(v.local),
1411
- approve: Boolean(v.approve),
1412
- dryRun: Boolean(v["dry-run"])
1413
- })
1414
- },
1415
- {
1416
- name: "remove",
1417
- aliases: ["rm", "uninstall"],
1418
- options: {
1419
- local: { type: "boolean", short: "l", default: false },
1420
- approve: { type: "boolean", short: "a", default: false },
1421
- "dry-run": { type: "boolean", default: false }
1422
- },
1423
- allowPositionals: true,
1424
- run: (p, v) => remove(p, {
1425
- local: Boolean(v.local),
1426
- approve: Boolean(v.approve),
1427
- dryRun: Boolean(v["dry-run"])
1428
- })
1429
- },
1430
- {
1431
- name: "update",
1432
- aliases: ["up"],
1433
- options: {
1434
- catalog: { type: "boolean", default: false },
1435
- "dry-run": { type: "boolean", default: false }
1436
- },
1437
- allowPositionals: true,
1438
- run: (p, v) => update(p, { catalogOnly: Boolean(v.catalog), dryRun: Boolean(v["dry-run"]) })
1439
- },
1440
- {
1441
- name: "list",
1442
- aliases: ["ls"],
1443
- options: { json: { type: "boolean", default: false } },
1444
- allowPositionals: false,
1445
- run: (_p, v) => list({ json: Boolean(v.json) })
1446
- },
1447
- {
1448
- name: "doctor",
1449
- aliases: [],
1450
- options: { json: { type: "boolean", default: false } },
1451
- allowPositionals: false,
1452
- run: (_p, v) => doctor({ json: Boolean(v.json) })
1453
- },
1454
- {
1455
- name: "init",
1456
- aliases: [],
1457
- options: {
1458
- name: { type: "string" },
1459
- description: { type: "string" }
1460
- },
1461
- allowPositionals: true,
1462
- run: (p, v) => init(p[0], {
1463
- name: v.name,
1464
- description: v.description
1465
- })
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;
1466
1669
  }
1467
- ];
1670
+ };
1671
+ function findSpec(name) {
1672
+ return COMMAND_SPECS.find((c) => c.name === name || c.aliases.includes(name));
1673
+ }
1468
1674
  async function main(argv) {
1469
1675
  const args = argv.filter((a) => a !== "--no-color");
1470
1676
  if (args.length !== argv.length) setColor(false);
@@ -1484,8 +1690,8 @@ async function main(argv) {
1484
1690
  if (first.startsWith("-")) {
1485
1691
  throw usageError(`Unknown option ${first}.`, 'Use "pify --help".');
1486
1692
  }
1487
- const command = COMMANDS.find((c) => c.name === first || c.aliases.includes(first));
1488
- if (!command) {
1693
+ const spec = findSpec(first);
1694
+ if (!spec) {
1489
1695
  throw usageError(
1490
1696
  `Unknown command: ${JSON.stringify(first)}`,
1491
1697
  'Run "pify --help" to see available commands.'
@@ -1493,26 +1699,26 @@ async function main(argv) {
1493
1699
  }
1494
1700
  const rest = args.slice(1);
1495
1701
  if (rest.includes("-h") || rest.includes("--help")) {
1496
- out(commandHelp(command.name));
1702
+ out(commandHelp(spec.name));
1497
1703
  return 0;
1498
1704
  }
1499
1705
  let parsed;
1500
1706
  try {
1501
1707
  parsed = parseArgs({
1502
1708
  args: rest,
1503
- options: command.options,
1504
- allowPositionals: command.allowPositionals,
1709
+ options: parseArgsOptionsFor(spec),
1710
+ allowPositionals: spec.positional !== "none",
1505
1711
  strict: true
1506
1712
  });
1507
1713
  } catch (err) {
1508
1714
  const message = err instanceof Error ? err.message : String(err);
1509
1715
  const unknown = /Unknown option '(--?[^']+)'/.exec(message)?.[1];
1510
1716
  throw usageError(
1511
- unknown ? `Unknown option ${unknown} for "${command.name}".` : message,
1512
- `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".`
1513
1719
  );
1514
1720
  }
1515
- return command.run(parsed.positionals, parsed.values);
1721
+ return RUNNERS[spec.name](parsed.positionals, parsed.values);
1516
1722
  }
1517
1723
  main(process.argv.slice(2)).then((code) => {
1518
1724
  process.exitCode = code;
package/dist/index.d.ts CHANGED
@@ -167,6 +167,39 @@ declare function assertSafeArg(arg: string, what?: string): void;
167
167
  */
168
168
  declare function which(command: string): string | null;
169
169
 
170
+ declare function generateCompletion(shell: string): string;
171
+
172
+ /**
173
+ * Declarative command surface — the single source of truth consumed by BOTH
174
+ * the argument parser (cli.ts) and the completion generator (completions.ts),
175
+ * so completions can never drift from the real CLI (oh-my-pi's principle).
176
+ */
177
+ interface FlagSpec {
178
+ name: string;
179
+ short?: string;
180
+ /** Flag takes a value (e.g. --pi-version <version>). */
181
+ takesValue?: boolean;
182
+ description: string;
183
+ }
184
+ type PositionalKind = "none" | "packages" | "updateTargets" | "directory" | "shell";
185
+ interface CommandSpec {
186
+ name: string;
187
+ aliases: string[];
188
+ description: string;
189
+ positional: PositionalKind;
190
+ flags: FlagSpec[];
191
+ }
192
+ declare const GLOBAL_FLAGS: FlagSpec[];
193
+ declare const COMMAND_SPECS: CommandSpec[];
194
+ declare const COMPLETION_SHELLS: readonly ["bash", "zsh", "fish", "powershell"];
195
+ type CompletionShell = (typeof COMPLETION_SHELLS)[number];
196
+ /** Build the strict parseArgs options table for one command. */
197
+ declare function parseArgsOptionsFor(spec: CommandSpec): Record<string, {
198
+ type: "boolean" | "string";
199
+ short?: string;
200
+ default?: boolean;
201
+ }>;
202
+
170
203
  /**
171
204
  * The five `pify init` file templates, embedded as string literals so the
172
205
  * tarball ships nothing extra. Every line of the package.json template
@@ -211,4 +244,4 @@ declare class PifyError extends Error {
211
244
  constructor(message: string, code?: ExitCodeValue, hint?: string);
212
245
  }
213
246
 
214
- export { type Catalog, type CatalogPackage, ExitCode, INDEX_TS, type InstallSpec, type InstallTarget, type InstalledPifyPackage, NODE_FLOOR, PI_PACKAGE, type PiSettings, type PiStatus, PifyError, README_MD, TESTED_PI_VERSION, type TemplateVars, VERSION, agentDir, assertSafeArg, compareSemver, fetchLatestPiVersion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, npmInstallPiArgs, officialInstallerCommand, packageJsonTemplate, parseInstallSpec, piStatus, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
247
+ export { COMMAND_SPECS, COMPLETION_SHELLS, type Catalog, type CatalogPackage, type CommandSpec, type CompletionShell, ExitCode, type FlagSpec, GLOBAL_FLAGS, INDEX_TS, type InstallSpec, type InstallTarget, type InstalledPifyPackage, NODE_FLOOR, PI_PACKAGE, type PiSettings, type PiStatus, PifyError, README_MD, TESTED_PI_VERSION, type TemplateVars, VERSION, agentDir, assertSafeArg, compareSemver, fetchLatestPiVersion, generateCompletion, installPi, installedPifyPackages, installedVersionOnDisk, interpolate, isOffline, loadBundledCatalog, loadCatalog, npmInstallPiArgs, officialInstallerCommand, packageJsonTemplate, parseArgsOptionsFor, parseInstallSpec, piStatus, readSettings, refreshCatalog, resolveInstallTarget, resolvePifyName, updatePi, validateCatalog, which };
package/dist/index.js CHANGED
@@ -551,6 +551,244 @@ function installedVersionOnDisk(name, scope) {
551
551
  }
552
552
  }
553
553
 
554
+ // src/registry.ts
555
+ var GLOBAL_FLAGS = [
556
+ { name: "help", short: "h", description: "Show help" },
557
+ { name: "version", short: "v", description: "Print the pify version" },
558
+ { name: "no-color", description: "Disable colored output" }
559
+ ];
560
+ var COMMAND_SPECS = [
561
+ {
562
+ name: "setup",
563
+ aliases: [],
564
+ description: "Install the pi coding agent (safe to re-run)",
565
+ positional: "none",
566
+ flags: [
567
+ { name: "force", description: "Reinstall pi even if present" },
568
+ { name: "pi-version", takesValue: true, description: "Install an exact pi version" },
569
+ { name: "installer", description: "Run pi's official interactive installer" }
570
+ ]
571
+ },
572
+ {
573
+ name: "install",
574
+ aliases: ["i", "add"],
575
+ description: "Install @pify packages by short name",
576
+ positional: "packages",
577
+ flags: [
578
+ { name: "local", short: "l", description: "Project scope" },
579
+ { name: "approve", short: "a", description: "Forward --approve to pi" },
580
+ { name: "dry-run", description: "Print the pi commands without running them" }
581
+ ]
582
+ },
583
+ {
584
+ name: "remove",
585
+ aliases: ["rm", "uninstall"],
586
+ description: "Remove installed @pify packages",
587
+ positional: "packages",
588
+ flags: [
589
+ { name: "local", short: "l", description: "Project scope" },
590
+ { name: "approve", short: "a", description: "Forward --approve to pi" },
591
+ { name: "dry-run", description: "Print the pi commands without running them" }
592
+ ]
593
+ },
594
+ {
595
+ name: "update",
596
+ aliases: ["up"],
597
+ description: "Update pi and installed @pify packages",
598
+ positional: "updateTargets",
599
+ flags: [
600
+ { name: "catalog", description: "Refresh the catalog cache only" },
601
+ { name: "dry-run", description: "Print the update plan without executing" }
602
+ ]
603
+ },
604
+ {
605
+ name: "list",
606
+ aliases: ["ls"],
607
+ description: "Show the @pify catalog with install state",
608
+ positional: "none",
609
+ flags: [{ name: "json", description: "Machine-readable output" }]
610
+ },
611
+ {
612
+ name: "doctor",
613
+ aliases: [],
614
+ description: "Diagnose the local pi/pify environment",
615
+ positional: "none",
616
+ flags: [{ name: "json", description: "Machine-readable output" }]
617
+ },
618
+ {
619
+ name: "init",
620
+ aliases: [],
621
+ description: "Scaffold a new Pi Package",
622
+ positional: "directory",
623
+ flags: [
624
+ { name: "name", takesValue: true, description: "npm package name" },
625
+ { name: "description", takesValue: true, description: "Package description" }
626
+ ]
627
+ },
628
+ {
629
+ name: "completions",
630
+ aliases: [],
631
+ description: "Print a shell completion script",
632
+ positional: "shell",
633
+ flags: []
634
+ }
635
+ ];
636
+ var COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell"];
637
+ function parseArgsOptionsFor(spec) {
638
+ const options = {};
639
+ for (const flag of spec.flags) {
640
+ options[flag.name] = flag.takesValue ? { type: "string", ...flag.short ? { short: flag.short } : {} } : { type: "boolean", default: false, ...flag.short ? { short: flag.short } : {} };
641
+ }
642
+ return options;
643
+ }
644
+
645
+ // src/completions.ts
646
+ function packageNames() {
647
+ return loadBundledCatalog().packages.map((p) => p.name);
648
+ }
649
+ function allNames(spec) {
650
+ return [spec.name, ...spec.aliases];
651
+ }
652
+ function flagTokens(spec) {
653
+ return spec.flags.flatMap((f) => [`--${f.name}`, ...f.short ? [`-${f.short}`] : []]);
654
+ }
655
+ function positionalWords(spec) {
656
+ switch (spec.positional) {
657
+ case "packages":
658
+ return packageNames();
659
+ case "updateTargets":
660
+ return ["pi", ...packageNames()];
661
+ case "shell":
662
+ return [...COMPLETION_SHELLS];
663
+ default:
664
+ return [];
665
+ }
666
+ }
667
+ function bashScript() {
668
+ const commands = COMMAND_SPECS.flatMap(allNames).join(" ");
669
+ const cases = COMMAND_SPECS.map((spec) => {
670
+ const words = [...positionalWords(spec), ...flagTokens(spec)].join(" ");
671
+ return ` ${allNames(spec).join("|")}) words="${words}" ;;`;
672
+ }).join("\n");
673
+ return `# bash completion for pify \u2014 generated by \`pify completions bash\`
674
+ # Install: eval "$(pify completions bash)" (or save to a bash-completion dir)
675
+ _pify_completions() {
676
+ local cur words
677
+ cur="\${COMP_WORDS[COMP_CWORD]}"
678
+ if [ "$COMP_CWORD" -eq 1 ]; then
679
+ COMPREPLY=($(compgen -W "${commands} ${GLOBAL_FLAGS.map((f) => `--${f.name}`).join(" ")}" -- "$cur"))
680
+ return
681
+ fi
682
+ case "\${COMP_WORDS[1]}" in
683
+ ${cases}
684
+ *) words="" ;;
685
+ esac
686
+ COMPREPLY=($(compgen -W "$words" -- "$cur"))
687
+ }
688
+ complete -F _pify_completions pify
689
+ `;
690
+ }
691
+ function zshScript() {
692
+ const commandDescs = COMMAND_SPECS.flatMap(
693
+ (spec) => allNames(spec).map((n) => ` '${n}:${spec.description.replace(/'/g, "")}'`)
694
+ ).join("\n");
695
+ const cases = COMMAND_SPECS.map((spec) => {
696
+ const words = [...positionalWords(spec), ...flagTokens(spec)].map((w) => `'${w}'`).join(" ");
697
+ return ` ${allNames(spec).join("|")}) _values 'value' ${words || "''"} ;;`;
698
+ }).join("\n");
699
+ return `#compdef pify
700
+ # zsh completion for pify \u2014 generated by \`pify completions zsh\`
701
+ # Install: eval "$(pify completions zsh)" (or save into a directory on $fpath)
702
+ _pify() {
703
+ local -a commands
704
+ commands=(
705
+ ${commandDescs}
706
+ )
707
+ if (( CURRENT == 2 )); then
708
+ _describe 'command' commands
709
+ return
710
+ fi
711
+ case "$words[2]" in
712
+ ${cases}
713
+ esac
714
+ }
715
+ _pify "$@"
716
+ `;
717
+ }
718
+ function fishScript() {
719
+ const lines = [
720
+ `# fish completion for pify \u2014 generated by \`pify completions fish\``,
721
+ `# Install: pify completions fish > ~/.config/fish/completions/pify.fish`,
722
+ `complete -c pify -f`
723
+ ];
724
+ for (const spec of COMMAND_SPECS) {
725
+ for (const name of allNames(spec)) {
726
+ lines.push(
727
+ `complete -c pify -n __fish_use_subcommand -a ${name} -d '${spec.description.replace(/'/g, "")}'`
728
+ );
729
+ }
730
+ const condition = `__fish_seen_subcommand_from ${allNames(spec).join(" ")}`;
731
+ const words = positionalWords(spec);
732
+ if (words.length > 0) {
733
+ lines.push(`complete -c pify -n '${condition}' -a '${words.join(" ")}'`);
734
+ }
735
+ for (const flag of spec.flags) {
736
+ const short = flag.short ? ` -s ${flag.short}` : "";
737
+ lines.push(
738
+ `complete -c pify -n '${condition}' -l ${flag.name}${short} -d '${flag.description.replace(/'/g, "")}'`
739
+ );
740
+ }
741
+ }
742
+ for (const flag of GLOBAL_FLAGS) {
743
+ const short = flag.short ? ` -s ${flag.short}` : "";
744
+ lines.push(`complete -c pify -l ${flag.name}${short} -d '${flag.description.replace(/'/g, "")}'`);
745
+ }
746
+ return `${lines.join("\n")}
747
+ `;
748
+ }
749
+ function powershellScript() {
750
+ const commands = COMMAND_SPECS.flatMap(allNames);
751
+ const caseBlocks = COMMAND_SPECS.map((spec) => {
752
+ const words = [...positionalWords(spec), ...flagTokens(spec)].map((w) => `'${w}'`).join(", ");
753
+ return ` ${allNames(spec).map((n) => `'${n}'`).join(" ")} { @(${words}) }`;
754
+ }).join("\n");
755
+ return `# PowerShell completion for pify \u2014 generated by \`pify completions powershell\`
756
+ # Install: pify completions powershell | Out-String | Invoke-Expression (add to $PROFILE)
757
+ Register-ArgumentCompleter -Native -CommandName pify -ScriptBlock {
758
+ param($wordToComplete, $commandAst, $cursorPosition)
759
+ $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
760
+ $candidates = if ($tokens.Count -le 1 -or ($tokens.Count -eq 2 -and $wordToComplete)) {
761
+ @(${commands.map((c) => `'${c}'`).join(", ")})
762
+ } else {
763
+ switch ($tokens[1]) {
764
+ ${caseBlocks}
765
+ default { @() }
766
+ }
767
+ }
768
+ $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
769
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
770
+ }
771
+ }
772
+ `;
773
+ }
774
+ function generateCompletion(shell) {
775
+ switch (shell) {
776
+ case "bash":
777
+ return bashScript();
778
+ case "zsh":
779
+ return zshScript();
780
+ case "fish":
781
+ return fishScript();
782
+ case "powershell":
783
+ return powershellScript();
784
+ default:
785
+ throw usageError(
786
+ `Unknown shell ${JSON.stringify(shell)}.`,
787
+ `Usage: pify completions <${COMPLETION_SHELLS.join("|")}>`
788
+ );
789
+ }
790
+ }
791
+
554
792
  // src/templates.ts
555
793
  function interpolate(template, vars) {
556
794
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
@@ -669,7 +907,10 @@ Then install anywhere with: pi install npm:{{name}}
669
907
  Tested with pi {{piVersion}}.
670
908
  `;
671
909
  export {
910
+ COMMAND_SPECS,
911
+ COMPLETION_SHELLS,
672
912
  ExitCode,
913
+ GLOBAL_FLAGS,
673
914
  INDEX_TS,
674
915
  NODE_FLOOR,
675
916
  PI_PACKAGE,
@@ -681,6 +922,7 @@ export {
681
922
  assertSafeArg,
682
923
  compareSemver,
683
924
  fetchLatestPiVersion,
925
+ generateCompletion,
684
926
  installPi,
685
927
  installedPifyPackages,
686
928
  installedVersionOnDisk,
@@ -691,6 +933,7 @@ export {
691
933
  npmInstallPiArgs,
692
934
  officialInstallerCommand,
693
935
  packageJsonTemplate,
936
+ parseArgsOptionsFor,
694
937
  parseInstallSpec,
695
938
  piStatus,
696
939
  readSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Front door for the Pify suite: install and update the pi coding agent, manage @pify packages, and scaffold Pi Packages",
5
5
  "keywords": [
6
6
  "pify",