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/src/coerce.ts CHANGED
@@ -29,6 +29,25 @@
29
29
  * Tries each type in order, returns first successful coercion.
30
30
  */
31
31
 
32
+ // ─── GokeError ───
33
+
34
+ /**
35
+ * Custom error class for CLI usage errors (unknown options, missing values,
36
+ * invalid types, etc.). Used by both the coercion layer and the framework
37
+ * to distinguish user-facing errors from unexpected failures.
38
+ */
39
+ export class GokeError extends Error {
40
+ constructor(message: string) {
41
+ super(message)
42
+ this.name = this.constructor.name
43
+ if (typeof Error.captureStackTrace === 'function') {
44
+ Error.captureStackTrace(this, this.constructor)
45
+ } else {
46
+ this.stack = new Error(message).stack
47
+ }
48
+ }
49
+ }
50
+
32
51
  // ─── Standard Schema types (vendored from @standard-schema/spec v1.1.0) ───
33
52
  // https://github.com/standard-schema/standard-schema
34
53
  //
@@ -108,23 +127,8 @@ export declare namespace StandardJSONSchemaV1 {
108
127
  /**
109
128
  * Wraps a plain JSON Schema object into a StandardJSONSchemaV1-compatible object.
110
129
  *
111
- * This is useful for dynamic use cases where you have a raw JSON Schema
112
- * (e.g. from an MCP tool's inputSchema) and need to pass it to Goke's
113
- * schema option which expects StandardJSONSchemaV1.
114
- *
115
- * @example
116
- * ```ts
117
- * import { wrapJsonSchema } from 'goke'
118
- *
119
- * // Wrap a plain JSON Schema for use with Goke options
120
- * const schema = wrapJsonSchema({ type: "number" })
121
- * cmd.option('--port <port>', 'Port', { schema })
122
- *
123
- * // Wrap MCP tool property schemas
124
- * for (const [name, propSchema] of Object.entries(tool.inputSchema.properties)) {
125
- * cmd.option(`--${name} <${name}>`, desc, { schema: wrapJsonSchema(propSchema) })
126
- * }
127
- * ```
130
+ * @internal This is an internal helper used by @goke/mcp to wrap MCP tool schemas.
131
+ * Users should pass Zod or other StandardSchema-compatible schemas to `.option()`.
128
132
  *
129
133
  * @param jsonSchema - A plain JSON Schema object (e.g. `{ type: "number" }`)
130
134
  * @returns A StandardJSONSchemaV1-compatible object that Goke can use for coercion
@@ -158,6 +162,8 @@ export interface JsonSchema {
158
162
  additionalProperties?: boolean | JsonSchema
159
163
  default?: unknown
160
164
  description?: string
165
+ /** JSON Schema deprecated annotation (draft 2019-09+) */
166
+ deprecated?: boolean
161
167
  }
162
168
 
