@tommy-ca/lazypi 0.10.3 → 0.10.4

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 +51 -4
  2. package/package.json +1 -1
package/bin/lazypi.mjs CHANGED
@@ -185,7 +185,7 @@ function matchesSelector(pkg, selectors) {
185
185
  return selectors.some((name) => name === pkg.category || name === pkg.id);
186
186
  }
187
187
 
188
- function resolveSelection(flags) {
188
+ export function resolveSelection(flags) {
189
189
  if (flags.only) {
190
190
  if (!validateSelectors(flags.only, "--only")) return null;
191
191
  return new Set(PACKAGES.filter((p) => matchesSelector(p, flags.only)).map((p) => p.id));
@@ -253,9 +253,47 @@ export function buildSpawnOptions(options = {}, _platformName = platform()) {
253
253
  return { ...options };
254
254
  }
255
255
 
256
+ function isWindowsCmdOrBat(resolved, platformName = platform()) {
257
+ return platformName === "win32" && /\.(cmd|bat)$/i.test(String(resolved));
258
+ }
259
+
260
+ const CMD_QUOTE_RE = /[\s&|<>^%!"()]/;
261
+
262
+ export function quoteCmdArg(value, { always = false } = {}) {
263
+ const text = String(value);
264
+ if (!always && text !== "" && !CMD_QUOTE_RE.test(text)) return text;
265
+ return `"${text.replaceAll("%", "%%").replaceAll('"', '""')}"`;
266
+ }
267
+
268
+ export function windowsComSpec(env = process.env) {
269
+ const comspec = env.ComSpec;
270
+ if (comspec && /(^|[\\/])cmd\.exe$/i.test(comspec)) return comspec;
271
+ const root = env.SystemRoot || "C:\\Windows";
272
+ return `${root}\\System32\\cmd.exe`;
273
+ }
274
+
275
+ export function pickWindowsWhereHit(stdout) {
276
+ const lines = String(stdout ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
277
+ return lines.find((line) => /\.(cmd|bat|exe|com)$/i.test(line)) ?? lines[0] ?? null;
278
+ }
279
+
280
+ export function windowsSpawnArgv(resolved, args = [], platformName = platform()) {
281
+ if (isWindowsCmdOrBat(resolved, platformName)) {
282
+ return {
283
+ command: windowsComSpec(),
284
+ args: ["/d", "/s", "/c", "call", quoteCmdArg(resolved, { always: true }), ...args.map((arg) => quoteCmdArg(arg))],
285
+ };
286
+ }
287
+ return { command: resolved, args: [...args] };
288
+ }
289
+
256
290
  function spawnCommand(command, args = [], options = {}) {
257
291
  const resolved = command === "pi" || command === "npm" ? commandPath(command) ?? command : command;
258
- return spawnSync(resolved, args, buildSpawnOptions(options));
292
+ const spawned = windowsSpawnArgv(resolved, args);
293
+ const spawnOptions = isWindowsCmdOrBat(resolved)
294
+ ? { ...buildSpawnOptions(options), shell: false, windowsVerbatimArguments: true }
295
+ : buildSpawnOptions(options);
296
+ return spawnSync(spawned.command, spawned.args, spawnOptions);
259
297
  }
260
298
 
261
299
  function hasCmd(name) {
@@ -355,6 +393,7 @@ function commandPath(name) {
355
393
  const command = platform() === "win32" ? "where" : "which";
356
394
  const probe = spawnSync(command, [name], buildSpawnOptions({ encoding: "utf8" }));
357
395
  if (probe.status !== 0) return null;
396
+ if (platform() === "win32") return pickWindowsWhereHit(probe.stdout);
358
397
  const first = String(probe.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).find(Boolean);
359
398
  return first || null;
360
399
  }
@@ -766,6 +805,11 @@ function cmdStatus(flags) {
766
805
  );
767
806
 
768
807
  printHeader(`Installed from LazyPi catalog (${installed.length}/${PACKAGES.length}):`);
808
+ const coreTotal = PACKAGES.filter((pkg) => pkg.category === "core").length;
809
+ const optionalTotal = PACKAGES.filter((pkg) => pkg.category === "optional").length;
810
+ const coreInstalled = installed.filter((pkg) => pkg.category === "core").length;
811
+ const optionalInstalled = installed.filter((pkg) => pkg.category === "optional").length;
812
+ console.log(dim(` core ${coreInstalled}/${coreTotal}, optional ${optionalInstalled}/${optionalTotal}`));
769
813
  if (installed.length === 0) console.log(dim(" none"));
770
814
  for (const pkg of installed) {
771
815
  const installedSource = [...sources].find((src) => sourcesMatch(pkg.source, src)) ?? pkg.source;
@@ -822,7 +866,7 @@ function cmdDoctor(flags) {
822
866
  let problems = 0;
823
867
  let warnings = 0;
824
868
  const pass = (msg) => console.log(` ${green("✓")} ${msg}`);
825
- const warn = (msg, { fatal = true } = {}) => {
869
+ const warn = (msg, { fatal = false } = {}) => {
826
870
  console.log(` ${yellow("!")} ${msg}`);
827
871
  if (fatal) problems++;
828
872
  else warnings++;
@@ -860,7 +904,10 @@ function cmdDoctor(flags) {
860
904
  else if (error) fail(`${path} is not valid JSON — ${error}`);
861
905
  else {
862
906
  pass(`${path} is readable`);
863
- const unpinnedGit = [...sources].filter((src) => /^git:github\.com\/[^/@]+\/[^@\s]+$/.test(src));
907
+ const unpinnedGit = [...sources].filter((src) => {
908
+ if (!/^git:github\.com\/[^/@]+\/[^@\s]+$/.test(src)) return false;
909
+ return !PACKAGES.some((pkg) => sourcesMatch(pkg.source, src) || isLegacySourceForPackage(pkg, src));
910
+ });
864
911
  for (const src of unpinnedGit) warn(`${src} is an unpinned git head — pin it to a commit for reproducible installs`, { fatal: false });
865
912
  }
866
913
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tommy-ca/lazypi",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Opinionated one-shot installer for a full-featured Pi coding agent setup.",
5
5
  "license": "MIT",
6
6
  "repository": {