goke 6.1.3 → 6.2.0

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/dist/goke.js CHANGED
@@ -2,7 +2,6 @@
2
2
  * Goke — a cac-inspired CLI framework.
3
3
  *
4
4
  * This file contains the entire core framework:
5
- * - GokeError: custom error class
6
5
  * - Option: CLI option parsing (flags, required/optional values)
7
6
  * - Command / GlobalCommand: command definition, help/version output
8
7
  * - Goke: main CLI class with parsing, matching, and execution
@@ -13,7 +12,7 @@
13
12
  import { EventEmitter } from 'events';
14
13
  import pc from 'picocolors';
15
14
  import mri from "./mri.js";
16
- import { coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js";
15
+ import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js";
17
16
  // ─── Node.js platform constants ───
18
17
  const processArgs = process.argv;
19
18
  const platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
@@ -70,13 +69,8 @@ const maxVisibleLength = (arr) => {
70
69
  };
71
70
  const ANSI_RE = /\x1B\[[0-9;]*m/g;
72
71
  const visibleLength = (value) => value.replace(ANSI_RE, '').length;
73
- const commandOrange = (value) => {
74
- if (!pc.isColorSupported) {
75
- return value;
76
- }
77
- return `\x1b[38;5;208m${value}\x1b[39m`;
78
- };
79
- const optionYellow = (value) => pc.bold(pc.yellowBright(value));
72
+ const commandGreen = (value) => pc.bold(pc.greenBright(value));
73
+ const optionBlue = (value) => pc.bold(pc.blueBright(value));
80
74
  const padRight = (str, length) => {
81
75
  return visibleLength(str) >= length ? str : `${str}${' '.repeat(length - visibleLength(str))}`;
82
76
  };
@@ -176,19 +170,6 @@ const camelcaseOptionName = (name) => {
176
170
  })
177
171
  .join('.');
178
172
  };
179
- // ─── GokeError ───
180
- class GokeError extends Error {
181
- constructor(message) {
182
- super(message);
183
- this.name = this.constructor.name;
184
- if (typeof Error.captureStackTrace === 'function') {
185
- Error.captureStackTrace(this, this.constructor);
186
- }
187
- else {
188
- this.stack = new Error(message).stack;
189
- }
190
- }
191
- }
192
173
  // ─── Option ───
