@gajae-code/utils 0.8.2 → 0.9.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.
@@ -39,6 +39,14 @@ export declare const Flags: {
39
39
  export declare const Args: {
40
40
  string<T extends ArgInput>(opts?: T): ArgDescriptor & T;
41
41
  };
42
+ /**
43
+ * Thrown when CLI argument/flag parsing or validation fails (unknown flag,
44
+ * bad option value, missing required arg, etc.). `run()` catches this to print
45
+ * the message and render usage instead of crashing as an uncaught exception.
46
+ */
47
+ export declare class CliParseError extends Error {
48
+ constructor(message: string);
49
+ }
42
50
  type FlagValue<D extends FlagDescriptor> = D["kind"] extends "boolean" ? D extends {
43
51
  default: boolean;
44
52
  } ? boolean : boolean | undefined : D["kind"] extends "integer" ? D extends {
@@ -1,9 +1,12 @@
1
1
  export declare const MIN_TAB_WIDTH = 1;
2
2
  export declare const MAX_TAB_WIDTH = 16;
3
3
  export declare const DEFAULT_TAB_WIDTH = 3;
4
+ type TabWidthChangeListener = (width: number) => void;
5
+ export declare function onDefaultTabWidthChange(listener: TabWidthChangeListener): () => void;
4
6
  export declare function getDefaultTabWidth(): number;
5
7
  export declare function setDefaultTabWidth(width: number): void;
6
8
  /**
7
9
  * Visible tab width in columns for `file` (from `.editorconfig` + default), or the default when `file` is omitted.
8
10
  */
9
11
  export declare function getIndentation(file?: string | null, projectDir?: string | null): number;
12
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/utils",
4
- "version": "0.8.2",
4
+ "version": "0.9.1",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@gajae-code/natives": "0.8.2",
34
+ "@gajae-code/natives": "0.9.1",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
package/src/cli.ts CHANGED
@@ -68,6 +68,18 @@ export const Args = {
68
68
  },
69
69
  };
70
70
 
71
+ /**
72
+ * Thrown when CLI argument/flag parsing or validation fails (unknown flag,
73
+ * bad option value, missing required arg, etc.). `run()` catches this to print
74
+ * the message and render usage instead of crashing as an uncaught exception.
75
+ */
76
+ export class CliParseError extends Error {
77
+ constructor(message: string) {
78
+ super(message);
79
+ this.name = "CliParseError";
80
+ }
81
+ }
82
+
71
83
  // ---------------------------------------------------------------------------
72
84
  // Parse result types — mirrors oclif's typed output from this.parse()
73
85
  // ---------------------------------------------------------------------------
@@ -174,12 +186,22 @@ export abstract class Command {
174
186
 
175
187
  // strict=false when command declares args (positionals must pass through)
176
188
  // or when the command itself opts out
177
- const { values: rawValues, positionals } = nodeParseArgs({
178
- args: this.argv,
179
- options,
180
- allowPositionals: true,
181
- strict,
182
- });
189
+ let rawValues: Record<string, string | boolean | Array<string | boolean> | undefined>;
190
+ let positionals: string[];
191
+ try {
192
+ const parsed = nodeParseArgs({
193
+ args: this.argv,
194
+ options,
195
+ allowPositionals: true,
196
+ strict,
197
+ });
198
+ rawValues = parsed.values;
199
+ positionals = parsed.positionals;
200
+ } catch (err) {
201
+ // node:util parseArgs throws on unknown flags / malformed input — surface
202
+ // it as a CliParseError so run() renders usage instead of crashing.
203
+ throw new CliParseError(err instanceof Error ? err.message : String(err));
204
+ }
183
205
 
184
206
  // Convert raw values to proper types and validate
185
207
  const flags: Record<string, unknown> = {};
@@ -191,7 +213,7 @@ export abstract class Command {
191
213
  } else {
192
214
  const n = Number.parseInt(raw as string, 10);
193
215
  if (Number.isNaN(n)) {
194
- throw new Error(`Expected integer for --${name}, got "${raw}"`);
216
+ throw new CliParseError(`Expected integer for --${name}, got "${raw}"`);
195
217
  }
196
218
  flags[name] = n;
197
219
  }
@@ -204,14 +226,16 @@ export abstract class Command {
204
226
  // Validate options constraint
205
227
  if (val !== undefined && desc.options && !Array.isArray(val)) {
206
228
  if (!desc.options.includes(val as string)) {
207
- throw new Error(`Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`);
229
+ throw new CliParseError(
230
+ `Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`,
231
+ );
208
232
  }
209
233
  }
210
234
  flags[name] = val;
211
235
  }
212
236
  // Validate required
213
237
  if (desc.required && flags[name] === undefined) {
214
- throw new Error(`Missing required flag: --${name}`);
238
+ throw new CliParseError(`Missing required flag: --${name}`);
215
239
  }
216
240
  }
217
241
 
@@ -230,13 +254,15 @@ export abstract class Command {
230
254
  }
231
255
  // Validate required
232
256
  if (desc.required && args[argName] === undefined) {
233
- throw new Error(`Missing required argument: ${argName}`);
257
+ throw new CliParseError(`Missing required argument: ${argName}`);
234
258
  }
235
259
  // Validate options constraint
236
260
  const argVal = args[argName];
237
261
  if (argVal !== undefined && desc.options && typeof argVal === "string") {
238
262
  if (!desc.options.includes(argVal)) {
239
- throw new Error(`Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`);
263
+ throw new CliParseError(
264
+ `Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`,
265
+ );
240
266
  }
241
267
  }
242
268
  }
