@tommy-ca/lazypi 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +30 -4
  2. package/bin/lazypi.mjs +101 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,17 +13,43 @@ LazyPi will:
13
13
  1. Install `pi` for you if it isn't installed yet.
14
14
  2. Ask if you want to install all the packages or choose which to install.
15
15
 
16
- That setup is the harness core — isolated sub-agents, a structured ask gate, skill visibility, $ skill mention, a long-objective gate, side chat, context budgeting, code simplification review, web research, FFF search, a workflow engine for sub-agent fan-out, and ponytail discipline review. A five-package optional tier (targeted LSP diagnostics, interactive shell overlays, autonomous research loops, structured todo tracking, markdown-backed memory) installs on request with `--only optional`. Other extras (skill arguments, MCP, ralph-wiggum, themes) install on demand with `pi install`.
16
+ That setup is the 12-package harness core. `--yes` installs those 12. On a TTY, Install everything installs all 17. Packages LazyPi dropped stay installable with `pi install`. LazyPi does not manage them. See the Catalog table and `docs/docs/philosophy.html`.
17
17
 
18
18
  That's it. Once done - run `pi` and experience a feature rich coding agent experience.
19
19
 
20
20
  Install is **idempotent** — LazyPi reads your Pi settings and skips any package that is already installed, so re-running is safe.
21
21
 
22
+ ## Catalog
23
+
24
+ `--yes` installs 12 core packages. TTY Install everything installs all 17.
25
+
26
+ | id | category | why |
27
+ | --- | --- | --- |
28
+ | subagents | core | Isolated specialists instead of one bloated agent |
29
+ | pi-ask-user | core | Stop and ask instead of guessing |
30
+ | pi-skillful | core | Keep 4 to 6 skills visible. Hide the rest |
31
+ | mention-skill | core | `$` mention is the one way to reach hidden skills |
32
+ | goal | core | Long-objective gate. Not a checkbox list |
33
+ | btw | core | Side chat that does not pollute history |
34
+ | context-usage | core | See what is burning the window |
35
+ | simplify | core | Review diffs for clarity after implementation |
36
+ | web-access | core | Search and fetch instead of stale training data |
37
+ | fff | core | Paged fuzzy search so grep does not flood the transcript |
38
+ | dynamic-workflows | core | Fan work out across subagents |
39
+ | ponytail | core | Stdlib-first discipline. Lean catalog as a habit |
40
+ | lsp | optional | Targeted diagnostics. Cataloged, not default |
41
+ | interactive-shell | optional | TUI overlays. Chrome, not control plane |
42
+ | autoresearch | optional | Experiment loops. Convenience, not core |
43
+ | todos | optional | Structured tracker. Optional because core forbids checkbox todos as default |
44
+ | memory | optional | Git-backed memory. Infrastructure, not control plane |
45
+
46
+ Dropped packages are outside this table. `pi install` still works. LazyPi does not manage them.
47
+
22
48
  ## Commands
23
49
 
24
50
  | Command | What it does |
25
51
  | --- | --- |
26
- | `npx @tommy-ca/lazypi` | Install all or selected catalog (interactive picker by default) |
52
+ | `npx @tommy-ca/lazypi` | TTY Install everything is 17. `--yes` is the 12 core packages. Picker otherwise. |
27
53
  | `npx @tommy-ca/lazypi remove <id>` | Remove a catalog package by id (or pass a raw pi source) |
28
54
  | `npx @tommy-ca/lazypi status` | Show which catalog packages are installed, missing, or extra |
29
55
  | `npx @tommy-ca/lazypi update` | Run `pi update` for installed Pi packages |
@@ -71,7 +97,7 @@ specs behave the same way, so `cd` out of the checkout first.
71
97
 
72
98
  ## Site / docs
73
99
 