193
174
  class Option {
194
175
  rawName;
@@ -205,6 +186,8 @@ class Option {
205
186
  default;
206
187
  /** Standard JSON Schema V1 schema for type coercion and inference */
207
188
  schema;
189
+ /** Whether this option is deprecated (hidden from help output) */
190
+ deprecated;
208
191
  /**
209
192
  * Create an option.
210
193
  * @param rawName - The raw option string, e.g. '--port <port>', '-v, --verbose'
@@ -223,6 +206,9 @@ class Option {
223
206
  if (meta.default !== undefined) {
224
207
  this.default = meta.default;
225
208
  }
209
+ if (meta.deprecated) {
210
+ this.deprecated = true;
211
+ }
226
212
  }
227
213
  else {
228
214
  this.description = '';
@@ -345,7 +331,11 @@ class Command {
345
331
  return option.names.includes(name);
346
332
  });
347
333
  }
348
- outputHelp() {
334
+ /**
335
+ * Return the formatted help string without printing it.
336
+ * Useful for embedding help text in documentation, tests, or other programmatic uses.
337
+ */
338
+ helpText() {
349
339
  const { name, commands } = this.cli;
350
340
  const { versionNumber, options: globalOptions, helpCallback, } = this.cli.globalCommand;
351
341
  let sections = [
@@ -362,7 +352,8 @@ class Command {
362
352
  if (showCommands) {
363
353
  const commandRows = commands.map((command) => {
364
354
  const displayName = command.rawName.trim() === '' ? name : command.rawName;
365
- const displayOptions = command.isDefaultCommand ? [] : command.options;
355
+ // Hide deprecated options from subcommand help output
356
+ const displayOptions = command.isDefaultCommand ? [] : command.options.filter((o) => !o.deprecated);
366
357
  return {
367
358
  command,
368
359
  displayName,
@@ -382,7 +373,7 @@ class Command {
382
373
  body: commandRows
383
374
  .map(({ command, displayName, displayOptions }) => {
384
375
  const commandDescription = formatWrappedDescription(command.description, descriptionWidth, sharedDescriptionColumn);
385
- const commandPrefix = ` ${pc.bold(commandOrange(displayName))}`;
376
+ const commandPrefix = ` ${pc.bold(commandGreen(displayName))}`;
386
377
  const commandPadding = ' '.repeat(Math.max(2, sharedDescriptionColumn - (2 + visibleLength(displayName))));
387
378
  const headerLine = commandDescription
388
379
  ? `${commandPrefix}${commandPadding}${commandDescription}`
@@ -393,16 +384,16 @@ class Command {
393
384
  const optionLines = displayOptions
394
385
  .map((option) => {
395
386
  const optionDescription = formatWrappedDescription(optionDescriptionText(option), descriptionWidth, sharedDescriptionColumn);
396
- const optionPrefix = ` ${optionYellow(option.rawName)}`;
387
+ const optionPrefix = ` ${optionBlue(option.rawName)}`;
397
388
  const optionPadding = ' '.repeat(Math.max(2, sharedDescriptionColumn - (4 + visibleLength(option.rawName))));
398
389
  return optionDescription
399
390
  ? `${optionPrefix}${optionPadding}${optionDescription}`
400
391
  : optionPrefix;
401
392
  })
402
393
  .join('\n');
403
- return `${headerLine}\n${optionLines}`;
394
+ return `${headerLine}\n\n${optionLines}`;
404
395
  })
405
- .join('\n\n'),
396
+ .join('\n\n\n'),
406
397
  });
407
398
  }
408
399
  const defaultCommandOptions = this.isGlobalCommand
@@ -432,6 +423,8 @@ class Command {
432
423
  if (!this.isGlobalCommand && !this.isDefaultCommand) {
433
424
  options = options.filter((option) => option.name !== 'version');
434
425
  }
426
+ // Hide deprecated options from help output
427
+ options = options.filter((option) => !option.deprecated);
435
428
  if (options.length > 0) {
436
429
  const longestOptionNameLength = maxVisibleLength(options.map((option) => option.rawName));
437
430
  const descriptionColumn = 2 + longestOptionNameLength + 2;
@@ -443,8 +436,8 @@ class Command {
443
436
  const optionLabel = padRight(option.rawName, longestOptionNameLength);
444
437
  const description = formatWrappedDescription(optionDescriptionText(option), descriptionWidth, descriptionColumn);
445
438
  return description
446
- ? ` ${optionYellow(optionLabel)} ${description}`
447
- : ` ${optionYellow(optionLabel)}`;
439
+ ? ` ${optionBlue(optionLabel)} ${description}`
440
+ : ` ${optionBlue(optionLabel)}`;
448
441
  })
449
442
  .join('\n'),
450
443
  });
@@ -475,13 +468,16 @@ class Command {
475
468
  if (helpCallback) {
476
469
  sections = helpCallback(sections) || sections;
477
470
  }
478
- this.cli.console.log(sections
471
+ return sections
479
472
  .map((section) => {
480
473
  return section.title
481
474
  ? `${pc.bold(pc.blue(section.title))}:\n${section.body}`
482
475
  : section.body;
483
476
  })
484
- .join('\n\n'));
477
+ .join('\n\n\n');
478
+ }
479
+ outputHelp() {
480
+ this.cli.console.log(this.helpText());
485
481
  }
486
482
  outputVersion() {
487
483
  const { name } = this.cli;
@@ -564,6 +560,24 @@ function createConsole(stdout, stderr) {
564
560
  },
565
561
  };
566
562
  }
563
+ // ─── Error formatting ───
564
+ /**
565
+ * Format an error for CLI output.
566
+ * Prints a red "error:" prefix with the message, followed by a dimmed stack trace.
567
+ */
568
+ function formatCliError(err) {
569
+ const lines = [];
570
+ lines.push(`${pc.red(pc.bold('error:'))} ${err.message}`);
571
+ if (err.stack) {
572
+ // Extract just the stack frames (skip the first line which is the message)
573
+ const stackLines = err.stack.split('\n').slice(1);
574
+ if (stackLines.length > 0) {
575
+ lines.push('');
576
+ lines.push(pc.red(pc.dim(stackLines.join('\n'))));
577
+ }
578
+ }
579
+ return lines.join('\n');
580
+ }
567
581
  class Goke extends EventEmitter {
568
582
  /** The program name to display in help and version message */
569
583
  name;
@@ -593,6 +607,8 @@ class Goke extends EventEmitter {
593
607
  console;
594
608
  /** Terminal width used to wrap help output text */
595
609
  columns;
610
+ /** Exit function called on CLI errors. Defaults to process.exit */
611
+ exit;
596
612
  #defaultArgv;
597
613
  /**
598
614
  * @param name The program name to display in help and version message
@@ -609,6 +625,7 @@ class Goke extends EventEmitter {
609
625
  this.stderr = options?.stderr ?? process.stderr;
610
626
  this.console = createConsole(this.stdout, this.stderr);
611
627
  this.columns = options?.columns ?? process.stdout.columns ?? Number.POSITIVE_INFINITY;
628
+ this.exit = options?.exit ?? ((code) => process.exit(code));
612
629
  this.#defaultArgv = options?.argv ?? processArgs;
613
630
  this.globalCommand = new GlobalCommand(this);
614
631
  this.globalCommand.usage('<command> [options]');
@@ -668,19 +685,24 @@ class Goke extends EventEmitter {
668
685
  this.globalCommand.example(example);
669
686
  return this;
670
687
  }
688
+ /**
689
+ * Return the formatted help string without printing it.
690
+ * When a sub-command is matched, returns help for that command.
691
+ * Otherwise returns the global help.
692
+ */
693
+ helpText() {
694
+ if (this.matchedCommand) {
695
+ return this.matchedCommand.helpText();
696
+ }
697
+ return this.globalCommand.helpText();
698
+ }
671
699
  /**
672
700
  * Output the corresponding help message
673
701
  * When a sub-command is matched, output the help message for the command
674
702
  * Otherwise output the global one.
675
- *
676
703
  */
677
704
  outputHelp() {
678
- if (this.matchedCommand) {
679
- this.matchedCommand.outputHelp();
680
- }
681
- else {
682
- this.globalCommand.outputHelp();
683
- }
705
+ this.console.log(this.helpText());
684
706
  }
685
707
  /**
686
708
  * Output help for commands matching a prefix.
@@ -726,6 +748,20 @@ class Goke extends EventEmitter {
726
748
  this.matchedCommand = undefined;
727
749
  this.matchedCommandName = undefined;
728
750
  }
751
+ /**
752
+ * Handle a CLI error by formatting it and writing to stderr.
753
+ * For GokeError / coercion errors, also includes a help hint.
754
+ */
755
+ handleCliError(err) {
756
+ this.console.error(formatCliError(err));
757
+ // Add help hint when help is enabled
758
+ if (this.showHelpOnExit) {
759
+ const cmdName = this.matchedCommandName
760
+ ? `${this.name} ${this.matchedCommandName} --help`
761
+ : `${this.name} --help`;
762
+ this.console.error(`\nRun "${cmdName}" for usage information.`);
763
+ }
764
+ }
729
765
  /**
730
766
  * Parse argv
731
767
  */
@@ -743,51 +779,60 @@ class Goke extends EventEmitter {
743
779
  const bLength = b.name.split(' ').filter(Boolean).length;
744
780
  return bLength - aLength;
745
781
  });
746
- // Search sub-commands
747
- for (const command of sortedCommands) {
748
- const parsed = this.mri(argv.slice(2), command);
749
- const result = command.isMatched(parsed.args);
750
- if (result.matched) {
751
- shouldParse = false;
752
- const matchedCommandName = parsed.args.slice(0, result.consumedArgs).join(' ');
753
- const parsedInfo = {
754
- ...parsed,
755
- args: parsed.args.slice(result.consumedArgs),
756
- };
757
- this.setParsedInfo(parsedInfo, command, matchedCommandName);
758
- this.emit(`command:${matchedCommandName}`, command);
759
- break; // Stop after first match (greedy matching)
782
+ // Search sub-commands — mri() can throw coercion errors, catch them
783
+ try {
784
+ for (const command of sortedCommands) {
785
+ const parsed = this.mri(argv.slice(2), command);
786
+ const result = command.isMatched(parsed.args);
787
+ if (result.matched) {
788
+ shouldParse = false;
789
+ const matchedCommandName = parsed.args.slice(0, result.consumedArgs).join(' ');
790
+ const parsedInfo = {
791
+ ...parsed,
792
+ args: parsed.args.slice(result.consumedArgs),
793
+ };
794
+ this.setParsedInfo(parsedInfo, command, matchedCommandName);
795
+ this.emit(`command:${matchedCommandName}`, command);
796
+ break; // Stop after first match (greedy matching)
797
+ }
760
798
  }
761
- }
762
- if (shouldParse) {
763
- // Search the default command
764
- for (const command of this.commands) {
765
- if (command.name === '') {
766
- // Check if any argument is a prefix of an existing command
767
- // If so, don't match the default command (user probably mistyped a subcommand)
768
- const parsed = this.mri(argv.slice(2), command);
769
- const firstArg = parsed.args[0];
770
- if (firstArg) {
771
- const isPrefixOfCommand = this.commands.some((cmd) => {
772
- if (cmd.name === '')
773
- return false;
774
- const cmdParts = cmd.name.split(' ');
775
- return cmdParts[0] === firstArg;
776
- });
777
- if (isPrefixOfCommand) {
778
- // Don't match default command - let it fall through to "unknown command"
779
- continue;
799
+ if (shouldParse) {
800
+ // Search the default command
801
+ for (const command of this.commands) {
802
+ if (command.name === '') {
803
+ // Check if any argument is a prefix of an existing command
804
+ // If so, don't match the default command (user probably mistyped a subcommand)
805
+ const parsed = this.mri(argv.slice(2), command);
806
+ const firstArg = parsed.args[0];
807
+ if (firstArg) {
808
+ const isPrefixOfCommand = this.commands.some((cmd) => {
809
+ if (cmd.name === '')
810
+ return false;
811
+ const cmdParts = cmd.name.split(' ');
812
+ return cmdParts[0] === firstArg;
813
+ });
814
+ if (isPrefixOfCommand) {
815
+ // Don't match default command - let it fall through to "unknown command"
816
+ continue;
817
+ }
780
818
  }
819
+ shouldParse = false;
820
+ this.setParsedInfo(parsed, command);
821
+ this.emit(`command:!`, command);
781
822
  }
782
- shouldParse = false;
783
- this.setParsedInfo(parsed, command);
784
- this.emit(`command:!`, command);
785
823
  }
786
824
  }
825
+ if (shouldParse) {
826
+ const parsed = this.mri(argv.slice(2));
827
+ this.setParsedInfo(parsed);
828
+ }
787
829
  }
788
- if (shouldParse) {
789
- const parsed = this.mri(argv.slice(2));
790
- this.setParsedInfo(parsed);
830
+ catch (err) {
831
+ if (err instanceof GokeError) {
832
+ this.handleCliError(err);
833
+ this.exit(1);
834
+ }
835
+ throw err;
791
836
  }
792
837
  if (this.options.help && this.showHelpOnExit) {
793
838
  if (!this.matchedCommand && this.args[0]) {
@@ -959,9 +1004,18 @@ class Goke extends EventEmitter {
959
1004
  const { args, options, matchedCommand: command } = this;
960
1005
  if (!command || !command.commandAction)
961
1006
  return;
962
- command.checkUnknownOptions();
963
- command.checkOptionValue();
964
- command.checkRequiredArgs();
1007
+ try {
1008
+ command.checkUnknownOptions();
1009
+ command.checkOptionValue();
1010
+ command.checkRequiredArgs();
1011
+ }
1012
+ catch (err) {
1013
+ if (err instanceof GokeError) {
1014
+ this.handleCliError(err);
1015
+ this.exit(1);
1016
+ }
1017
+ throw err;
1018
+ }
965
1019
  const actionArgs = [];
966
1020
  command.args.forEach((arg, index) => {
967
1021
  if (arg.variadic) {
@@ -972,7 +1026,20 @@ class Goke extends EventEmitter {
972
1026
  }
973
1027
  });
974
1028
  actionArgs.push(options);
975
- return command.commandAction.apply(this, actionArgs);
1029
+ const result = command.commandAction.apply(this, actionArgs);
1030
+ // If the action returns a promise, catch async errors
1031
+ if (result && typeof result === 'object' && typeof result.catch === 'function') {
1032
+ result.catch((err) => {
1033
+ if (err instanceof Error) {
1034
+ this.handleCliError(err);
1035
+ }
1036
+ else {
1037
+ this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`);
1038
+ }
1039
+ this.exit(1);
1040
+ });
1041
+ }
1042
+ return result;
976
1043
  }
977
1044
  }
978
1045
  export { createConsole, Command };
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ export { goke, Goke, Command };
11
11
  export { createConsole } from "./goke.js";
12
12
  export type { GokeOutputStream, GokeConsole, GokeOptions } from "./goke.js";
13
13
  export type { StandardTypedV1, StandardJSONSchemaV1, JsonSchema } from "./coerce.js";
14
- export { coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js";
14
+ export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js";
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC;;;GAGG;AACH,QAAA,MAAM,IAAI,GAAI,aAAS,EAAE,UAAU,WAAW,SAA4B,CAAA;AAE1E,eAAe,IAAI,CAAA;AACnB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC3E,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC;;;GAGG;AACH,QAAA,MAAM,IAAI,GAAI,aAAS,EAAE,UAAU,WAAW,SAA4B,CAAA;AAE1E,eAAe,IAAI,CAAA;AACnB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC3E,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -8,4 +8,4 @@ const goke = (name = '', options) => new Goke(name, options);
8
8
  export default goke;
9
9
  export { goke, Goke, Command };
10
10
  export { createConsole } from "./goke.js";
11
- export { coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js";
11
+ export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goke",
3
- "version": "6.1.3",
3
+ "version": "6.2.0",
4
4
  "type": "module",
5
5
  "description": "Simple yet powerful framework for building command-line apps. Inspired by cac.",
6
6
  "repository": {