@@ -407,7 +433,7 @@ export async function run(opts: RunOptions): Promise<void> {
407
433
  const instance = new Cmd(commandArgv, config);
408
434
  await instance.run();
409
435
  } else {
410
- const config = await loadAllCommands(opts);
436
+ const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
411
437
  renderCommandHelp(bin, entry.name, config.commands.get(entry.name) ?? Cmd);
412
438
  }
413
439
  return;
@@ -425,7 +451,19 @@ export async function run(opts: RunOptions): Promise<void> {
425
451
  const Cmd = await entry.load();
426
452
  const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
427
453
  const instance = new Cmd(commandArgv, config);
428
- await instance.run();
454
+ try {
455
+ await instance.run();
456
+ } catch (err) {
457
+ if (err instanceof CliParseError) {
458
+ // Invalid args/flags for a real command: print the problem + usage and
459
+ // exit with a usage error, instead of crashing as an uncaught exception.
460
+ process.stderr.write(`${err.message}\n\n`);
461
+ renderCommandHelp(bin, entry.name, Cmd);
462
+ process.exitCode = 2;
463
+ return;
464
+ }
465
+ throw err;
466
+ }
429
467
  }
430
468
 
431
469
  /** Resolve all command loaders for help/alias display. */
@@ -13,6 +13,9 @@ export const DEFAULT_TAB_WIDTH = 3;
13
13
  const EDITORCONFIG_NAME = ".editorconfig";
14
14
 
15
15
  let defaultTabWidth = DEFAULT_TAB_WIDTH;
16
+ type TabWidthChangeListener = (width: number) => void;
17
+
18
+ const tabWidthChangeListeners = new Set<TabWidthChangeListener>();
16
19
 
17
20
  const editorConfigCache = new Map<string, ParsedEditorConfig>();
18
21
  const editorConfigChainCache = new Map<string, ChainEntry[]>();
@@ -278,13 +281,25 @@ function resolveEditorConfigTabWidth(match: EditorConfigMatch | undefined, fallb
278
281
 
279
282
  return undefined;
280
283
  }
284
+ export function onDefaultTabWidthChange(listener: TabWidthChangeListener): () => void {
285
+ tabWidthChangeListeners.add(listener);
286
+ return () => {
287
+ tabWidthChangeListeners.delete(listener);
288
+ };
289
+ }
281
290
 
282
291
  export function getDefaultTabWidth(): number {
283
292
  return defaultTabWidth;
284
293
  }
285
294
 
286
295
  export function setDefaultTabWidth(width: number): void {
287
- defaultTabWidth = clampTabWidth(width);
296
+ const next = clampTabWidth(width);
297
+ if (defaultTabWidth === next) return;
298
+ defaultTabWidth = next;
299
+ indentationCache.clear();
300
+ for (const listener of tabWidthChangeListeners) {
301
+ listener(next);
302
+ }
288
303
  }
289
304
 
290
305
  /**