@tommy-ca/lazypi 0.10.1 → 0.10.2

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 (2) hide show
  1. package/bin/lazypi.mjs +91 -31
  2. package/package.json +1 -1
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
 
@@ -152,8 +176,9 @@ function validateSelectors(list, label) {
152
176
  console.error(red(`Unknown ${label}: ${bad.join(", ")}`));
153
177
  console.error(`Valid categories: ${CATEGORIES.join(", ")}`);
154
178
  console.error(`Valid package ids: ${[...ids].join(", ")}`);
155
- exit(2);
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));
@@ -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
  }
@@ -524,6 +556,7 @@ async function ensurePi(flags) {
524
556
  // ---------------------------------------------------------------------------
525
557
  async function cmdInstall(flags) {
526
558
  let selectedIds = resolveSelection(flags);
559
+ if (!selectedIds) return 2;
527
560
 
528
561
  const usedSelectionFlag = Boolean(flags.only || flags.except);
529
562
  const interactive = !flags.yes && !usedSelectionFlag && isInteractive();
@@ -532,6 +565,18 @@ async function cmdInstall(flags) {
532
565
  console.log(renderLogo());
533
566
  intro(bold("LazyPi"));
534
567
  }
568
+
569
+ const settings = readSettings(flags.local);
570
+ if (settings.error) {
571
+ if (interactive) {
572
+ log.error(`Could not parse ${settings.path} — ${settings.error}`);
573
+ outro(red("Aborted."));
574
+ } else {
575
+ printSettingsParseError(settings);
576
+ }
577
+ return 2;
578
+ }
579
+
535
580
  if (!(await ensurePi(flags))) return 127;
536
581
 
537
582
  if (interactive) {
@@ -550,8 +595,7 @@ async function cmdInstall(flags) {
550
595
  return 0;
551
596
  }
552
597
 
553
- const { sources: installedSources, error: settingsError } = readInstalledSources(flags.local);
554
- if (settingsError) log.warn(`Could not parse ${settingsPath(flags.local)} — ${settingsError}`);
598
+ const { sources: installedSources } = readInstalledSources(flags.local);
555
599
 
556
600
  const toInstall = selected.filter((pkg) => {
557
601
  return !isPackageInstalled(pkg, installedSources);
@@ -706,7 +750,7 @@ function cmdStatus(flags) {
706
750
  console.log(yellow(" (not found — Pi has not written settings yet)"));
707
751
  } else if (error) {
708
752
  console.error(red(` could not parse: ${error}`));
709
- return 1;
753
+ return 2;
710
754
  }
711
755
 
712
756
  const installed = PACKAGES.filter((pkg) => packageInstallStatus(pkg, sources).installed);
@@ -746,14 +790,14 @@ function cmdStatus(flags) {
746
790
  // update
747
791
  // ---------------------------------------------------------------------------
748
792
  async function cmdUpdate(flags) {
749
- if (!(await ensurePi(flags))) return 127;
750
-
751
793
  const settings = readSettings(flags.local);
752
794
  if (settings.error) {
753
- console.error(red(`Could not parse ${settingsPath(flags.local)} — ${settings.error}`));
754
- return 1;
795
+ printSettingsParseError(settings);
796
+ return 2;
755
797
  }
756
798
 
799
+ if (!(await ensurePi(flags))) return 127;
800
+
757
801
  console.log(bold("pi update"));
758
802
 
759
803
  return spawnCommand("pi", flags.local ? ["update", "--extensions"] : ["update"], { stdio: "inherit" }).status ?? 1;
@@ -763,6 +807,12 @@ async function cmdUpdate(flags) {
763
807
  // doctor
764
808
  // ---------------------------------------------------------------------------
765
809
  function cmdDoctor(flags) {
810
+ const settings = readSettings(flags.local);
811
+ if (settings.error) {
812
+ printSettingsParseError(settings);
813
+ return 2;
814
+ }
815
+
766
816
  let problems = 0;
767
817
  let warnings = 0;
768
818
  const pass = (msg) => console.log(` ${green("✓")} ${msg}`);
@@ -831,12 +881,18 @@ function cmdDoctor(flags) {
831
881
  // remove
832
882
  // ---------------------------------------------------------------------------
833
883
  async function cmdRemove(flags, targets) {
884
+ const installed = readInstalledSources(flags.local);
885
+ if (installed.error) {
886
+ printSettingsParseError(installed);
887
+ return 2;
888
+ }
889
+
834
890
  if (targets.length === 0) {
835
891
  if (!isInteractive()) {
836
892
  console.error(red("Usage: npx @tommy-ca/lazypi remove <id|source> [...]"));
837
893
  return 2;
838
894
  }
839
- const { sources } = readInstalledSources(flags.local);
895
+ const { sources } = installed;
840
896
  const installedPkgs = PACKAGES.filter((p) => isPackagePresent(p, sources));
841
897
  if (installedPkgs.length === 0) {
842
898
  console.log(yellow("No catalog packages are installed."));
@@ -860,10 +916,9 @@ async function cmdRemove(flags, targets) {
860
916
  targets = picked;
861
917
  }
862
918
 
863
- const { sources: installedSources } = readInstalledSources(flags.local);
919
+ const { sources: installedSources } = installed;
864
920
  let exitCode = 0;
865
921
  for (const target of targets) {
866
- // Resolve a catalog id to its source string, or pass through raw sources
867
922
  const pkg = PACKAGES.find((p) => p.id === target);
868
923
  const source = pkg ? pkg.source : target;
869
924
 
@@ -896,6 +951,11 @@ async function main() {
896
951
  console.log(`@tommy-ca/lazypi ${VERSION}`);
897
952
  return 0;
898
953
  }
954
+ if (flags.usageError) {
955
+ console.error(red(flags.usageError));
956
+ printHelp();
957
+ return 2;
958
+ }
899
959
  if (flags.help) {
900
960
  printHelp();
901
961
  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.2",
4
4
  "description": "Opinionated one-shot installer for a full-featured Pi coding agent setup.",
5
5
  "license": "MIT",
6
6
  "repository": {