@oh-my-pi/pi-utils 16.5.1 → 16.5.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.5.2] - 2026-07-14
6
+
7
+ ### Fixed
8
+
9
+ - Improved CLI argument and flag validation error output to display a concise error message and command usage instead of a minified code frame.
10
+ - Corrected required variadic positionals to render as `MODELS...` instead of `[MODELS]` in usage help.
11
+
5
12
  ## [16.5.1] - 2026-07-14
6
13
 
7
14
  ### Added
@@ -1,3 +1,13 @@
1
+ /**
2
+ * A user-facing argument/flag validation failure. Thrown by {@link Command.parse}
3
+ * for missing/invalid positionals and flags. The top-level {@link run} handler
4
+ * prints its message plus the command usage line to stderr and exits 1, instead
5
+ * of letting it bubble to the process-level catch — which would dump a minified
6
+ * `dist/cli.js` code frame over a plain argument mistake (issue #5369).
7
+ */
8
+ export declare class CliUsageError extends Error {
9
+ constructor(message: string);
10
+ }
1
11
  export interface FlagDescriptor<K extends "string" | "boolean" | "integer" = "string" | "boolean" | "integer"> {
2
12
  kind: K;
3
13
  description?: string;
@@ -91,6 +101,8 @@ export declare abstract class Command {
91
101
  }
92
102
  /** Render full root help: header, default command details, subcommand list. */
93
103
  export declare function renderRootHelp(config: CliConfig): void;
104
+ /** Build the single USAGE line for a command (without the leading label). */
105
+ export declare function commandUsageLine(bin: string, id: string, Cmd: CommandCtor): string;
94
106
  /** Render help for a single command. */
95
107
  export declare function renderCommandHelp(bin: string, id: string, Cmd: CommandCtor): void;
96
108
  /** A lazily-loaded command: canonical name, loader, and optional aliases. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "16.5.1",
4
+ "version": "16.5.2",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "16.5.1",
34
+ "@oh-my-pi/pi-natives": "16.5.2",
35
35
  "handlebars": "^4.7.9",
36
36
  "winston": "^3.19.0",
37
37
  "winston-daily-rotate-file": "^5.0.0"
package/src/cli.ts CHANGED
@@ -28,6 +28,20 @@ function startupMarker(text: string): void {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * A user-facing argument/flag validation failure. Thrown by {@link Command.parse}
33
+ * for missing/invalid positionals and flags. The top-level {@link run} handler
34
+ * prints its message plus the command usage line to stderr and exits 1, instead
35
+ * of letting it bubble to the process-level catch — which would dump a minified
36
+ * `dist/cli.js` code frame over a plain argument mistake (issue #5369).
37
+ */
38
+ export class CliUsageError extends Error {
39
+ constructor(message: string) {
40
+ super(message);
41
+ this.name = "CliUsageError";
42
+ }
43
+ }
44
+
31
45
  // ---------------------------------------------------------------------------
32
46
  // Flag & Arg descriptors
33
47
  // ---------------------------------------------------------------------------
@@ -190,12 +204,18 @@ export abstract class Command {
190
204
 
191
205
  // strict=false when command declares args (positionals must pass through)
192
206
  // or when the command itself opts out
193
- const { values: rawValues, positionals } = nodeParseArgs({
194
- args: this.argv,
195
- options,
196
- allowPositionals: true,
197
- strict,
198
- });
207
+ const { values: rawValues, positionals } = (() => {
208
+ try {
209
+ return nodeParseArgs({
210
+ args: this.argv,
211
+ options,
212
+ allowPositionals: true,
213
+ strict,
214
+ });
215
+ } catch (error) {
216
+ throw new CliUsageError(error instanceof Error ? error.message : String(error));
217
+ }
218
+ })();
199
219
 
200
220
  // Convert raw values to proper types and validate
201
221
  const flags: Record<string, unknown> = {};
@@ -207,7 +227,7 @@ export abstract class Command {
207
227
  } else {
208
228
  const n = Number.parseInt(raw as string, 10);
209
229
  if (Number.isNaN(n)) {
210
- throw new Error(`Expected integer for --${name}, got "${raw}"`);
230
+ throw new CliUsageError(`Expected integer for --${name}, got "${raw}"`);
211
231
  }
212
232
  flags[name] = n;
213
233
  }
@@ -220,14 +240,16 @@ export abstract class Command {
220
240
  // Validate options constraint
221
241
  if (val !== undefined && desc.options && !Array.isArray(val)) {
222
242
  if (!desc.options.includes(val as string)) {
223
- throw new Error(`Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`);
243
+ throw new CliUsageError(
244
+ `Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`,
245
+ );
224
246
  }
225
247
  }
226
248
  flags[name] = val;
227
249
  }
228
250
  // Validate required
229
251
  if (desc.required && flags[name] === undefined) {
230
- throw new Error(`Missing required flag: --${name}`);
252
+ throw new CliUsageError(`Missing required flag: --${name}`);
231
253
  }
232
254
  }
233
255
 
@@ -246,13 +268,15 @@ export abstract class Command {
246
268
  }
247
269
  // Validate required
248
270
  if (desc.required && args[argName] === undefined) {
249
- throw new Error(`Missing required argument: ${argName}`);
271
+ throw new CliUsageError(`Missing required argument: ${argName}`);
250
272
  }
251
273
  // Validate options constraint
252
274
  const argVal = args[argName];
253
275
  if (argVal !== undefined && desc.options && typeof argVal === "string") {
254
276
  if (!desc.options.includes(argVal)) {
255
- throw new Error(`Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`);
277
+ throw new CliUsageError(
278
+ `Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`,
279
+ );
256
280
  }
257
281
  }
258
282
  }
