goke 6.5.1 → 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/README.md +39 -9
- package/dist/__test__/readme-examples.test.d.ts +5 -0
- package/dist/__test__/readme-examples.test.d.ts.map +1 -0
- package/dist/__test__/readme-examples.test.js +169 -0
- package/dist/__test__/types.test-d.js +238 -1
- package/dist/goke.d.ts +105 -16
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +21 -1
- package/dist/runtime-node.js +2 -2
- package/package.json +2 -2
- package/src/__test__/readme-examples.test.ts +225 -0
- package/src/__test__/types.test-d.ts +262 -1
- package/src/goke.ts +148 -15
- package/src/runtime-node.ts +2 -2
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type-level tests for schema-based option inference.
|
|
3
3
|
* These tests verify that TypeScript infers the correct types from
|
|
4
|
-
* option names (template literals) and StandardJSONSchemaV1 schemas
|
|
4
|
+
* option names (template literals) and StandardJSONSchemaV1 schemas,
|
|
5
|
+
* and that `.action()` callbacks receive fully-typed positional args
|
|
6
|
+
* and options objects.
|
|
5
7
|
*
|
|
6
8
|
* These use expectTypeOf from vitest for compile-time type assertions.
|
|
7
9
|
*/
|
|
8
10
|
import { describe, test, expectTypeOf } from 'vitest'
|
|
11
|
+
import { z } from 'zod'
|
|
9
12
|
import type { StandardTypedV1, StandardJSONSchemaV1 } from '../coerce.js'
|
|
13
|
+
import type { GokeExecutionContext } from '../goke.js'
|
|
10
14
|
import goke from '../index.js'
|
|
11
15
|
|
|
12
16
|
// ─── Import type helpers from Command.ts ───
|
|
@@ -167,3 +171,260 @@ describe('type-level: middleware use() callback inference', () => {
|
|
|
167
171
|
})
|
|
168
172
|
})
|
|
169
173
|
})
|
|
174
|
+
|
|
175
|
+
describe('type-level: command() .action() positional args inference', () => {
|
|
176
|
+
test('command with no args → action receives only (options, ctx)', () => {
|
|
177
|
+
goke('test')
|
|
178
|
+
.command('deploy', 'Deploy the app')
|
|
179
|
+
.action((options, ctx) => {
|
|
180
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
181
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('command with one required arg → action receives (arg, options, ctx)', () => {
|
|
186
|
+
goke('test')
|
|
187
|
+
.command('get <id>', 'Fetch a resource by id')
|
|
188
|
+
.action((id, options, ctx) => {
|
|
189
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
190
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
191
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('command with two required args → action receives both as strings', () => {
|
|
196
|
+
goke('test')
|
|
197
|
+
.command('convert <input> <output>', 'Convert file formats')
|
|
198
|
+
.action((input, output, options) => {
|
|
199
|
+
expectTypeOf(input).toEqualTypeOf<string>()
|
|
200
|
+
expectTypeOf(output).toEqualTypeOf<string>()
|
|
201
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('command with optional arg → arg type includes undefined', () => {
|
|
206
|
+
goke('test')
|
|
207
|
+
.command('run [script]', 'Run a script')
|
|
208
|
+
.action((script, options) => {
|
|
209
|
+
expectTypeOf(script).toEqualTypeOf<string | undefined>()
|
|
210
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('command with variadic required arg → arg is string[]', () => {
|
|
215
|
+
goke('test')
|
|
216
|
+
.command('exec <...args>', 'Run a binary with args')
|
|
217
|
+
.action((args, options) => {
|
|
218
|
+
expectTypeOf(args).toEqualTypeOf<string[]>()
|
|
219
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('command with variadic optional arg → arg is string[]', () => {
|
|
224
|
+
goke('test')
|
|
225
|
+
.command('run [...rest]', 'Variadic optional')
|
|
226
|
+
.action((rest, options) => {
|
|
227
|
+
expectTypeOf(rest).toEqualTypeOf<string[]>()
|
|
228
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('multi-word command with required arg', () => {
|
|
233
|
+
goke('test')
|
|
234
|
+
.command('mcp getNodeXml <id>', 'Get XML for a node')
|
|
235
|
+
.action((id, options) => {
|
|
236
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
237
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('default command with one positional arg', () => {
|
|
242
|
+
goke('test')
|
|
243
|
+
.command('<file>', 'Default command')
|
|
244
|
+
.action((file, options) => {
|
|
245
|
+
expectTypeOf(file).toEqualTypeOf<string>()
|
|
246
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('mixed required and optional positional args', () => {
|
|
251
|
+
goke('test')
|
|
252
|
+
.command('send <to> [cc]', 'Send a message')
|
|
253
|
+
.action((to, cc, options) => {
|
|
254
|
+
expectTypeOf(to).toEqualTypeOf<string>()
|
|
255
|
+
expectTypeOf(cc).toEqualTypeOf<string | undefined>()
|
|
256
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('type-level: command() .action() option inference', () => {
|
|
262
|
+
test('single schema-based option is visible on options param', () => {
|
|
263
|
+
goke('test')
|
|
264
|
+
.command('serve', 'Start server')
|
|
265
|
+
.option('--port <port>', z.number())
|
|
266
|
+
.action((options, ctx) => {
|
|
267
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
268
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('multiple schema-based options are accumulated', () => {
|
|
273
|
+
goke('test')
|
|
274
|
+
.command('serve', 'Start server')
|
|
275
|
+
.option('--port <port>', z.number())
|
|
276
|
+
.option('--host <host>', z.string())
|
|
277
|
+
.option('--verbose', z.boolean())
|
|
278
|
+
.action((options) => {
|
|
279
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
280
|
+
expectTypeOf(options.host).toEqualTypeOf<string>()
|
|
281
|
+
// Boolean flag is optional (no <...> brackets)
|
|
282
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('required vs optional option shape', () => {
|
|
287
|
+
goke('test')
|
|
288
|
+
.command('cmd', 'Command')
|
|
289
|
+
.option('--name <name>', z.string())
|
|
290
|
+
.option('--count [count]', z.number())
|
|
291
|
+
.action((options) => {
|
|
292
|
+
expectTypeOf(options.name).toEqualTypeOf<string>()
|
|
293
|
+
expectTypeOf(options.count).toEqualTypeOf<number | undefined>()
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('camelCase conversion for kebab-case option names', () => {
|
|
298
|
+
goke('test')
|
|
299
|
+
.command('build', 'Build')
|
|
300
|
+
.option('--out-dir <dir>', z.string())
|
|
301
|
+
.option('--my-long-flag <val>', z.string())
|
|
302
|
+
.action((options) => {
|
|
303
|
+
expectTypeOf(options.outDir).toEqualTypeOf<string>()
|
|
304
|
+
expectTypeOf(options.myLongFlag).toEqualTypeOf<string>()
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('options combined with positional args', () => {
|
|
309
|
+
goke('test')
|
|
310
|
+
.command('convert <input> <output>', 'Convert file format')
|
|
311
|
+
.option('--quality <quality>', z.number())
|
|
312
|
+
.option('--format <format>', z.enum(['png', 'jpg', 'webp']))
|
|
313
|
+
.action((input, output, options, ctx) => {
|
|
314
|
+
expectTypeOf(input).toEqualTypeOf<string>()
|
|
315
|
+
expectTypeOf(output).toEqualTypeOf<string>()
|
|
316
|
+
expectTypeOf(options.quality).toEqualTypeOf<number>()
|
|
317
|
+
expectTypeOf(options.format).toEqualTypeOf<'png' | 'jpg' | 'webp'>()
|
|
318
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('global options from Goke are visible inside command actions', () => {
|
|
323
|
+
goke('test')
|
|
324
|
+
.option('--verbose', z.boolean())
|
|
325
|
+
.command('serve', 'Start server')
|
|
326
|
+
.option('--port <port>', z.number())
|
|
327
|
+
.action((options) => {
|
|
328
|
+
// Global option from cli.option()
|
|
329
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
330
|
+
// Command-local option
|
|
331
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('untyped option (string description) produces loose value type', () => {
|
|
336
|
+
goke('test')
|
|
337
|
+
.command('serve', 'Start server')
|
|
338
|
+
.option('--port <port>', 'Port number')
|
|
339
|
+
.action((options) => {
|
|
340
|
+
// Without a schema the runtime still guarantees required value options are strings.
|
|
341
|
+
expectTypeOf(options.port).toEqualTypeOf<string>()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('untyped optional value options keep raw mri sentinel shapes', () => {
|
|
346
|
+
goke('test')
|
|
347
|
+
.command('serve', 'Start server')
|
|
348
|
+
.option('--host [host]', 'Optional host override')
|
|
349
|
+
.option('--verbose', 'Verbose output')
|
|
350
|
+
.action((options) => {
|
|
351
|
+
expectTypeOf(options.host).toEqualTypeOf<string | boolean | undefined>()
|
|
352
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('accessing a non-existent option in action is a type error', () => {
|
|
357
|
+
goke('test')
|
|
358
|
+
.command('serve', 'Start server')
|
|
359
|
+
.option('--port <port>', z.number())
|
|
360
|
+
.action((options) => {
|
|
361
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
362
|
+
// @ts-expect-error nonExistent was never declared
|
|
363
|
+
options.nonExistent
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('accessing a non-existent positional arg in action is a type error', () => {
|
|
368
|
+
goke('test')
|
|
369
|
+
.command('get <id>', 'Fetch resource')
|
|
370
|
+
.action((id, options, ctx, ...rest) => {
|
|
371
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
372
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
373
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
374
|
+
// No more positional slots — rest should be empty
|
|
375
|
+
expectTypeOf(rest).toEqualTypeOf<[]>()
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('action callback can omit trailing params (fewer-args is valid)', () => {
|
|
380
|
+
// Dropping context is fine
|
|
381
|
+
goke('test')
|
|
382
|
+
.command('serve', 'Start server')
|
|
383
|
+
.option('--port <port>', z.number())
|
|
384
|
+
.action((options) => {
|
|
385
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Dropping everything is fine
|
|
389
|
+
goke('test')
|
|
390
|
+
.command('serve', 'Start server')
|
|
391
|
+
.option('--port <port>', z.number())
|
|
392
|
+
.action(() => {})
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
describe('type-level: README TypeScript examples', () => {
|
|
397
|
+
test('README TypeScript example infers positional args and typed options', () => {
|
|
398
|
+
goke('my-program')
|
|
399
|
+
.command('serve <entry>', 'Start the app')
|
|
400
|
+
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
401
|
+
.option('--watch', 'Watch files')
|
|
402
|
+
.action((entry, options, { console, process }) => {
|
|
403
|
+
expectTypeOf(entry).toEqualTypeOf<string>()
|
|
404
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
405
|
+
expectTypeOf(options.watch).toEqualTypeOf<boolean | undefined>()
|
|
406
|
+
expectTypeOf(console.log).toBeFunction()
|
|
407
|
+
expectTypeOf(process.cwd).toEqualTypeOf<string>()
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('README global options and middleware example stays typed end-to-end', () => {
|
|
412
|
+
goke('mycli')
|
|
413
|
+
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
414
|
+
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
415
|
+
.use((options, { process }) => {
|
|
416
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
417
|
+
expectTypeOf(options.apiUrl).toEqualTypeOf<string | undefined>()
|
|
418
|
+
expectTypeOf(process.stdin).toEqualTypeOf<string>()
|
|
419
|
+
})
|
|
420
|
+
.command('deploy <env>', 'Deploy to an environment')
|
|
421
|
+
.option('--dry-run', 'Preview without deploying')
|
|
422
|
+
.action((env, options, ctx) => {
|
|
423
|
+
expectTypeOf(env).toEqualTypeOf<string>()
|
|
424
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
425
|
+
expectTypeOf(options.apiUrl).toEqualTypeOf<string | undefined>()
|
|
426
|
+
expectTypeOf(options.dryRun).toEqualTypeOf<boolean | undefined>()
|
|
427
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
})
|
package/src/goke.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
529
|
+
OptionRawName extends string,
|
|
442
530
|
S extends StandardJSONSchemaV1
|
|
443
|
-
>(
|
|
444
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1062
|
-
|
|
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
|
|
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)
|
package/src/runtime-node.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
28
|
+
process.stdout.write(url + '\n')
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|