163
169
  /**
@@ -236,7 +242,7 @@ export function coerceBySchema(
236
242
  }
237
243
  }
238
244
  // Schema does NOT expect array — repeated flags are not allowed
239
- throw new Error(
245
+ throw new GokeError(
240
246
  `Option --${optionName} does not accept multiple values. ` +
241
247
  `Use an array schema (e.g. { type: "array" }) to allow repeated flags.`
242
248
  )
@@ -289,7 +295,7 @@ export function coerceBySchema(
289
295
  return allowed
290
296
  }
291
297
  }
292
- throw new Error(
298
+ throw new GokeError(
293
299
  `Invalid value for --${optionName}: expected one of ${schema.enum.map(v => JSON.stringify(v)).join(', ')}, got ${JSON.stringify(value)}`
294
300
  )
295
301
  }
@@ -301,7 +307,7 @@ export function coerceBySchema(
301
307
  const targetType = constVal === null ? 'null' : typeof constVal as string
302
308
  const coerced = coerceToSingleType(value, targetType, optionName)
303
309
  if (coerced === constVal) return coerced
304
- throw new Error(
310
+ throw new GokeError(
305
311
  `Invalid value for --${optionName}: expected ${JSON.stringify(constVal)}, got ${JSON.stringify(value)}`
306
312
  )
307
313
  }
@@ -316,7 +322,7 @@ export function coerceBySchema(
316
322
  // Try next variant
317
323
  }
318
324
  }
319
- throw new Error(
325
+ throw new GokeError(
320
326
  `Invalid value for --${optionName}: ${JSON.stringify(value)} does not match any allowed type`
321
327
  )
322
328
  }
@@ -351,7 +357,7 @@ export function coerceBySchema(
351
357
  // Try next type
352
358
  }
353
359
  }
354
- throw new Error(
360
+ throw new GokeError(
355
361
  `Invalid value for --${optionName}: expected ${schemaType.join(' or ')}, got ${JSON.stringify(value)}`
356
362
  )
357
363
  }
@@ -401,11 +407,11 @@ function coerceToNumber(value: string | boolean, optionName: string): number {
401
407
  return value ? 1 : 0
402
408
  }
403
409
  if (value === '') {
404
- throw new Error(`Invalid value for --${optionName}: expected number, got empty string`)
410
+ throw new GokeError(`Invalid value for --${optionName}: expected number, got empty string`)
405
411
  }
406
412
  const num = +value
407
413
  if (!Number.isFinite(num)) {
408
- throw new Error(`Invalid value for --${optionName}: expected number, got ${JSON.stringify(value)}`)
414
+ throw new GokeError(`Invalid value for --${optionName}: expected number, got ${JSON.stringify(value)}`)
409
415
  }
410
416
  return num
411
417
  }
@@ -413,7 +419,7 @@ function coerceToNumber(value: string | boolean, optionName: string): number {
413
419
  function coerceToInteger(value: string | boolean, optionName: string): number {
414
420
  const num = coerceToNumber(value, optionName)
415
421
  if (num % 1 !== 0) {
416
- throw new Error(`Invalid value for --${optionName}: expected integer, got ${JSON.stringify(value)}`)
422
+ throw new GokeError(`Invalid value for --${optionName}: expected integer, got ${JSON.stringify(value)}`)
417
423
  }
418
424
  return num
419
425
  }
@@ -424,21 +430,21 @@ function coerceToBoolean(value: string | boolean, optionName: string): boolean {
424
430
  }
425
431
  if (value === 'true') return true
426
432
  if (value === 'false') return false
427
- throw new Error(
433
+ throw new GokeError(
428
434
  `Invalid value for --${optionName}: expected true or false, got ${JSON.stringify(value)}`
429
435
  )
430
436
  }
431
437
 
432
438
  function coerceToNull(value: string | boolean, optionName: string): null {
433
439
  if (typeof value === 'string' && value === '') return null
434
- throw new Error(
440
+ throw new GokeError(
435
441
  `Invalid value for --${optionName}: expected empty string for null, got ${JSON.stringify(value)}`
436
442
  )
437
443
  }
438
444
 
439
445
  function coerceToObject(value: string | boolean, optionName: string): Record<string, unknown> {
440
446
  if (typeof value !== 'string') {
441
- throw new Error(`Invalid value for --${optionName}: expected JSON object, got ${typeof value}`)
447
+ throw new GokeError(`Invalid value for --${optionName}: expected JSON object, got ${typeof value}`)
442
448
  }
443
449
  try {
444
450
  const parsed = JSON.parse(value)
@@ -447,7 +453,7 @@ function coerceToObject(value: string | boolean, optionName: string): Record<str
447
453
  }
448
454
  return parsed as Record<string, unknown>
449
455
  } catch {
450
- throw new Error(
456
+ throw new GokeError(
451
457
  `Invalid value for --${optionName}: expected valid JSON object, got ${JSON.stringify(value)}`
452
458
  )
453
459
  }
@@ -455,7 +461,7 @@ function coerceToObject(value: string | boolean, optionName: string): Record<str
455
461
 
456
462
  function coerceToArray(value: string | boolean, optionName: string): unknown[] {
457
463
  if (typeof value !== 'string') {
458
- throw new Error(`Invalid value for --${optionName}: expected JSON array, got ${typeof value}`)
464
+ throw new GokeError(`Invalid value for --${optionName}: expected JSON array, got ${typeof value}`)
459
465
  }
460
466
  try {
461
467
  const parsed = JSON.parse(value)
@@ -464,7 +470,7 @@ function coerceToArray(value: string | boolean, optionName: string): unknown[] {
464
470
  }
465
471
  return parsed
466
472
  } catch {
467
- throw new Error(
473
+ throw new GokeError(
468
474
  `Invalid value for --${optionName}: expected valid JSON array, got ${JSON.stringify(value)}`
469
475
  )
470
476
  }
@@ -521,18 +527,21 @@ export function isStandardSchema(value: unknown): value is StandardJSONSchemaV1
521
527
  }
522
528
 
523
529
  /**
524
- * Extract description and default value from a StandardJSONSchemaV1-compatible schema.
525
- * Calls extractJsonSchema() internally and pulls `description` and `default` fields.
530
+ * Extract description, default value, and deprecated flag from a StandardJSONSchemaV1-compatible schema.
531
+ * Calls extractJsonSchema() internally and pulls `description`, `default`, and `deprecated` fields.
526
532
  */
