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/README.md +239 -3
- package/dist/__test__/index.test.js +347 -8
- package/dist/coerce.d.ts +15 -19
- package/dist/coerce.d.ts.map +1 -1
- package/dist/coerce.js +39 -33
- package/dist/goke.d.ts +25 -2
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +149 -82
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/__test__/index.test.ts +412 -9
- package/src/coerce.ts +44 -35
- package/src/goke.ts +165 -91
- package/src/index.ts +1 -1
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
|
|
112
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
525
|
-
* Calls extractJsonSchema() internally and pulls `description` and `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 = ` ${
|
|
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
|
-
? ` ${
|
|
642
|
-
: ` ${
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
.
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
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"
|