@@ -294,15 +318,34 @@ export function renderRootHelp(config: CliConfig): void {
294
318
  process.stdout.write(lines.join("\n"));
295
319
  }
296
320
 
321
+ /**
322
+ * Format a command's positional args for a USAGE line. Required args render
323
+ * bare (`MODELS`), optional args wrapped in brackets (`[MODELS]`), and
324
+ * `multiple` args get a trailing ellipsis (`MODELS...`) so a required
325
+ * variadic reads as `MODELS...`, not the misleading optional `[MODELS]`.
326
+ */
327
+ function formatUsageArgs(Cmd: CommandCtor): string {
328
+ const entries = Object.entries(Cmd.args ?? {});
329
+ if (entries.length === 0) return "";
330
+ const parts = entries.map(([name, desc]) => {
331
+ const label = `${name.toUpperCase()}${desc.multiple ? "..." : ""}`;
332
+ return desc.required ? label : `[${label}]`;
333
+ });
334
+ return ` ${parts.join(" ")}`;
335
+ }
336
+
337
+ /** Build the single USAGE line for a command (without the leading label). */
338
+ export function commandUsageLine(bin: string, id: string, Cmd: CommandCtor): string {
339
+ const hasFlags = Object.keys(Cmd.flags ?? {}).length > 0;
340
+ return `$ ${bin} ${id}${formatUsageArgs(Cmd)}${hasFlags ? " [FLAGS]" : ""}`;
341
+ }
342
+
297
343
  /** Render help for a single command. */
298
344
  export function renderCommandHelp(bin: string, id: string, Cmd: CommandCtor): void {
299
345
  const lines: string[] = [];
300
346
  if (Cmd.description) lines.push(`${Cmd.description}\n`);
301
347
  lines.push("USAGE");
302
- const argNames = Object.keys(Cmd.args ?? {});
303
- const argStr = argNames.length > 0 ? ` ${argNames.map(n => `[${n.toUpperCase()}]`).join(" ")}` : "";
304
- const hasFlags = Object.keys(Cmd.flags ?? {}).length > 0;
305
- lines.push(` $ ${bin} ${id}${argStr}${hasFlags ? " [FLAGS]" : ""}\n`);
348
+ lines.push(` ${commandUsageLine(bin, id, Cmd)}\n`);
306
349
  renderCommandBody(lines, Cmd);
307
350
  process.stdout.write(lines.join("\n"));
308
351
  }
@@ -435,7 +478,22 @@ export async function run(opts: RunOptions): Promise<void> {
435
478
  const Cmd = await loadEntry(entry);
436
479
  const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
437
480
  const instance = new Cmd(commandArgv, config);
438
- await instance.run();
481
+ try {
482
+ await instance.run();
483
+ } catch (error) {
484
+ // A usage mistake (missing/invalid arg or flag) is not a crash: print the
485
+ // message and the command's usage line, then exit 1. Letting it reach the
486
+ // process-level catch would dump a minified `dist/cli.js` code frame over a
487
+ // plain argument error (issue #5369).
488
+ if (error instanceof CliUsageError) {
489
+ process.stderr.write(`error: ${error.message}\n\n`);
490
+ process.stderr.write(`USAGE\n ${commandUsageLine(bin, entry.name, Cmd)}\n`);
491
+ process.stderr.write(`\nRun \`${bin} ${entry.name} --help\` for details.\n`);
492
+ process.exitCode = 1;
493
+ return;
494
+ }
495
+ throw error;
496
+ }
439
497
  }
440
498
 
441
499
  /** Load one command module, leaving streaming markers around the import. */