527
- export function extractSchemaMetadata(schema: StandardJSONSchemaV1): { description?: string; default?: unknown } {
533
+ export function extractSchemaMetadata(schema: StandardJSONSchemaV1): { description?: string; default?: unknown; deprecated?: boolean } {
528
534
  const jsonSchema = extractJsonSchema(schema)
529
535
  if (!jsonSchema) return {}
530
- const result: { description?: string; default?: unknown } = {}
536
+ const result: { description?: string; default?: unknown; deprecated?: boolean } = {}
531
537
  if (typeof jsonSchema.description === 'string') {
532
538
  result.description = jsonSchema.description
533
539
  }
534
540
  if (jsonSchema.default !== undefined) {
535
541
  result.default = jsonSchema.default
536
542
  }
543
+ if (jsonSchema.deprecated === true) {
544
+ result.deprecated = true
545
+ }
537
546
  return result
538
547
  }
package/src/goke.ts 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
@@ -14,7 +13,7 @@
14
13
  import { EventEmitter } from 'events'
15
14
  import pc from 'picocolors'
16
15
  import mri from "./mri.js"
17
- import { coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
16
+ import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
18
17
  import type { StandardJSONSchemaV1 } from "./coerce.js"
19
18
 
20
19
  // ─── Node.js platform constants ───
@@ -95,14 +94,9 @@ const ANSI_RE = /\x1B\[[0-9;]*m/g
95
94
 
96
95
  const visibleLength = (value: string) => value.replace(ANSI_RE, '').length
97
96
 
98
- const commandOrange = (value: string) => {
99
- if (!pc.isColorSupported) {
100
- return value
101
- }
102
- return `\x1b[38;5;208m${value}\x1b[39m`
103
- }
97
+ const commandGreen = (value: string) => pc.bold(pc.greenBright(value))
104
98
 
105
- const optionYellow = (value: string) => pc.bold(pc.yellowBright(value))
99
+ const optionBlue = (value: string) => pc.bold(pc.blueBright(value))
106
100
 
107
101
  const padRight = (str: string, length: number) => {
108
102
  return visibleLength(str) >= length ? str : `${str}${' '.repeat(length - visibleLength(str))}`
@@ -223,20 +217,6 @@ const camelcaseOptionName = (name: string) => {
223
217
  .join('.')
224
218
  }
225
219
 
226
- // ─── GokeError ───
227
-
228
- class GokeError extends Error {
229
- constructor(message: string) {
230
- super(message)
231
- this.name = this.constructor.name
232
- if (typeof Error.captureStackTrace === 'function') {
233
- Error.captureStackTrace(this, this.constructor)
234
- } else {
235
- this.stack = new Error(message).stack
236
- }
237
- }
238
- }
239
-
240
220
  // ─── Option ───
241
221
 
242
222
  class Option {
@@ -253,6 +233,8 @@ class Option {
253
233
  default?: unknown
254
234
  /** Standard JSON Schema V1 schema for type coercion and inference */
255
235
  schema?: StandardJSONSchemaV1
236
+ /** Whether this option is deprecated (hidden from help output) */
237
+ deprecated?: boolean
256
238
 
257
239
  /**
258
240
  * Create an option.
@@ -273,6 +255,9 @@ class Option {
273
255
  if (meta.default !== undefined) {
274
256
  this.default = meta.default
275
257
  }
258
+ if (meta.deprecated) {
259
+ this.deprecated = true
260
+ }
276
261
  } else {
277
262
  this.description = ''
278
263
  }
@@ -502,7 +487,11 @@ class Command {
502
487
  })
503
488
  }
504
489
 
505
- outputHelp() {
490
+ /**
491
+ * Return the formatted help string without printing it.
492
+ * Useful for embedding help text in documentation, tests, or other programmatic uses.
493
+ */
494
+ helpText(): string {
506
495
  const { name, commands } = this.cli
507
496
  const {
508
497
  versionNumber,
@@ -528,7 +517,8 @@ class Command {
528
517
  if (showCommands) {
529
518
  const commandRows = commands.map((command) => {
530
519
  const displayName = command.rawName.trim() === '' ? name : command.rawName
531
- const displayOptions = command.isDefaultCommand ? [] : command.options
520
+ // Hide deprecated options from subcommand help output
521
+ const displayOptions = command.isDefaultCommand ? [] : command.options.filter((o) => !o.deprecated)
532
522
  return {
533
523
  command,
534
524
  displayName,
@@ -556,7 +546,7 @@ class Command {
556
546
  descriptionWidth,
557
547
  sharedDescriptionColumn,
558
548
  )
559
- const commandPrefix = ` ${pc.bold(commandOrange(displayName))}`
549
+ const commandPrefix = ` ${pc.bold(commandGreen(displayName))}`
560
550
  const commandPadding = ' '.repeat(
561
551
  Math.max(2, sharedDescriptionColumn - (2 + visibleLength(displayName)))
562
552
  )
@@ -575,7 +565,7 @@ class Command {
575
565
  descriptionWidth,
576
566
  sharedDescriptionColumn,
577
567
  )
578
- const optionPrefix = ` ${optionYellow(option.rawName)}`
568
+ const optionPrefix = ` ${optionBlue(option.rawName)}`
579
569
  const optionPadding = ' '.repeat(
580
570
  Math.max(2, sharedDescriptionColumn - (4 + visibleLength(option.rawName)))
581
571
  )
@@ -585,9 +575,9 @@ class Command {
585
575
  })
586
576
  .join('\n')
587
577
 
588
- return `${headerLine}\n${optionLines}`
578
+ return `${headerLine}\n\n${optionLines}`
589
579
  })
590
- .join('\n\n'),
580
+ .join('\n\n\n'),
591
581
  })
592
582
  }
593
583
 
@@ -621,6 +611,8 @@ class Command {
621
611
  if (!this.isGlobalCommand && !this.isDefaultCommand) {
622
612
  options = options.filter((option) => option.name !== 'version')
623
613
  }
614
+ // Hide deprecated options from help output
615
+ options = options.filter((option) => !option.deprecated)
624
616
  if (options.length > 0) {
625
617
  const longestOptionNameLength = maxVisibleLength(
626
618
  options.map((option) => option.rawName)
@@ -638,8 +630,8 @@ class Command {
638
630
  descriptionColumn,
639
631
  )
640
632
  return description
641
- ? ` ${optionYellow(optionLabel)} ${description}`
642
- : ` ${optionYellow(optionLabel)}`
633
+ ? ` ${optionBlue(optionLabel)} ${description}`
634
+ : ` ${optionBlue(optionLabel)}`
643
635
  })
644
636
  .join('\n'),
645
637
  })
@@ -674,15 +666,17 @@ class Command {
674
666
  sections = helpCallback(sections) || sections
675
667
  }
676
668
 
677
- this.cli.console.log(
678
- sections
679
- .map((section) => {
680
- return section.title
681
- ? `${pc.bold(pc.blue(section.title))}:\n${section.body}`
682
- : section.body
683
- })
684
- .join('\n\n')
685
- )
669
+ return sections
670
+ .map((section) => {
671
+ return section.title
672
+ ? `${pc.bold(pc.blue(section.title))}:\n${section.body}`
673
+ : section.body
674
+ })
675
+ .join('\n\n\n')
676
+ }
677
+
678
+ outputHelp() {
679
+ this.cli.console.log(this.helpText())
686
680
  }
687
681
 
688
682
  outputVersion() {
@@ -793,6 +787,11 @@ interface GokeOptions {
793
787
  argv?: string[]
794
788
  /** Terminal width used to wrap help output. Defaults to process.stdout.columns, or Infinity when unavailable */
795
789
  columns?: number
790
+ /**
791
+ * Custom exit function called on CLI errors (unknown option, missing value, etc.).
792
+ * Defaults to process.exit. Set to a no-op or throw to prevent exit in tests.
793
+ */
794
+ exit?: (code: number) => void
796
795
  }
797
796
 
798
797
  /**
@@ -813,6 +812,26 @@ function createConsole(stdout: GokeOutputStream, stderr: GokeOutputStream): Goke
813
812
  }
814
813
  }
815
814
 
815
+ // ─── Error formatting ───
816
+
817
+ /**
818
+ * Format an error for CLI output.
819
+ * Prints a red "error:" prefix with the message, followed by a dimmed stack trace.
820
+ */
821
+ function formatCliError(err: Error): string {
822
+ const lines: string[] = []
823
+ lines.push(`${pc.red(pc.bold('error:'))} ${err.message}`)
824
+ if (err.stack) {
825
+ // Extract just the stack frames (skip the first line which is the message)
826
+ const stackLines = err.stack.split('\n').slice(1)
827
+ if (stackLines.length > 0) {
828
+ lines.push('')
829
+ lines.push(pc.red(pc.dim(stackLines.join('\n'))))
830
+ }
831
+ }
832
+ return lines.join('\n')
833
+ }
834
+
816
835
  // ─── Goke (main CLI class) ───
817
836
 
818
837
  interface ParsedArgv {
@@ -853,6 +872,8 @@ class Goke extends EventEmitter {
853
872
  readonly console: GokeConsole
854
873
  /** Terminal width used to wrap help output text */
855
874
  readonly columns: number
875
+ /** Exit function called on CLI errors. Defaults to process.exit */
876
+ readonly exit: (code: number) => void
856
877
 
857
878
  #defaultArgv: string[]
858
879
 
@@ -871,6 +892,7 @@ class Goke extends EventEmitter {
871
892
  this.stderr = options?.stderr ?? process.stderr
872
893
  this.console = createConsole(this.stdout, this.stderr)
873
894
  this.columns = options?.columns ?? process.stdout.columns ?? Number.POSITIVE_INFINITY
895
+ this.exit = options?.exit ?? ((code: number) => process.exit(code))
874
896
  this.#defaultArgv = options?.argv ?? processArgs
875
897
  this.globalCommand = new GlobalCommand(this)
876
898
  this.globalCommand.usage('<command> [options]')
@@ -937,18 +959,25 @@ class Goke extends EventEmitter {
937
959
  return this
938
960
  }
939
961
 
962
+ /**
963
+ * Return the formatted help string without printing it.
964
+ * When a sub-command is matched, returns help for that command.
965
+ * Otherwise returns the global help.
966
+ */
967
+ helpText(): string {
968
+ if (this.matchedCommand) {
969
+ return this.matchedCommand.helpText()
970
+ }
971
+ return this.globalCommand.helpText()
972
+ }
973
+
940
974
  /**
941
975
  * Output the corresponding help message
942
976
  * When a sub-command is matched, output the help message for the command
943
977
  * Otherwise output the global one.
944
- *
945
978
  */
946
979
  outputHelp() {
947
- if (this.matchedCommand) {
948
- this.matchedCommand.outputHelp()
949
- } else {
950
- this.globalCommand.outputHelp()
951
- }
980
+ this.console.log(this.helpText())
952
981
  }
953
982
 
954
983
  /**
@@ -1008,6 +1037,22 @@ class Goke extends EventEmitter {
1008
1037
  this.matchedCommandName = undefined
1009
1038
  }
1010
1039
 
1040
+ /**
1041
+ * Handle a CLI error by formatting it and writing to stderr.
1042
+ * For GokeError / coercion errors, also includes a help hint.
1043
+ */
1044
+ private handleCliError(err: Error): void {
1045
+ this.console.error(formatCliError(err))
1046
+
1047
+ // Add help hint when help is enabled
1048
+ if (this.showHelpOnExit) {
1049
+ const cmdName = this.matchedCommandName
1050
+ ? `${this.name} ${this.matchedCommandName} --help`
1051
+ : `${this.name} --help`
1052
+ this.console.error(`\nRun "${cmdName}" for usage information.`)
1053
+ }
1054
+ }
1055
+
1011
1056
  /**
1012
1057
  * Parse argv
1013
1058
  */
@@ -1032,53 +1077,61 @@ class Goke extends EventEmitter {
1032
1077
  return bLength - aLength
1033
1078
  })
1034
1079
 
1035
- // Search sub-commands
1036
- for (const command of sortedCommands) {
1037
- const parsed = this.mri(argv.slice(2), command)
1038
-
1039
- const result = command.isMatched(parsed.args as string[])
1040
- if (result.matched) {
1041
- shouldParse = false
1042
- const matchedCommandName = parsed.args.slice(0, result.consumedArgs).join(' ')
1043
- const parsedInfo = {
1044
- ...parsed,
1045
- args: parsed.args.slice(result.consumedArgs),
1080
+ // Search sub-commands — mri() can throw coercion errors, catch them
1081
+ try {
1082
+ for (const command of sortedCommands) {
1083
+ const parsed = this.mri(argv.slice(2), command)
1084
+
1085
+ const result = command.isMatched(parsed.args as string[])
1086
+ if (result.matched) {
1087
+ shouldParse = false
1088
+ const matchedCommandName = parsed.args.slice(0, result.consumedArgs).join(' ')
1089
+ const parsedInfo = {
1090
+ ...parsed,
1091
+ args: parsed.args.slice(result.consumedArgs),
1092
+ }
1093
+ this.setParsedInfo(parsedInfo, command, matchedCommandName)
1094
+ this.emit(`command:${matchedCommandName}`, command)
1095
+ break // Stop after first match (greedy matching)
1046
1096
  }
1047
- this.setParsedInfo(parsedInfo, command, matchedCommandName)
1048
- this.emit(`command:${matchedCommandName}`, command)
1049
- break // Stop after first match (greedy matching)
1050
1097
  }
1051
- }
1052
1098
 
1053
- if (shouldParse) {
1054
- // Search the default command
1055
- for (const command of this.commands) {
1056
- if (command.name === '') {
1057
- // Check if any argument is a prefix of an existing command
1058
- // If so, don't match the default command (user probably mistyped a subcommand)
1059
- const parsed = this.mri(argv.slice(2), command)
1060
- const firstArg = parsed.args[0]
1061
- if (firstArg) {
1062
- const isPrefixOfCommand = this.commands.some((cmd) => {
1063
- if (cmd.name === '') return false
1064
- const cmdParts = cmd.name.split(' ')
1065
- return cmdParts[0] === firstArg
1066
- })
1067
- if (isPrefixOfCommand) {
1068
- // Don't match default command - let it fall through to "unknown command"
1069
- continue
1099
+ if (shouldParse) {
1100
+ // Search the default command
1101
+ for (const command of this.commands) {
1102
+ if (command.name === '') {
1103
+ // Check if any argument is a prefix of an existing command
1104
+ // If so, don't match the default command (user probably mistyped a subcommand)
1105
+ const parsed = this.mri(argv.slice(2), command)
1106
+ const firstArg = parsed.args[0]
1107
+ if (firstArg) {
1108
+ const isPrefixOfCommand = this.commands.some((cmd) => {
1109
+ if (cmd.name === '') return false
1110
+ const cmdParts = cmd.name.split(' ')
1111
+ return cmdParts[0] === firstArg
1112
+ })
1113
+ if (isPrefixOfCommand) {
1114
+ // Don't match default command - let it fall through to "unknown command"
1115
+ continue
1116
+ }
1070
1117
  }
1118
+ shouldParse = false
1119
+ this.setParsedInfo(parsed, command)
1120
+ this.emit(`command:!`, command)
1071
1121
  }
1072
- shouldParse = false
1073
- this.setParsedInfo(parsed, command)
1074
- this.emit(`command:!`, command)
1075
1122
  }
1076
1123
  }
1077
- }
1078
1124
 
1079
- if (shouldParse) {
1080
- const parsed = this.mri(argv.slice(2))
1081
- this.setParsedInfo(parsed)
1125
+ if (shouldParse) {
1126
+ const parsed = this.mri(argv.slice(2))
1127
+ this.setParsedInfo(parsed)
1128
+ }
1129
+ } catch (err) {
1130
+ if (err instanceof GokeError) {
1131
+ this.handleCliError(err)
1132
+ this.exit(1)
1133
+ }
1134
+ throw err
1082
1135
  }
1083
1136
 
1084
1137
  if (this.options.help && this.showHelpOnExit) {
@@ -1275,11 +1328,17 @@ class Goke extends EventEmitter {
1275
1328
 
1276
1329
  if (!command || !command.commandAction) return
1277
1330
 
1278
- command.checkUnknownOptions()
1279
-
1280
- command.checkOptionValue()
1281
-
1282
- command.checkRequiredArgs()
1331
+ try {
1332
+ command.checkUnknownOptions()
1333
+ command.checkOptionValue()
1334
+ command.checkRequiredArgs()
1335
+ } catch (err) {
1336
+ if (err instanceof GokeError) {
1337
+ this.handleCliError(err)
1338
+ this.exit(1)
1339
+ }
1340
+ throw err
1341
+ }
1283
1342
 
1284
1343
  const actionArgs: any[] = []
1285
1344
  command.args.forEach((arg, index) => {
@@ -1290,7 +1349,22 @@ class Goke extends EventEmitter {
1290
1349
  }
1291
1350
  })
1292
1351
  actionArgs.push(options)
1293
- return command.commandAction.apply(this, actionArgs)
1352
+
1353
+ const result = command.commandAction.apply(this, actionArgs)
1354
+
1355
+ // If the action returns a promise, catch async errors
1356
+ if (result && typeof result === 'object' && typeof result.catch === 'function') {
1357
+ result.catch((err: unknown) => {
1358
+ if (err instanceof Error) {
1359
+ this.handleCliError(err)
1360
+ } else {
1361
+ this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`)
1362
+ }
1363
+ this.exit(1)
1364
+ })
1365
+ }
1366
+
1367
+ return result
1294
1368
  }
1295
1369
  }
1296
1370
 
package/src/index.ts CHANGED
@@ -13,4 +13,4 @@ export { goke, Goke, Command }
13
13
  export { createConsole } from "./goke.js"
14
14
  export type { GokeOutputStream, GokeConsole, GokeOptions } from "./goke.js"
15
15
  export type { StandardTypedV1, StandardJSONSchemaV1, JsonSchema } from "./coerce.js"
16
- export { coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js"
16
+ export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js"