74
- The site at [lazypi.org](https://lazypi.org) lives in `docs/` and is a Jekyll site compiled by GitHub Pages automatically on push to `master`.
100
+ The Jekyll source lives in `docs/`. This fork does not enable GitHub Pages, and [lazypi.org](https://lazypi.org) still serves the upstream installer.
75
101
 
76
102
  To preview locally (requires Ruby + Bundler):
77
103
 
@@ -106,4 +132,4 @@ before treating releases as automated.
106
132
 
107
133
  ---
108
134
 
109
- For the full list of included packages and optional extras, see [lazypi.org](https://lazypi.org).
135
+ The catalog table above is the npm list. `docs/docs/philosophy.html` explains membership. Run `npx @tommy-ca/lazypi` to install.
package/bin/lazypi.mjs CHANGED
@@ -96,6 +96,25 @@ function renderLogo() {
96
96
  // ---------------------------------------------------------------------------
97
97
  const KNOWN_COMMANDS = new Set(["install", "status", "update", "doctor", "remove"]);
98
98
 
99
+ function assignSelector(flags, key, raw) {
100
+ const list = parseList(raw);
101
+ if (list.length === 0) {
102
+ flags.usageError ??= `--${key} requires a non-empty list`;
103
+ return;
104
+ }
105
+ flags[key] = list;
106
+ }
107
+
108
+ function consumeSelector(args, i, flags, key) {
109
+ const value = args[i + 1];
110
+ if (value == null || value.startsWith("-")) {
111
+ flags.usageError ??= `--${key} requires a non-empty list`;
112
+ return i;
113
+ }
114
+ assignSelector(flags, key, value);
115
+ return i + 1;
116
+ }
117
+
99
118
  function parseArgs(args) {
100
119
  const flags = {
101
120
  command: "install",
@@ -107,6 +126,7 @@ function parseArgs(args) {
107
126
  only: null,
108
127
  except: null,
109
128
  targets: [],
129
+ usageError: null,
110
130
  };
111
131
 
112
132
  let i = 0;
@@ -121,10 +141,10 @@ function parseArgs(args) {
121
141
  else if (arg === "-y" || arg === "--yes") flags.yes = true;
122
142
  else if (arg === "-V" || arg === "--version") flags.version = true;
123
143
  else if (arg === "-h" || arg === "--help") flags.help = true;
124
- else if (arg === "--only") flags.only = parseList(args[++i]);
125
- else if (arg.startsWith("--only=")) flags.only = parseList(arg.slice("--only=".length));
126
- else if (arg === "--except") flags.except = parseList(args[++i]);
127
- else if (arg.startsWith("--except=")) flags.except = parseList(arg.slice("--except=".length));
144
+ else if (arg === "--only") i = consumeSelector(args, i, flags, "only");
145
+ else if (arg.startsWith("--only=")) assignSelector(flags, "only", arg.slice("--only=".length));
146
+ else if (arg === "--except") i = consumeSelector(args, i, flags, "except");
147
+ else if (arg.startsWith("--except=")) assignSelector(flags, "except", arg.slice("--except=".length));
128
148
  else if (flags.command === "remove" && !arg.startsWith("-")) flags.targets.push(arg);
129
149
  else {
130
150
  console.error(red(`Unknown argument: ${arg}`));
@@ -134,6 +154,10 @@ function parseArgs(args) {
134
154
  }
135
155
  }
136
156
 
157
+ if (flags.only && flags.except) {
158
+ flags.usageError ??= "Use either --only or --except, not both";
159
+ }
160
+
137
161
  return flags;
138
162
  }
139
163
 
@@ -151,9 +175,10 @@ function validateSelectors(list, label) {
151
175
  if (bad.length > 0) {
152
176
  console.error(red(`Unknown ${label}: ${bad.join(", ")}`));
153
177
  console.error(`Valid categories: ${CATEGORIES.join(", ")}`);
154
- console.error(`Valid package ids: ${[...ids].join(", ")}`);
155
- exit(2);
178
+ console.error(`Valid package ids: ${[...ids].join(", ")}`);
179
+ return false;
156
180
  }
181
+ return true;
157
182
  }
158
183
 
159
184
  function matchesSelector(pkg, selectors) {
@@ -162,11 +187,11 @@ function matchesSelector(pkg, selectors) {
162
187
 
163
188
  function resolveSelection(flags) {
164
189
  if (flags.only) {
165
- validateSelectors(flags.only, "--only");
190
+ if (!validateSelectors(flags.only, "--only")) return null;
166
191
  return new Set(PACKAGES.filter((p) => matchesSelector(p, flags.only)).map((p) => p.id));
167
192
  }
168
193
  if (flags.except) {
169
- validateSelectors(flags.except, "--except");
194
+ if (!validateSelectors(flags.except, "--except")) return null;
170
195
  return new Set(PACKAGES.filter((p) => !matchesSelector(p, flags.except)).map((p) => p.id));
171
196
  }
172
197
  return new Set(PACKAGES.filter((p) => p.category === "core").map((p) => p.id));
@@ -206,8 +231,8 @@ ${bold("Default behaviour:")}
206
231
  ${bold("Categories:")}
207
232
  core the harness control plane: sub-agents, workflows, ask gate, skill visibility, $ mention, goal, side chat, context budget, simplify, web research, fff search, ponytail discipline
208
233
  optional only when selected (--only optional, picker, or the everything flow): lsp diagnostics, interactive-shell overlays, autoresearch loops, todo tracking, markdown memory
209
- Non-catalog extras (pi install npm:<source>): skill-args, mcp,
210
- ralph-wiggum, curated-themes (65 dark themes)
234
+ (Dropped packages such as skill-args, mcp, ralph-wiggum, curated-themes are
235
+ outside the catalog — pi install npm:<source> still works for them.)
211
236
 
212
237
  ${bold("Examples:")}
213
238
  npx @tommy-ca/lazypi # core catalog (interactive picker on a TTY)
@@ -224,18 +249,13 @@ ${bold("Examples:")}
224
249
  // Pi / settings plumbing
225
250
  // ---------------------------------------------------------------------------
226
251
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
227
- // On Windows, package-manager CLIs and global Node bins are usually `.cmd`
228
- // shims. Node's child_process docs note that those need to be launched via a
229
- // shell, so we route spawned commands through the platform shell there while
230
- // keeping direct execution on Unix.
231
- export function buildSpawnOptions(options = {}, platformName = platform()) {
232
- const resolved = { ...options };
233
- if (platformName === "win32" && resolved.shell == null) resolved.shell = true;
234
- return resolved;
252
+ export function buildSpawnOptions(options = {}, _platformName = platform()) {
253
+ return { ...options };
235
254
  }
236
255
 
237
256
  function spawnCommand(command, args = [], options = {}) {
238
- return spawnSync(command, args, buildSpawnOptions(options));
257
+ const resolved = command === "pi" || command === "npm" ? commandPath(command) ?? command : command;
258
+ return spawnSync(resolved, args, buildSpawnOptions(options));
239
259
  }
240
260
 
241
261
  function hasCmd(name) {
@@ -296,11 +316,19 @@ function writeSettings(local, mutate) {
296
316
  return { ok: true, path: current.path, backup, changed: true };
297
317
  }
298
318
 
299
- function writeSubagentOverrides(local) {
319
+ export function writeSubagentOverrides(local) {
300
320
  return writeSettings(local, (settings) => {
301
- const overrides = {};
302
- for (const name of SUBAGENT_BUILTIN_MODELS) overrides[name] = { model: "" };
303
- settings.subagents = { ...(settings.subagents ?? {}), agentOverrides: { ...(settings.subagents?.agentOverrides ?? {}), ...overrides } };
321
+ const existing = settings.subagents?.agentOverrides ?? {};
322
+ const overrides = { ...existing };
323
+ let changed = false;
324
+ for (const name of SUBAGENT_BUILTIN_MODELS) {
325
+ const current = overrides[name];
326
+ if (current && typeof current === "object" && !Array.isArray(current) && current.model === "") continue;
327
+ overrides[name] = { ...(current && typeof current === "object" && !Array.isArray(current) ? current : {}), model: "" };
328
+ changed = true;
329
+ }
330
+ if (!changed) return false;
331
+ settings.subagents = { ...(settings.subagents ?? {}), agentOverrides: overrides };
304
332
  return true;
305
333
  });
306
334
  }
@@ -325,12 +353,16 @@ function readInstalledSources(local) {
325
353
 
326
354
  function commandPath(name) {
327
355
  const command = platform() === "win32" ? "where" : "which";
328
- const probe = spawnCommand(command, [name], { encoding: "utf8" });
356
+ const probe = spawnSync(command, [name], buildSpawnOptions({ encoding: "utf8" }));
329
357
  if (probe.status !== 0) return null;
330
358
  const first = String(probe.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).find(Boolean);
331
359
  return first || null;
332
360
  }
333
361
 
362
+ function printSettingsParseError(current) {
363
+ console.error(red(`Could not parse ${current.path} — ${current.error}`));
364
+ }
365
+
334
366
  function legacySourcesForPackage(pkg) {
335
367
  return Array.isArray(pkg.legacySources) ? pkg.legacySources : [];
336
368
  }
@@ -499,6 +531,11 @@ async function ensurePi(flags) {
499
531
  if (hasCmd("pi")) return true;
500
532
 
501
533
  log.warn("Could not find the `pi` command on PATH.");
534
+ if (!flags.yes && !isInteractive()) {
535
+ log.error("Install Pi first (`npm install -g @earendil-works/pi-coding-agent`), then re-run `npx @tommy-ca/lazypi`.");
536
+ return false;
537
+ }
538
+
502
539
  const ok = flags.yes || (await confirm("Install Pi now with `npm install -g @earendil-works/pi-coding-agent`?", true));
503
540
  if (!ok) {
504
541
  log.error("Install Pi first, then re-run `npx @tommy-ca/lazypi`.");
@@ -524,6 +561,7 @@ async function ensurePi(flags) {
524
561
  // ---------------------------------------------------------------------------
525
562
  async function cmdInstall(flags) {
526
563
  let selectedIds = resolveSelection(flags);
564
+ if (!selectedIds) return 2;
527
565
 
528
566
  const usedSelectionFlag = Boolean(flags.only || flags.except);
529
567
  const interactive = !flags.yes && !usedSelectionFlag && isInteractive();
@@ -532,6 +570,18 @@ async function cmdInstall(flags) {
532
570
  console.log(renderLogo());
533
571
  intro(bold("LazyPi"));
534
572
  }
573
+
574
+ const settings = readSettings(flags.local);
575
+ if (settings.error) {
576
+ if (interactive) {
577
+ log.error(`Could not parse ${settings.path} — ${settings.error}`);
578
+ outro(red("Aborted."));
579
+ } else {
580
+ printSettingsParseError(settings);
581
+ }
582
+ return 2;
583
+ }
584
+
535
585
  if (!(await ensurePi(flags))) return 127;
536
586
 
537
587
  if (interactive) {
@@ -550,8 +600,7 @@ async function cmdInstall(flags) {
550
600
  return 0;
551
601
  }
552
602
 
553
- const { sources: installedSources, error: settingsError } = readInstalledSources(flags.local);
554
- if (settingsError) log.warn(`Could not parse ${settingsPath(flags.local)} — ${settingsError}`);
603
+ const { sources: installedSources } = readInstalledSources(flags.local);
555
604
 
556
605
  const toInstall = selected.filter((pkg) => {
557
606
  return !isPackageInstalled(pkg, installedSources);
@@ -706,7 +755,7 @@ function cmdStatus(flags) {
706
755
  console.log(yellow(" (not found — Pi has not written settings yet)"));
707
756
  } else if (error) {
708
757
  console.error(red(` could not parse: ${error}`));
709
- return 1;
758
+ return 2;
710
759
  }
711
760
 
712
761
  const installed = PACKAGES.filter((pkg) => packageInstallStatus(pkg, sources).installed);
@@ -719,7 +768,8 @@ function cmdStatus(flags) {
719
768
  printHeader(`Installed from LazyPi catalog (${installed.length}/${PACKAGES.length}):`);
720
769
  if (installed.length === 0) console.log(dim(" none"));
721
770
  for (const pkg of installed) {
722
- console.log(` ${green("✓")} [${pkg.category}] ${pkg.id.padEnd(20)} ${dim(pkg.source)}`);
771
+ const installedSource = [...sources].find((src) => sourcesMatch(pkg.source, src)) ?? pkg.source;
772
+ console.log(` ${green("✓")} [${pkg.category}] ${pkg.id.padEnd(20)} ${dim(installedSource)}`);
723
773
  }
724
774
 
725
775
  printHeader(`Installed with legacy catalog sources (${legacy.length}):`);
@@ -746,14 +796,14 @@ function cmdStatus(flags) {
746
796
  // update
747
797
  // ---------------------------------------------------------------------------
748
798
  async function cmdUpdate(flags) {
749
- if (!(await ensurePi(flags))) return 127;
750
-
751
799
  const settings = readSettings(flags.local);
752
800
  if (settings.error) {
753
- console.error(red(`Could not parse ${settingsPath(flags.local)} — ${settings.error}`));
754
- return 1;
801
+ printSettingsParseError(settings);
802
+ return 2;
755
803
  }
756
804
 
805
+ if (!(await ensurePi(flags))) return 127;
806
+
757
807
  console.log(bold("pi update"));
758
808
 
759
809
  return spawnCommand("pi", flags.local ? ["update", "--extensions"] : ["update"], { stdio: "inherit" }).status ?? 1;
@@ -763,6 +813,12 @@ async function cmdUpdate(flags) {
763
813
  // doctor
764
814
  // ---------------------------------------------------------------------------
765
815
  function cmdDoctor(flags) {
816
+ const settings = readSettings(flags.local);
817
+ if (settings.error) {
818
+ printSettingsParseError(settings);
819
+ return 2;
820
+ }
821
+
766
822
  let problems = 0;
767
823
  let warnings = 0;
768
824
  const pass = (msg) => console.log(` ${green("✓")} ${msg}`);
@@ -831,12 +887,18 @@ function cmdDoctor(flags) {
831
887
  // remove
832
888
  // ---------------------------------------------------------------------------
833
889
  async function cmdRemove(flags, targets) {
890
+ const installed = readInstalledSources(flags.local);
891
+ if (installed.error) {
892
+ printSettingsParseError(installed);
893
+ return 2;
894
+ }
895
+
834
896
  if (targets.length === 0) {
835
897
  if (!isInteractive()) {
836
898
  console.error(red("Usage: npx @tommy-ca/lazypi remove <id|source> [...]"));
837
899
  return 2;
838
900
  }
839
- const { sources } = readInstalledSources(flags.local);
901
+ const { sources } = installed;
840
902
  const installedPkgs = PACKAGES.filter((p) => isPackagePresent(p, sources));
841
903
  if (installedPkgs.length === 0) {
842
904
  console.log(yellow("No catalog packages are installed."));
@@ -860,10 +922,9 @@ async function cmdRemove(flags, targets) {
860
922
  targets = picked;
861
923
  }
862
924
 
863
- const { sources: installedSources } = readInstalledSources(flags.local);
925
+ const { sources: installedSources } = installed;
864
926
  let exitCode = 0;
865
927
  for (const target of targets) {
866
- // Resolve a catalog id to its source string, or pass through raw sources
867
928
  const pkg = PACKAGES.find((p) => p.id === target);
868
929
  const source = pkg ? pkg.source : target;
869
930
 
@@ -896,6 +957,11 @@ async function main() {
896
957
  console.log(`@tommy-ca/lazypi ${VERSION}`);
897
958
  return 0;
898
959
  }
960
+ if (flags.usageError) {
961
+ console.error(red(flags.usageError));
962
+ printHelp();
963
+ return 2;
964
+ }
899
965
  if (flags.help) {
900
966
  printHelp();
901
967
  return flags.badArg ? 2 : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tommy-ca/lazypi",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Opinionated one-shot installer for a full-featured Pi coding agent setup.",
5
5
  "license": "MIT",
6
6
  "repository": {