goke 6.3.1 → 6.4.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 +104 -32
- package/dist/__test__/index.test.js +23 -0
- package/dist/__test__/just-bash.test.d.ts +5 -0
- package/dist/__test__/just-bash.test.d.ts.map +1 -0
- package/dist/__test__/just-bash.test.js +97 -0
- package/dist/__test__/types.test-d.js +10 -4
- package/dist/goke.d.ts +33 -13
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +113 -41
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/just-bash.d.ts +23 -0
- package/dist/just-bash.d.ts.map +1 -0
- package/dist/just-bash.js +65 -0
- package/dist/runtime-browser.d.ts +31 -0
- package/dist/runtime-browser.d.ts.map +1 -0
- package/dist/runtime-browser.js +47 -0
- package/dist/runtime-node.d.ts +8 -0
- package/dist/runtime-node.d.ts.map +1 -0
- package/dist/runtime-node.js +27 -0
- package/package.json +9 -1
- package/src/__test__/index.test.ts +31 -0
- package/src/__test__/just-bash.test.ts +124 -0
- package/src/__test__/types.test-d.ts +10 -4
- package/src/goke.ts +143 -44
- package/src/index.ts +2 -2
- package/src/just-bash.ts +90 -0
- package/src/runtime-browser.ts +59 -0
- package/src/runtime-node.ts +29 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- **Yet so powerful**. Enable features like default command, git-like subcommands, validation for required arguments and options, variadic arguments, dot-nested options, automated help message generation and so on.
|
|
16
16
|
- **Space-separated subcommands**: Support multi-word commands like `mcp login`, `git remote add`.
|
|
17
17
|
- **Schema-based type coercion**: Use Zod, Valibot, ArkType, or plain JSON Schema for automatic type coercion and TypeScript type inference. Description and default values are extracted from the schema automatically.
|
|
18
|
+
- **Injected execution context**: Prefer `{ console, process }` in actions and middleware for portable output, simpler tests, and alternate runtimes like JustBash.
|
|
18
19
|
- **Type-safe middleware**: Register `.use()` callbacks that run before commands with full type inference from global options.
|
|
19
20
|
- **Developer friendly**. Written in TypeScript.
|
|
20
21
|
|
|
@@ -43,7 +44,7 @@ cli.option(
|
|
|
43
44
|
)
|
|
44
45
|
cli.option('--name <name>', 'Provide your name')
|
|
45
46
|
|
|
46
|
-
cli.command('lint [...files]', 'Lint files').action((files, options) => {
|
|
47
|
+
cli.command('lint [...files]', 'Lint files').action((files, options, { console }) => {
|
|
47
48
|
console.log(files, options)
|
|
48
49
|
})
|
|
49
50
|
|
|
@@ -52,7 +53,7 @@ cli
|
|
|
52
53
|
.option('--minify', 'Minify output')
|
|
53
54
|
.example('build src/index.ts')
|
|
54
55
|
.example('build src/index.ts --minify')
|
|
55
|
-
.action(async (entry, options) => { // options is type safe! no need to type it
|
|
56
|
+
.action(async (entry, options, { console }) => { // options is type safe! no need to type it
|
|
56
57
|
console.log(entry, options)
|
|
57
58
|
})
|
|
58
59
|
|
|
@@ -132,7 +133,7 @@ cli
|
|
|
132
133
|
.option('--channel <name>', 'Target channel: stable, beta, alpha')
|
|
133
134
|
.option('--notes-file <path>', 'Markdown file used as release notes')
|
|
134
135
|
.option('--dry-run', 'Preview every step without publishing')
|
|
135
|
-
.action((version, options) => {
|
|
136
|
+
.action((version, options, { console }) => {
|
|
136
137
|
console.log('release', version, options)
|
|
137
138
|
})
|
|
138
139
|
|
|
@@ -158,7 +159,7 @@ cli
|
|
|
158
159
|
.option('--target <migration>', 'Apply up to a specific migration id')
|
|
159
160
|
.option('--dry-run', 'Print plan only, do not execute SQL')
|
|
160
161
|
.option('--verbose', 'Show each executed statement')
|
|
161
|
-
.action((options) => {
|
|
162
|
+
.action((options, { console }) => {
|
|
162
163
|
console.log('migrate', options)
|
|
163
164
|
})
|
|
164
165
|
|
|
@@ -298,7 +299,7 @@ cli
|
|
|
298
299
|
z.string().default('production').describe('Target environment'),
|
|
299
300
|
)
|
|
300
301
|
.option('--dry-run', 'Preview without deploying')
|
|
301
|
-
.action((options) => {
|
|
302
|
+
.action((options, { console }) => {
|
|
302
303
|
console.log(`Deploying to ${options.env}...`)
|
|
303
304
|
})
|
|
304
305
|
|
|
@@ -306,22 +307,22 @@ cli
|
|
|
306
307
|
cli
|
|
307
308
|
.command('init', 'Initialize a new project')
|
|
308
309
|
.option('--template <template>', 'Project template')
|
|
309
|
-
.action((options) => {
|
|
310
|
+
.action((options, { console }) => {
|
|
310
311
|
console.log('Initializing project...')
|
|
311
312
|
})
|
|
312
313
|
|
|
313
|
-
cli.command('login', 'Authenticate with the server').action(() => {
|
|
314
|
+
cli.command('login', 'Authenticate with the server').action((options, { console }) => {
|
|
314
315
|
console.log('Opening browser for login...')
|
|
315
316
|
})
|
|
316
317
|
|
|
317
|
-
cli.command('logout', 'Clear saved credentials').action(() => {
|
|
318
|
+
cli.command('logout', 'Clear saved credentials').action((options, { console }) => {
|
|
318
319
|
console.log('Logged out')
|
|
319
320
|
})
|
|
320
321
|
|
|
321
322
|
cli
|
|
322
323
|
.command('status', 'Show deployment status')
|
|
323
324
|
.option('--json', 'Output as JSON')
|
|
324
|
-
.action((options) => {
|
|
325
|
+
.action((options, { console }) => {
|
|
325
326
|
console.log('Fetching status...')
|
|
326
327
|
})
|
|
327
328
|
|
|
@@ -329,7 +330,7 @@ cli
|
|
|
329
330
|
.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
330
331
|
.option('--follow', 'Follow log output')
|
|
331
332
|
.option('--lines <n>', z.number().default(100).describe('Number of lines'))
|
|
332
|
-
.action((deploymentId, options) => {
|
|
333
|
+
.action((deploymentId, options, { console }) => {
|
|
333
334
|
console.log(`Streaming logs for ${deploymentId}...`)
|
|
334
335
|
})
|
|
335
336
|
|
|
@@ -351,6 +352,8 @@ deploy --help # shows all commands
|
|
|
351
352
|
|
|
352
353
|
Global options are defined on the CLI instance and apply to all commands. Use `.use()` to register middleware that runs before any command action — useful for reacting to global options like setting up logging, initializing state, or configuring services.
|
|
353
354
|
|
|
355
|
+
Prefer the injected `{ console, process }` argument over global `console` and `process.exit`. It keeps commands easier to test and lets the same command code run inside alternate runtimes like JustBash.
|
|
356
|
+
|
|
354
357
|
Middleware runs in registration order, after option parsing and validation, but before the matched command's `.action()` callback.
|
|
355
358
|
|
|
356
359
|
```ts
|
|
@@ -362,24 +365,24 @@ const cli = goke('mycli')
|
|
|
362
365
|
cli
|
|
363
366
|
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
364
367
|
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
365
|
-
.use((options) => {
|
|
368
|
+
.use((options, { console }) => {
|
|
366
369
|
// options.verbose and options.apiUrl are fully typed here
|
|
367
370
|
if (options.verbose) {
|
|
368
|
-
|
|
371
|
+
console.log('verbose mode enabled')
|
|
369
372
|
}
|
|
370
373
|
})
|
|
371
374
|
|
|
372
375
|
cli
|
|
373
376
|
.command('deploy <env>', 'Deploy to an environment')
|
|
374
377
|
.option('--dry-run', 'Preview without deploying')
|
|
375
|
-
.action((env, options) => {
|
|
378
|
+
.action((env, options, { console }) => {
|
|
376
379
|
// options includes both command options (dryRun) and global options (verbose, apiUrl)
|
|
377
380
|
console.log(`Deploying to ${env} via ${options.apiUrl}`)
|
|
378
381
|
})
|
|
379
382
|
|
|
380
383
|
cli
|
|
381
384
|
.command('status', 'Show deployment status')
|
|
382
|
-
.action((options) => {
|
|
385
|
+
.action((options, { console }) => {
|
|
383
386
|
console.log('Checking status...')
|
|
384
387
|
})
|
|
385
388
|
|
|
@@ -392,14 +395,16 @@ Type safety is positional — each `.use()` callback only sees options declared
|
|
|
392
395
|
```ts
|
|
393
396
|
cli
|
|
394
397
|
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
395
|
-
.use((options) => {
|
|
398
|
+
.use((options, { process }) => {
|
|
396
399
|
options.verbose // boolean — typed
|
|
400
|
+
process.argv // string[] — typed
|
|
397
401
|
options.port // TypeScript error — not declared yet
|
|
398
402
|
})
|
|
399
403
|
.option('--port <port>', z.number().describe('Port'))
|
|
400
|
-
.use((options) => {
|
|
404
|
+
.use((options, { console }) => {
|
|
401
405
|
options.verbose // boolean — still visible
|
|
402
406
|
options.port // number — now visible
|
|
407
|
+
console.error('ready')
|
|
403
408
|
})
|
|
404
409
|
```
|
|
405
410
|
|
|
@@ -408,9 +413,10 @@ Middleware supports async functions. If any middleware is async, the remaining m
|
|
|
408
413
|
```ts
|
|
409
414
|
cli
|
|
410
415
|
.option('--token <token>', z.string().describe('API token'))
|
|
411
|
-
.use(async (options) => {
|
|
416
|
+
.use(async (options, { console }) => {
|
|
412
417
|
const client = await connectToApi(options.token)
|
|
413
418
|
globalState.client = client
|
|
419
|
+
console.log('connected')
|
|
414
420
|
})
|
|
415
421
|
```
|
|
416
422
|
|
|
@@ -426,7 +432,7 @@ const cli = goke()
|
|
|
426
432
|
cli
|
|
427
433
|
.command('rm <dir>', 'Remove a dir')
|
|
428
434
|
.option('-r, --recursive', 'Remove recursively')
|
|
429
|
-
.action((dir, options) => {
|
|
435
|
+
.action((dir, options, { console }) => {
|
|
430
436
|
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''))
|
|
431
437
|
})
|
|
432
438
|
|
|
@@ -444,17 +450,17 @@ import { goke } from 'goke'
|
|
|
444
450
|
|
|
445
451
|
const cli = goke('mycli')
|
|
446
452
|
|
|
447
|
-
cli.command('mcp login <url>', 'Login to MCP server').action((url) => {
|
|
453
|
+
cli.command('mcp login <url>', 'Login to MCP server').action((url, options, { console }) => {
|
|
448
454
|
console.log('Logging in to', url)
|
|
449
455
|
})
|
|
450
456
|
|
|
451
|
-
cli.command('mcp logout', 'Logout from MCP server').action(() => {
|
|
457
|
+
cli.command('mcp logout', 'Logout from MCP server').action((options, { console }) => {
|
|
452
458
|
console.log('Logged out')
|
|
453
459
|
})
|
|
454
460
|
|
|
455
461
|
cli
|
|
456
462
|
.command('git remote add <name> <url>', 'Add a git remote')
|
|
457
|
-
.action((name, url) => {
|
|
463
|
+
.action((name, url, options, { console }) => {
|
|
458
464
|
console.log('Adding remote', name, url)
|
|
459
465
|
})
|
|
460
466
|
|
|
@@ -479,7 +485,7 @@ cli
|
|
|
479
485
|
.option('--workers <workers>', z.int().describe('Worker count'))
|
|
480
486
|
.option('--tags <tag>', z.array(z.string()).describe('Tags (repeatable)'))
|
|
481
487
|
.option('--verbose', 'Verbose output')
|
|
482
|
-
.action((options) => {
|
|
488
|
+
.action((options, { console }) => {
|
|
483
489
|
// options.port is number, options.host is string, etc.
|
|
484
490
|
console.log(options)
|
|
485
491
|
})
|
|
@@ -510,7 +516,7 @@ cli
|
|
|
510
516
|
.option('--old-port <port>', z.number().meta({ deprecated: true, description: 'Use --port instead' }))
|
|
511
517
|
// Current option: visible in help
|
|
512
518
|
.option('--port <port>', z.number().describe('Port number'))
|
|
513
|
-
.action((options) => {
|
|
519
|
+
.action((options, { console }) => {
|
|
514
520
|
const port = options.port ?? options.oldPort
|
|
515
521
|
console.log('Starting on port', port)
|
|
516
522
|
})
|
|
@@ -525,7 +531,9 @@ When users run `--help`, deprecated options won't appear, but `--old-port 3000`
|
|
|
525
531
|
|
|
526
532
|
When using brackets in command name, angled brackets indicate required command arguments, while square brackets indicate optional arguments.
|
|
527
533
|
|
|
528
|
-
When using brackets in option name, angled brackets indicate that a string / number value is required, while square brackets indicate that the value
|
|
534
|
+
When using brackets in option name, angled brackets indicate that a string / number value is required, while square brackets indicate that the value is optional.
|
|
535
|
+
|
|
536
|
+
**Optionality is determined solely by bracket syntax, not by the schema.** `[square brackets]` makes an option optional regardless of whether the schema is `z.string()` or `z.string().optional()`. The schema's `.optional()` is never consulted for this — it only affects type coercion. This means `z.string()` with `[--name]` is treated as optional: if the flag is omitted, `options.name` is `undefined` even though the schema has no `.optional()`.
|
|
529
537
|
|
|
530
538
|
### Negated Options
|
|
531
539
|
|
|
@@ -546,7 +554,7 @@ The last argument of a command can be variadic. To make an argument variadic you
|
|
|
546
554
|
cli
|
|
547
555
|
.command('build <entry> [...otherFiles]', 'Build your app')
|
|
548
556
|
.option('--foo', 'Foo option')
|
|
549
|
-
.action((entry, otherFiles, options) => {
|
|
557
|
+
.action((entry, otherFiles, options, { console }) => {
|
|
550
558
|
console.log(entry)
|
|
551
559
|
console.log(otherFiles)
|
|
552
560
|
console.log(options)
|
|
@@ -604,7 +612,7 @@ cli
|
|
|
604
612
|
.command('build', 'desc')
|
|
605
613
|
.option('--env <env>', 'Set envs')
|
|
606
614
|
.example('--env.API_SECRET xxx')
|
|
607
|
-
.action((options) => {
|
|
615
|
+
.action((options, { console }) => {
|
|
608
616
|
console.log(options)
|
|
609
617
|
})
|
|
610
618
|
```
|
|
@@ -617,7 +625,7 @@ Register a command that will be used when no other command is matched.
|
|
|
617
625
|
cli
|
|
618
626
|
.command('[...files]', 'Build files')
|
|
619
627
|
.option('--minimize', 'Minimize output')
|
|
620
|
-
.action((files, options) => {
|
|
628
|
+
.action((files, options, { console }) => {
|
|
621
629
|
console.log(files)
|
|
622
630
|
console.log(options.minimize)
|
|
623
631
|
})
|
|
@@ -632,11 +640,46 @@ try {
|
|
|
632
640
|
cli.parse(process.argv, { run: false })
|
|
633
641
|
await cli.runMatchedCommand()
|
|
634
642
|
} catch (error) {
|
|
635
|
-
|
|
643
|
+
const message = error instanceof Error ? error.stack : String(error)
|
|
644
|
+
process.stderr.write(String(message) + '\n')
|
|
636
645
|
process.exit(1)
|
|
637
646
|
}
|
|
638
647
|
```
|
|
639
648
|
|
|
649
|
+
### Testing with mocked console and exit
|
|
650
|
+
|
|
651
|
+
Because goke derives its injected `{ console, process }` from the CLI's configured output streams and `exit` function, tests can override them directly and assert on the calls.
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
655
|
+
import { goke, GokeProcessExit } from 'goke'
|
|
656
|
+
|
|
657
|
+
describe('deploy command', () => {
|
|
658
|
+
test('writes output and exits with injected mocks', () => {
|
|
659
|
+
const stdout = { write: vi.fn<(data: string) => void>() }
|
|
660
|
+
const stderr = { write: vi.fn<(data: string) => void>() }
|
|
661
|
+
const exit = vi.fn<(code: number) => void>()
|
|
662
|
+
|
|
663
|
+
const cli = goke('acme', { stdout, stderr, exit })
|
|
664
|
+
|
|
665
|
+
cli
|
|
666
|
+
.command('deploy', 'Deploy the project')
|
|
667
|
+
.action((options, { console, process }) => {
|
|
668
|
+
console.log('deploying')
|
|
669
|
+
process.exit(2)
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
expect(() => {
|
|
673
|
+
cli.parse(['node', 'acme', 'deploy'], { run: true })
|
|
674
|
+
}).toThrow(GokeProcessExit)
|
|
675
|
+
|
|
676
|
+
expect(stdout.write).toHaveBeenCalledWith('deploying\n')
|
|
677
|
+
expect(exit).toHaveBeenCalledWith(2)
|
|
678
|
+
expect(stderr.write).not.toHaveBeenCalled()
|
|
679
|
+
})
|
|
680
|
+
})
|
|
681
|
+
```
|
|
682
|
+
|
|
640
683
|
### With TypeScript
|
|
641
684
|
|
|
642
685
|
```ts
|
|
@@ -657,7 +700,7 @@ cli
|
|
|
657
700
|
.command('serve <entry>', 'Start the app')
|
|
658
701
|
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
659
702
|
.option('--watch', 'Watch files')
|
|
660
|
-
.action((entry, options) => {
|
|
703
|
+
.action((entry, options, { console }) => {
|
|
661
704
|
// entry: string
|
|
662
705
|
// options.port: number
|
|
663
706
|
// options.watch: boolean
|
|
@@ -675,6 +718,33 @@ import { openInBrowser } from 'goke'
|
|
|
675
718
|
openInBrowser('https://example.com/dashboard')
|
|
676
719
|
```
|
|
677
720
|
|
|
721
|
+
### Expose a goke CLI to JustBash
|
|
722
|
+
|
|
723
|
+
Use `cli.createJustBashCommand()` to expose a goke CLI as a single JustBash custom command. JustBash command names are single-token executables, but the goke CLI behind them can still use multi-word subcommands like `child commandwithspaces`.
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
import { goke } from 'goke'
|
|
727
|
+
import { z } from 'zod'
|
|
728
|
+
import { Bash } from 'just-bash'
|
|
729
|
+
|
|
730
|
+
const cli = goke('parent')
|
|
731
|
+
|
|
732
|
+
cli
|
|
733
|
+
.command('child commandwithspaces', 'Run nested command')
|
|
734
|
+
.option('--name <name>', z.string().describe('Name'))
|
|
735
|
+
.action((options, { console }) => {
|
|
736
|
+
console.log(`hello ${options.name}`)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
const bash = new Bash({
|
|
740
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
await bash.exec('parent child commandwithspaces --name Tommy')
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Prefer the injected `{ console, process }` helpers in command implementations so the same command code works cleanly both in the regular CLI runtime and through the JustBash bridge.
|
|
747
|
+
|
|
678
748
|
## References
|
|
679
749
|
|
|
680
750
|
### CLI Instance
|
|
@@ -710,9 +780,9 @@ Add a global option. The second argument is either:
|
|
|
710
780
|
|
|
711
781
|
#### cli.use(callback)
|
|
712
782
|
|
|
713
|
-
- Type: `(callback: (options: Opts) => void | Promise<void>) => CLI`
|
|
783
|
+
- Type: `(callback: (options: Opts, { console, process }) => void | Promise<void>) => CLI`
|
|
714
784
|
|
|
715
|
-
Register a middleware function that runs before the matched command action. Middleware runs in registration order, after option parsing and validation. The callback receives the parsed global options, typed according to all `.option()` calls that precede the `.use()` in the chain.
|
|
785
|
+
Register a middleware function that runs before the matched command action. Middleware runs in registration order, after option parsing and validation. The callback receives the parsed global options, typed according to all `.option()` calls that precede the `.use()` in the chain, plus an injected `{ console, process }` helper object.
|
|
716
786
|
|
|
717
787
|
#### cli.parse(argv?)
|
|
718
788
|
|
|
@@ -766,6 +836,8 @@ Basically the same as `cli.option` but this adds the option to specific command.
|
|
|
766
836
|
|
|
767
837
|
- Type: `(callback: ActionCallback) => Command`
|
|
768
838
|
|
|
839
|
+
Command callbacks receive positional args first, then parsed options, then an injected `{ console, process }` object. Prefer those injected helpers over global `console` and `process.exit` so commands stay easier to test and can run inside alternate runtimes like JustBash.
|
|
840
|
+
|
|
769
841
|
#### command.alias(name)
|
|
770
842
|
|
|
771
843
|
- Type: `(name: string) => Command`
|
|
@@ -796,7 +868,7 @@ cli.on('command:!', () => {
|
|
|
796
868
|
})
|
|
797
869
|
|
|
798
870
|
cli.on('command:*', () => {
|
|
799
|
-
|
|
871
|
+
process.stderr.write(`Invalid command: ${cli.args.join(' ')}\n`)
|
|
800
872
|
process.exit(1)
|
|
801
873
|
})
|
|
802
874
|
```
|
|
@@ -1436,6 +1436,29 @@ describe('schema description and default extraction', () => {
|
|
|
1436
1436
|
expect(stdout.text).not.toContain('--legacy');
|
|
1437
1437
|
expect(stdout.text).not.toContain('Deprecated global');
|
|
1438
1438
|
});
|
|
1439
|
+
test('hidden commands are not shown in help output', () => {
|
|
1440
|
+
const stdout = createTestOutputStream();
|
|
1441
|
+
const cli = goke('mycli', { stdout });
|
|
1442
|
+
cli.command('visible', 'A visible command');
|
|
1443
|
+
cli.command('secret', 'A hidden command').hidden();
|
|
1444
|
+
cli.help();
|
|
1445
|
+
cli.parse(['node', 'bin', '--help'], { run: false });
|
|
1446
|
+
expect(stdout.text).toContain('visible');
|
|
1447
|
+
expect(stdout.text).toContain('A visible command');
|
|
1448
|
+
expect(stdout.text).not.toContain('secret');
|
|
1449
|
+
expect(stdout.text).not.toContain('A hidden command');
|
|
1450
|
+
});
|
|
1451
|
+
test('hidden command still parses and runs', () => {
|
|
1452
|
+
const cli = gokeTestable('mycli');
|
|
1453
|
+
let result = {};
|
|
1454
|
+
cli
|
|
1455
|
+
.command('secret', 'A hidden command')
|
|
1456
|
+
.hidden()
|
|
1457
|
+
.option('--value <v>', z.string().describe('some value'))
|
|
1458
|
+
.action((options) => { result = options; });
|
|
1459
|
+
cli.parse(['node', 'bin', 'secret', '--value', 'hello']);
|
|
1460
|
+
expect(result.value).toBe('hello');
|
|
1461
|
+
});
|
|
1439
1462
|
});
|
|
1440
1463
|
describe('helpText()', () => {
|
|
1441
1464
|
test('returns help string without printing', () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"just-bash.test.d.ts","sourceRoot":"","sources":["../../src/__test__/just-bash.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for injected execution context, clone isolation, and the JustBash bridge.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import goke from '../index.js';
|
|
7
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
8
|
+
const stripAnsi = (text) => text.replace(ANSI_RE, '');
|
|
9
|
+
function createTestOutputStream() {
|
|
10
|
+
const lines = [];
|
|
11
|
+
return {
|
|
12
|
+
lines,
|
|
13
|
+
get text() { return stripAnsi(lines.join('')); },
|
|
14
|
+
write(data) { lines.push(data); },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function gokeTestable(name = '', options) {
|
|
18
|
+
return goke(name, {
|
|
19
|
+
...options,
|
|
20
|
+
exit: () => { },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
describe('injected execution context', () => {
|
|
24
|
+
test('command action receives injected console and process', () => {
|
|
25
|
+
const stdout = createTestOutputStream();
|
|
26
|
+
const cli = gokeTestable('mycli', { stdout });
|
|
27
|
+
let seenArgv;
|
|
28
|
+
cli
|
|
29
|
+
.command('status', 'Show status')
|
|
30
|
+
.action((options, { console, process }) => {
|
|
31
|
+
console.log('ready');
|
|
32
|
+
seenArgv = process.argv;
|
|
33
|
+
});
|
|
34
|
+
cli.parse(['node', 'bin', 'status'], { run: true });
|
|
35
|
+
expect(stdout.text).toBe('ready\n');
|
|
36
|
+
expect(seenArgv).toEqual(['node', 'bin', 'status']);
|
|
37
|
+
});
|
|
38
|
+
test('middleware receives injected console and process', () => {
|
|
39
|
+
const stdout = createTestOutputStream();
|
|
40
|
+
const cli = gokeTestable('mycli', { stdout });
|
|
41
|
+
let seenArgv;
|
|
42
|
+
cli
|
|
43
|
+
.use((options, { console, process }) => {
|
|
44
|
+
console.log('middleware');
|
|
45
|
+
seenArgv = process.argv;
|
|
46
|
+
})
|
|
47
|
+
.command('build', 'Build')
|
|
48
|
+
.action(() => { });
|
|
49
|
+
cli.parse(['node', 'bin', 'build'], { run: true });
|
|
50
|
+
expect(stdout.text).toBe('middleware\n');
|
|
51
|
+
expect(seenArgv).toEqual(['node', 'bin', 'build']);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('clone', () => {
|
|
55
|
+
test('clone creates isolated parse state', () => {
|
|
56
|
+
const cli = gokeTestable('mycli');
|
|
57
|
+
cli.command('build', 'Build').action(() => { });
|
|
58
|
+
const cloned = cli.clone({ exit: () => { } });
|
|
59
|
+
cloned.parse(['node', 'bin', 'build'], { run: false });
|
|
60
|
+
expect(cloned).not.toBe(cli);
|
|
61
|
+
expect(cloned.matchedCommandName).toBe('build');
|
|
62
|
+
expect(cli.matchedCommandName).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('createJustBashCommand', () => {
|
|
66
|
+
test('runs multi-word goke subcommands through one just-bash command', async () => {
|
|
67
|
+
const cli = gokeTestable('parent');
|
|
68
|
+
cli
|
|
69
|
+
.command('child commandwithspaces', 'Run nested command')
|
|
70
|
+
.option('--name <name>', z.string().describe('Name'))
|
|
71
|
+
.action((options, { console }) => {
|
|
72
|
+
console.log(`hello ${options.name}`);
|
|
73
|
+
});
|
|
74
|
+
const customCommand = await cli.createJustBashCommand();
|
|
75
|
+
const result = await customCommand.execute(['child', 'commandwithspaces', '--name', 'Tommy']);
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
stdout: 'hello Tommy\n',
|
|
78
|
+
stderr: '',
|
|
79
|
+
exitCode: 0,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
test('maps injected process.exit to a command exit code', async () => {
|
|
83
|
+
const cli = gokeTestable('parent');
|
|
84
|
+
cli
|
|
85
|
+
.command('fail', 'Exit with custom code')
|
|
86
|
+
.action((options, { process }) => {
|
|
87
|
+
process.exit(7);
|
|
88
|
+
});
|
|
89
|
+
const customCommand = await cli.createJustBashCommand();
|
|
90
|
+
const result = await customCommand.execute(['fail']);
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
stdout: '',
|
|
93
|
+
stderr: '',
|
|
94
|
+
exitCode: 7,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -73,9 +73,12 @@ describe('type-level: middleware use() callback inference', () => {
|
|
|
73
73
|
goke('test')
|
|
74
74
|
.option('--port <port>', schema1)
|
|
75
75
|
.option('--host <host>', schema2)
|
|
76
|
-
.use((options) => {
|
|
76
|
+
.use((options, { console, process }) => {
|
|
77
77
|
expectTypeOf(options.port).toEqualTypeOf();
|
|
78
78
|
expectTypeOf(options.host).toEqualTypeOf();
|
|
79
|
+
expectTypeOf(process.argv).toEqualTypeOf();
|
|
80
|
+
expectTypeOf(process.stdout.write).toEqualTypeOf();
|
|
81
|
+
expectTypeOf(console.log).toBeFunction();
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
84
|
test('use() only sees options declared before it', () => {
|
|
@@ -83,23 +86,26 @@ describe('type-level: middleware use() callback inference', () => {
|
|
|
83
86
|
const schema2 = {};
|
|
84
87
|
goke('test')
|
|
85
88
|
.option('--verbose', schema1)
|
|
86
|
-
.use((options) => {
|
|
89
|
+
.use((options, { process }) => {
|
|
87
90
|
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
91
|
+
expectTypeOf(process.exit).toEqualTypeOf();
|
|
88
92
|
// @ts-expect-error port is not declared yet
|
|
89
93
|
options.port;
|
|
90
94
|
})
|
|
91
95
|
.option('--port <port>', schema2)
|
|
92
|
-
.use((options) => {
|
|
96
|
+
.use((options, { console }) => {
|
|
93
97
|
// Now both are visible
|
|
94
98
|
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
95
99
|
expectTypeOf(options.port).toEqualTypeOf();
|
|
100
|
+
expectTypeOf(console.error).toBeFunction();
|
|
96
101
|
});
|
|
97
102
|
});
|
|
98
103
|
test('accessing a non-existent option is a type error', () => {
|
|
99
104
|
const schema = {};
|
|
100
105
|
goke('test')
|
|
101
106
|
.option('--port <port>', schema)
|
|
102
|
-
.use((options) => {
|
|
107
|
+
.use((options, { process }) => {
|
|
108
|
+
expectTypeOf(process.stderr.write).toEqualTypeOf();
|
|
103
109
|
// @ts-expect-error nonExistent was never defined
|
|
104
110
|
options.nonExistent;
|
|
105
111
|
});
|
package/dist/goke.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - createConsole: factory for console-like objects from output streams
|
|
10
10
|
* - Utility functions: string helpers, bracket parsing, dot-prop access
|
|
11
11
|
*/
|
|
12
|
-
import { EventEmitter } from 'events';
|
|
13
12
|
import type { StandardJSONSchemaV1 } from "./coerce.js";
|
|
13
|
+
import { EventEmitter, openInBrowser } from '#runtime';
|
|
14
14
|
declare class Option {
|
|
15
15
|
rawName: string;
|
|
16
16
|
/** Option name */
|
|
@@ -34,6 +34,7 @@ declare class Option {
|
|
|
34
34
|
* When a schema is provided, description and default are extracted from the JSON Schema.
|
|
35
35
|
*/
|
|
36
36
|
constructor(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1);
|
|
37
|
+
clone(): Option;
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
40
|
* Converts a kebab-case string to camelCase at the type level.
|
|
@@ -101,6 +102,7 @@ declare class Command {
|
|
|
101
102
|
examples: CommandExample[];
|
|
102
103
|
helpCallback?: HelpCallback;
|
|
103
104
|
globalCommand?: GlobalCommand;
|
|
105
|
+
_hidden?: boolean;
|
|
104
106
|
constructor(rawName: string, description: string, config: CommandConfig | undefined, cli: Goke<any>);
|
|
105
107
|
usage(text: string): this;
|
|
106
108
|
allowUnknownOptions(): this;
|
|
@@ -128,6 +130,7 @@ declare class Command {
|
|
|
128
130
|
};
|
|
129
131
|
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this;
|
|
130
132
|
alias(name: string): this;
|
|
133
|
+
hidden(): this;
|
|
131
134
|
action(callback: (...args: any[]) => any): this;
|
|
132
135
|
isMatched(args: string[]): {
|
|
133
136
|
matched: boolean;
|
|
@@ -177,6 +180,22 @@ interface GokeOutputStream {
|
|
|
177
180
|
interface GokeConsole {
|
|
178
181
|
log(...args: unknown[]): void;
|
|
179
182
|
error(...args: unknown[]): void;
|
|
183
|
+
warn(...args: unknown[]): void;
|
|
184
|
+
info(...args: unknown[]): void;
|
|
185
|
+
}
|
|
186
|
+
interface GokeProcess {
|
|
187
|
+
argv: string[];
|
|
188
|
+
stdout: GokeOutputStream;
|
|
189
|
+
stderr: GokeOutputStream;
|
|
190
|
+
exit(code: number): never | void;
|
|
191
|
+
}
|
|
192
|
+
interface GokeExecutionContext {
|
|
193
|
+
console: GokeConsole;
|
|
194
|
+
process: GokeProcess;
|
|
195
|
+
}
|
|
196
|
+
declare class GokeProcessExit extends Error {
|
|
197
|
+
code: number;
|
|
198
|
+
constructor(code: number);
|
|
180
199
|
}
|
|
181
200
|
/**
|
|
182
201
|
* Options for configuring a Goke CLI instance.
|
|
@@ -217,7 +236,7 @@ declare class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
|
217
236
|
commands: Command[];
|
|
218
237
|
/** Middleware functions that run before the matched command action, in registration order */
|
|
219
238
|
middlewares: Array<{
|
|
220
|
-
action: (options: any) => void | Promise<void>;
|
|
239
|
+
action: (options: any, context: GokeExecutionContext) => void | Promise<void>;
|
|
221
240
|
}>;
|
|
222
241
|
globalCommand: GlobalCommand;
|
|
223
242
|
matchedCommand?: Command;
|
|
@@ -251,6 +270,11 @@ declare class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
|
251
270
|
* @param options Configuration for stdout, stderr, and argv
|
|
252
271
|
*/
|
|
253
272
|
constructor(name?: string, options?: GokeOptions);
|
|
273
|
+
clone(options?: GokeOptions): Goke<Opts>;
|
|
274
|
+
private createExecutionContext;
|
|
275
|
+
createJustBashCommand(options?: {
|
|
276
|
+
name?: string;
|
|
277
|
+
}): Promise<import("./just-bash.js").JustBashCommand>;
|
|
254
278
|
/**
|
|
255
279
|
* Add a global usage text.
|
|
256
280
|
*
|
|
@@ -279,20 +303,21 @@ declare class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
|
279
303
|
* options (e.g. setting up logging, initializing state).
|
|
280
304
|
*
|
|
281
305
|
* The callback receives the parsed options object, typed according to all
|
|
282
|
-
* `.option()` calls that precede this `.use()` in the chain
|
|
306
|
+
* `.option()` calls that precede this `.use()` in the chain, plus an injected
|
|
307
|
+
* execution context with `{ console, process }` for portable output and exits.
|
|
283
308
|
*
|
|
284
309
|
* @example
|
|
285
310
|
* ```ts
|
|
286
311
|
* cli
|
|
287
312
|
* .option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
288
|
-
* .use((options) => {
|
|
313
|
+
* .use((options, { console }) => {
|
|
289
314
|
* if (options.verbose) {
|
|
290
|
-
*
|
|
315
|
+
* console.log('verbose mode enabled')
|
|
291
316
|
* }
|
|
292
317
|
* })
|
|
293
318
|
* ```
|
|
294
319
|
*/
|
|
295
|
-
use(callback: (options: Opts) => void | Promise<void>): this;
|
|
320
|
+
use(callback: (options: Opts, context: GokeExecutionContext) => void | Promise<void>): this;
|
|
296
321
|
/**
|
|
297
322
|
* Show help message when `-h, --help` flags appear.
|
|
298
323
|
*
|
|
@@ -349,12 +374,7 @@ declare class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
|
349
374
|
private mri;
|
|
350
375
|
runMatchedCommand(): any;
|
|
351
376
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
* In non-TTY environments (CI, piped output, agents), prints the URL to stdout instead.
|
|
355
|
-
*/
|
|
356
|
-
declare function openInBrowser(url: string): void;
|
|
357
|
-
export type { GokeOutputStream, GokeConsole, GokeOptions };
|
|
358
|
-
export { createConsole, Command, openInBrowser };
|
|
377
|
+
export type { GokeOutputStream, GokeConsole, GokeOptions, GokeProcess, GokeExecutionContext };
|
|
378
|
+
export { createConsole, Command, GokeProcessExit, openInBrowser };
|
|
359
379
|
export default Goke;
|
|
360
380
|
//# sourceMappingURL=goke.d.ts.map
|