goke 6.5.0 → 6.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/src/goke.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - Utility functions: string helpers, bracket parsing, dot-prop access
11
11
  */
12
12
 
13
- import pc from 'picocolors'
13
+ import pc from './picocolors.js'
14
14
  import mri from "./mri.js"
15
15
  import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
16
16
  import type { StandardJSONSchemaV1 } from "./coerce.js"
@@ -348,6 +348,92 @@ type OptionEntry<RawName extends string, Schema> =
348
348
  ? { [K in ExtractOptionName<RawName>]?: InferSchemaOutput<Schema> }
349
349
  : { [K in ExtractOptionName<RawName>]: InferSchemaOutput<Schema> }
350
350
 
351
+ /**
352
+ * Infer the raw runtime value shape for an option declared without a schema.
353
+ *
354
+ * Required value options (`--port <port>`) always reach actions as strings.
355
+ * Optional value options (`--host [host]`) can be strings, the sentinel
356
+ * boolean `true` when passed without a value, or `undefined` when omitted.
357
+ * Plain flags (`--verbose`) are booleans.
358
+ */
359
+ type UntypedOptionValue<RawName extends string> =
360
+ RawName extends `${string}<${string}>` ? string :
361
+ RawName extends `${string}[${string}]` ? string | boolean | undefined :
362
+ boolean | undefined
363
+
364
+ /**
365
+ * Build the option type entry for a `.option()` call that uses a plain
366
+ * description (no schema).
367
+ */
368
+ type UntypedOptionEntry<RawName extends string> =
369
+ RawName extends `${string}<${string}>`
370
+ ? { [K in ExtractOptionName<RawName>]: UntypedOptionValue<RawName> }
371
+ : { [K in ExtractOptionName<RawName>]?: UntypedOptionValue<RawName> }
372
+
373
+ /**
374
+ * Tokenize a command raw name by splitting on whitespace.
375
+ * "mcp getNodeXml <id>" → ["mcp", "getNodeXml", "<id>"]
376
+ * "" → []
377
+ */
378
+ type TokenizeName<S extends string, Acc extends readonly string[] = []> =
379
+ S extends `${infer Head} ${infer Rest}`
380
+ ? TokenizeName<Rest, [...Acc, Head]>
381
+ : S extends ''
382
+ ? Acc
383
+ : [...Acc, S]
384
+
385
+ /**
386
+ * Given a single token, return the corresponding positional arg type or
387
+ * `never` if the token is not a bracketed arg.
388
+ *
389
+ * `<id>` → string (required)
390
+ * `[id]` → string | undefined (optional)
391
+ * `<...files>` → string[] (variadic required)
392
+ * `[...files]` → string[] (variadic optional)
393
+ * Anything else → never (filtered out by ExtractCommandArgs)
394
+ */
395
+ type TokenToArgType<T extends string> =
396
+ T extends `<...${string}>` ? string[] :
397
+ T extends `[...${string}]` ? string[] :
398
+ T extends `<${string}>` ? string :
399
+ T extends `[${string}]` ? string | undefined :
400
+ never
401
+
402
+ /**
403
+ * Filter a tokenized command raw name down to the positional arg tokens
404
+ * and map each to its inferred type.
405
+ */
406
+ type ExtractCommandArgs<T extends readonly string[]> =
407
+ T extends readonly [infer Head extends string, ...infer Tail extends string[]]
408
+ ? [TokenToArgType<Head>] extends [never]
409
+ ? ExtractCommandArgs<Tail>
410
+ : [TokenToArgType<Head>, ...ExtractCommandArgs<Tail>]
411
+ : []
412
+
413
+ /**
414
+ * Extract the tuple of positional arg types from a command raw name.
415
+ *
416
+ * "mcp getNodeXml <id>" → [string]
417
+ * "convert <input> <output>" → [string, string]
418
+ * "run [script]" → [string | undefined]
419
+ * "exec [...args]" → [string[]]
420
+ * "deploy" → []
421
+ */
422
+ type ExtractPositionalArgs<RawName extends string> =
423
+ ExtractCommandArgs<TokenizeName<RawName>>
424
+
425
+ /**
426
+ * Build the full argument tuple passed to a command's action callback.
427
+ *
428
+ * Format: [...positionalArgs, options, executionContext]
429
+ *
430
+ * This matches the runtime behavior in Goke.runMatchedCommand(): the action
431
+ * is called with positional args from the parsed command, then the parsed
432
+ * options object, then the injected GokeExecutionContext.
433
+ */
434
+ type ActionArgs<RawName extends string, Opts> =
435
+ [...ExtractPositionalArgs<RawName>, Opts, GokeExecutionContext]
436
+
351
437
  interface CommandArg {
352
438
  required: boolean
353
439
  value: string
@@ -368,7 +454,7 @@ type HelpCallback = (sections: HelpSection[]) => void | HelpSection[]
368
454
 
369
455
  type CommandExample = ((bin: string) => string) | string
370
456
 
371
- class Command {
457
+ class Command<RawName extends string = string, Opts = {}> {
372
458
  options: Option[]
373
459
  aliasNames: string[]
374
460
  /* Parsed command name */
@@ -383,7 +469,7 @@ class Command {
383
469
  _hidden?: boolean
384
470
 
385
471
  constructor(
386
- public rawName: string,
472
+ public rawName: RawName,
387
473
  public description: string,
388
474
  public config: CommandConfig = {},
389
475
  public cli: Goke<any>
@@ -426,7 +512,9 @@ class Command {
426
512
  *
427
513
  * The second argument is either a description string or a StandardJSONSchemaV1
428
514
  * schema. When a schema is provided, description and default are extracted from
429
- * the JSON Schema automatically.
515
+ * the JSON Schema automatically, and the option's type is tracked on the
516
+ * Command's `Opts` type parameter so that subsequent `.action()` callbacks
517
+ * receive a fully-typed options object.
430
518
  *
431
519
  * @example
432
520
  * ```ts
@@ -438,10 +526,16 @@ class Command {
438
526
  * ```
439
527
  */
440
528
  option<
441
- RawName extends string,
529
+ OptionRawName extends string,
442
530
  S extends StandardJSONSchemaV1
443
- >(rawName: RawName, schema: S): Command & { __opts: OptionEntry<RawName, S> }
444
- option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this
531
+ >(
532
+ rawName: OptionRawName,
533
+ schema: S,
534
+ ): Command<RawName, Opts & OptionEntry<OptionRawName, S>>
535
+ option<OptionRawName extends string>(
536
+ rawName: OptionRawName,
537
+ description?: string,
538
+ ): Command<RawName, Opts & UntypedOptionEntry<OptionRawName>>
445
539
  option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
446
540
  const option = new Option(rawName, descriptionOrSchema)
447
541
  this.options.push(option)
@@ -458,7 +552,24 @@ class Command {
458
552
  return this
459
553
  }
460
554
 
461
- action(callback: (...args: any[]) => any) {
555
+ /**
556
+ * Register the action callback that runs when this command is matched.
557
+ *
558
+ * The callback receives positional args extracted from the command's raw name,
559
+ * followed by the parsed options object and the injected GokeExecutionContext.
560
+ *
561
+ * Positional arg types are inferred from the raw name at the type level:
562
+ * `command('convert <input> <output>')` → `(input: string, output: string, options, ctx)`
563
+ * `command('run [script]')` → `(script: string | undefined, options, ctx)`
564
+ * `command('exec [...args]')` → `(args: string[], options, ctx)`
565
+ *
566
+ * The options object is typed according to every `.option()` call chained
567
+ * on this command, plus any global options declared on the parent Goke
568
+ * instance before `.command()` was called.
569
+ */
570
+ action(
571
+ callback: (...args: ActionArgs<RawName, Opts>) => unknown | Promise<unknown>,
572
+ ): this {
462
573
  this.commandAction = callback
463
574
  return this
464
575
  }
@@ -919,14 +1030,14 @@ interface ParsedArgv {
919
1030
  }
920
1031
  }
921
1032
 
922
- class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
1033
+ class Goke<Opts = {}> extends EventEmitter {
923
1034
  /** The program name to display in help and version message */
924
1035
  name: string
925
- commands: Command[]
1036
+ commands: Command<any, any>[]
926
1037
  /** Middleware functions that run before the matched command action, in registration order */
927
1038
  middlewares: Array<{ action: (options: any, context: GokeExecutionContext) => void | Promise<void> }>
928
1039
  globalCommand: GlobalCommand
929
- matchedCommand?: Command
1040
+ matchedCommand?: Command<any, any>
930
1041
  matchedCommandName?: string
931
1042
  /**
932
1043
  * Raw CLI arguments
@@ -1056,10 +1167,24 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
1056
1167
  }
1057
1168
 
1058
1169
  /**
1059
- * Add a sub-command
1170
+ * Add a sub-command.
1171
+ *
1172
+ * The returned Command is parameterized by the literal `rawName` (so positional
1173
+ * args can be inferred at the type level) and by this Goke's accumulated global
1174
+ * `Opts` (so global options declared before `.command()` are visible inside
1175
+ * `.action()` callbacks alongside the command's own options).
1060
1176
  */
1061
- command(rawName: string, description?: string, config?: CommandConfig) {
1062
- const command = new Command(rawName, description || '', config, this)
1177
+ command<CommandRawName extends string>(
1178
+ rawName: CommandRawName,
1179
+ description?: string,
1180
+ config?: CommandConfig,
1181
+ ): Command<CommandRawName, Opts> {
1182
+ const command = new Command<CommandRawName, Opts>(
1183
+ rawName,
1184
+ description || '',
1185
+ config,
1186
+ this,
1187
+ )
1063
1188
  command.globalCommand = this.globalCommand
1064
1189
  this.commands.push(command)
1065
1190
  return command
@@ -1071,13 +1196,21 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
1071
1196
  * Which is also applied to sub-commands.
1072
1197
  *
1073
1198
  * When a StandardJSONSchemaV1 schema is provided, the return type is narrowed
1074
- * to include the inferred option type — enabling type-safe `.use()` callbacks.
1199
+ * to include the inferred option type — enabling type-safe `.use()` callbacks
1200
+ * and typed `options` params inside command `.action()` handlers.
1201
+ *
1202
+ * When a plain description string is provided, the option is still tracked on
1203
+ * the Goke's `Opts` type, but with a loose `string | boolean | undefined` value
1204
+ * type (since no coercion schema is available).
1075
1205
  */
1076
1206
  option<
1077
1207
  RawName extends string,
1078
1208
  S extends StandardJSONSchemaV1
1079
1209
  >(rawName: RawName, schema: S): Goke<Opts & OptionEntry<RawName, S>>
1080
- option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this
1210
+ option<RawName extends string>(
1211
+ rawName: RawName,
1212
+ description?: string,
1213
+ ): Goke<Opts & UntypedOptionEntry<RawName>>
1081
1214
  option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
1082
1215
  const option = new Option(rawName, descriptionOrSchema)
1083
1216
  this.globalCommand.options.push(option)
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Vendored from picocolors by Alexey Raspopov (MIT license).
3
+ * Source: https://github.com/alexeyraspopov/picocolors/blob/main/picocolors.js
4
+ */
5
+
6
+ import { process } from '#runtime'
7
+
8
+ type Formatter = (input: unknown) => string
9
+
10
+ interface PicoColors {
11
+ isColorSupported: boolean
12
+ reset: Formatter
13
+ bold: Formatter
14
+ dim: Formatter
15
+ italic: Formatter
16
+ underline: Formatter
17
+ inverse: Formatter
18
+ hidden: Formatter
19
+ strikethrough: Formatter
20
+ black: Formatter
21
+ red: Formatter
22
+ green: Formatter
23
+ yellow: Formatter
24
+ blue: Formatter
25
+ magenta: Formatter
26
+ cyan: Formatter
27
+ white: Formatter
28
+ gray: Formatter
29
+ bgBlack: Formatter
30
+ bgRed: Formatter
31
+ bgGreen: Formatter
32
+ bgYellow: Formatter
33
+ bgBlue: Formatter
34
+ bgMagenta: Formatter
35
+ bgCyan: Formatter
36
+ bgWhite: Formatter
37
+ blackBright: Formatter
38
+ redBright: Formatter
39
+ greenBright: Formatter
40
+ yellowBright: Formatter
41
+ blueBright: Formatter
42
+ magentaBright: Formatter
43
+ cyanBright: Formatter
44
+ whiteBright: Formatter
45
+ bgBlackBright: Formatter
46
+ bgRedBright: Formatter
47
+ bgGreenBright: Formatter
48
+ bgYellowBright: Formatter
49
+ bgBlueBright: Formatter
50
+ bgMagentaBright: Formatter
51
+ bgCyanBright: Formatter
52
+ bgWhiteBright: Formatter
53
+ }
54
+
55
+ const argv = process.argv || []
56
+ const env = process.env || {}
57
+ const isColorSupported =
58
+ !(!!env.NO_COLOR || argv.includes('--no-color'))
59
+ && (
60
+ !!env.FORCE_COLOR
61
+ || argv.includes('--color')
62
+ || process.platform === 'win32'
63
+ || (process.stdout.isTTY && env.TERM !== 'dumb')
64
+ || !!env.CI
65
+ )
66
+
67
+ const replaceClose = (string: string, close: string, replace: string, index: number) => {
68
+ let result = ''
69
+ let cursor = 0
70
+
71
+ do {
72
+ result += string.substring(cursor, index) + replace
73
+ cursor = index + close.length
74
+ index = string.indexOf(close, cursor)
75
+ } while (~index)
76
+
77
+ return result + string.substring(cursor)
78
+ }
79
+
80
+ const formatter = (open: string, close: string, replace = open): Formatter =>
81
+ (input) => {
82
+ const string = String(input)
83
+ const index = string.indexOf(close, open.length)
84
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close
85
+ }
86
+
87
+ const createColors = (enabled = isColorSupported): PicoColors => {
88
+ const f = enabled ? formatter : () => String
89
+
90
+ return {
91
+ isColorSupported: enabled,
92
+ reset: f('\x1b[0m', '\x1b[0m'),
93
+ bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'),
94
+ dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'),
95
+ italic: f('\x1b[3m', '\x1b[23m'),
96
+ underline: f('\x1b[4m', '\x1b[24m'),
97
+ inverse: f('\x1b[7m', '\x1b[27m'),
98
+ hidden: f('\x1b[8m', '\x1b[28m'),
99
+ strikethrough: f('\x1b[9m', '\x1b[29m'),
100
+ black: f('\x1b[30m', '\x1b[39m'),
101
+ red: f('\x1b[31m', '\x1b[39m'),
102
+ green: f('\x1b[32m', '\x1b[39m'),
103
+ yellow: f('\x1b[33m', '\x1b[39m'),
104
+ blue: f('\x1b[34m', '\x1b[39m'),
105
+ magenta: f('\x1b[35m', '\x1b[39m'),
106
+ cyan: f('\x1b[36m', '\x1b[39m'),
107
+ white: f('\x1b[37m', '\x1b[39m'),
108
+ gray: f('\x1b[90m', '\x1b[39m'),
109
+ bgBlack: f('\x1b[40m', '\x1b[49m'),
110
+ bgRed: f('\x1b[41m', '\x1b[49m'),
111
+ bgGreen: f('\x1b[42m', '\x1b[49m'),
112
+ bgYellow: f('\x1b[43m', '\x1b[49m'),
113
+ bgBlue: f('\x1b[44m', '\x1b[49m'),
114
+ bgMagenta: f('\x1b[45m', '\x1b[49m'),
115
+ bgCyan: f('\x1b[46m', '\x1b[49m'),
116
+ bgWhite: f('\x1b[47m', '\x1b[49m'),
117
+ blackBright: f('\x1b[90m', '\x1b[39m'),
118
+ redBright: f('\x1b[91m', '\x1b[39m'),
119
+ greenBright: f('\x1b[92m', '\x1b[39m'),
120
+ yellowBright: f('\x1b[93m', '\x1b[39m'),
121
+ blueBright: f('\x1b[94m', '\x1b[39m'),
122
+ magentaBright: f('\x1b[95m', '\x1b[39m'),
123
+ cyanBright: f('\x1b[96m', '\x1b[39m'),
124
+ whiteBright: f('\x1b[97m', '\x1b[39m'),
125
+ bgBlackBright: f('\x1b[100m', '\x1b[49m'),
126
+ bgRedBright: f('\x1b[101m', '\x1b[49m'),
127
+ bgGreenBright: f('\x1b[102m', '\x1b[49m'),
128
+ bgYellowBright: f('\x1b[103m', '\x1b[49m'),
129
+ bgBlueBright: f('\x1b[104m', '\x1b[49m'),
130
+ bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
131
+ bgCyanBright: f('\x1b[106m', '\x1b[49m'),
132
+ bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
133
+ }
134
+ }
135
+
136
+ const pc = createColors()
137
+
138
+ export { createColors }
139
+ export type { PicoColors }
140
+ export default pc
@@ -12,7 +12,7 @@ const fs: GokeFs = nodeFs
12
12
 
13
13
  function openInBrowser(url: string): void {
14
14
  if (!process.stdout.isTTY) {
15
- process.stderr.write(url + '\n')
15
+ process.stdout.write(url + '\n')
16
16
  return
17
17
  }
18
18
 
@@ -25,7 +25,7 @@ function openInBrowser(url: string): void {
25
25
  execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' })
26
26
  }
27
27
  } catch {
28
- process.stderr.write(url + '\n')
28
+ process.stdout.write(url + '\n')
29
29
  }
30
30
  }
31
31