goke 6.4.0 → 6.5.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 +124 -59
- package/dist/__test__/index.test.js +80 -0
- package/dist/__test__/just-bash.test.js +128 -0
- package/dist/__test__/types.test-d.js +9 -3
- package/dist/goke-fs.d.ts +25 -0
- package/dist/goke-fs.d.ts.map +1 -0
- package/dist/goke-fs.js +4 -0
- package/dist/goke.d.ts +22 -1
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +24 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/just-bash.d.ts +3 -1
- package/dist/just-bash.d.ts.map +1 -1
- package/dist/just-bash.js +163 -1
- package/dist/runtime-browser.d.ts +5 -1
- package/dist/runtime-browser.d.ts.map +1 -1
- package/dist/runtime-browser.js +28 -1
- package/dist/runtime-node.d.ts +3 -1
- package/dist/runtime-node.d.ts.map +1 -1
- package/dist/runtime-node.js +3 -1
- package/package.json +2 -1
- package/src/__test__/index.test.ts +93 -0
- package/src/__test__/just-bash.test.ts +171 -3
- package/src/__test__/types.test-d.ts +9 -3
- package/src/goke-fs.ts +26 -0
- package/src/goke.ts +39 -4
- package/src/index.ts +1 -1
- package/src/just-bash.ts +187 -2
- package/src/runtime-browser.ts +35 -1
- package/src/runtime-node.ts +4 -1
package/README.md
CHANGED
|
@@ -15,7 +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,
|
|
18
|
+
- **Injected execution context**: Prefer `{ fs, console, process }` in actions and middleware for portable storage, output, and runtime metadata across Node.js, tests, and JustBash.
|
|
19
19
|
- **Type-safe middleware**: Register `.use()` callbacks that run before commands with full type inference from global options.
|
|
20
20
|
- **Developer friendly**. Written in TypeScript.
|
|
21
21
|
|
|
@@ -44,8 +44,8 @@ cli.option(
|
|
|
44
44
|
)
|
|
45
45
|
cli.option('--name <name>', 'Provide your name')
|
|
46
46
|
|
|
47
|
-
cli.command('lint [...files]', 'Lint files').action((files, options, { console }) => {
|
|
48
|
-
console.log(files, options)
|
|
47
|
+
cli.command('lint [...files]', 'Lint files').action((files, options, { console, process }) => {
|
|
48
|
+
console.log(files, options, process.cwd)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
cli
|
|
@@ -53,8 +53,8 @@ cli
|
|
|
53
53
|
.option('--minify', 'Minify output')
|
|
54
54
|
.example('build src/index.ts')
|
|
55
55
|
.example('build src/index.ts --minify')
|
|
56
|
-
.action(async (entry, options, { console }) => { // options is type safe! no need to type it
|
|
57
|
-
console.log(entry, options)
|
|
56
|
+
.action(async (entry, options, { console, process }) => { // options is type safe! no need to type it
|
|
57
|
+
console.log(entry, options, process.env.NODE_ENV)
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
cli.example((bin) => `${bin} lint src/**/*.ts`)
|
|
@@ -133,8 +133,8 @@ cli
|
|
|
133
133
|
.option('--channel <name>', 'Target channel: stable, beta, alpha')
|
|
134
134
|
.option('--notes-file <path>', 'Markdown file used as release notes')
|
|
135
135
|
.option('--dry-run', 'Preview every step without publishing')
|
|
136
|
-
.action((version, options, { console }) => {
|
|
137
|
-
console.log('release', version, options)
|
|
136
|
+
.action((version, options, { console, process }) => {
|
|
137
|
+
console.log('release', version, options, process.cwd)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
cli
|
|
@@ -159,8 +159,8 @@ cli
|
|
|
159
159
|
.option('--target <migration>', 'Apply up to a specific migration id')
|
|
160
160
|
.option('--dry-run', 'Print plan only, do not execute SQL')
|
|
161
161
|
.option('--verbose', 'Show each executed statement')
|
|
162
|
-
.action((options, { console }) => {
|
|
163
|
-
console.log('migrate', options)
|
|
162
|
+
.action((options, { console, process }) => {
|
|
163
|
+
console.log('migrate', options, process.stdin)
|
|
164
164
|
})
|
|
165
165
|
|
|
166
166
|
cli.help()
|
|
@@ -299,39 +299,39 @@ cli
|
|
|
299
299
|
z.string().default('production').describe('Target environment'),
|
|
300
300
|
)
|
|
301
301
|
.option('--dry-run', 'Preview without deploying')
|
|
302
|
-
.action((options, { console }) => {
|
|
303
|
-
console.log(`Deploying to ${options.env}...`)
|
|
302
|
+
.action((options, { console, process }) => {
|
|
303
|
+
console.log(`Deploying to ${options.env} from ${process.cwd}...`)
|
|
304
304
|
})
|
|
305
305
|
|
|
306
306
|
// Subcommands
|
|
307
307
|
cli
|
|
308
308
|
.command('init', 'Initialize a new project')
|
|
309
309
|
.option('--template <template>', 'Project template')
|
|
310
|
-
.action((options, { console }) => {
|
|
311
|
-
console.log('Initializing project
|
|
310
|
+
.action((options, { console, process }) => {
|
|
311
|
+
console.log('Initializing project in', process.cwd)
|
|
312
312
|
})
|
|
313
313
|
|
|
314
|
-
cli.command('login', 'Authenticate with the server').action((options, { console }) => {
|
|
315
|
-
console.log('Opening browser for login
|
|
314
|
+
cli.command('login', 'Authenticate with the server').action((options, { console, process }) => {
|
|
315
|
+
console.log('Opening browser for login from', process.cwd)
|
|
316
316
|
})
|
|
317
317
|
|
|
318
|
-
cli.command('logout', 'Clear saved credentials').action((options, { console }) => {
|
|
319
|
-
console.log('Logged out')
|
|
318
|
+
cli.command('logout', 'Clear saved credentials').action((options, { console, process }) => {
|
|
319
|
+
console.log('Logged out', process.env.USER)
|
|
320
320
|
})
|
|
321
321
|
|
|
322
322
|
cli
|
|
323
323
|
.command('status', 'Show deployment status')
|
|
324
324
|
.option('--json', 'Output as JSON')
|
|
325
|
-
.action((options, { console }) => {
|
|
326
|
-
console.log('Fetching status
|
|
325
|
+
.action((options, { console, process }) => {
|
|
326
|
+
console.log('Fetching status from', process.cwd)
|
|
327
327
|
})
|
|
328
328
|
|
|
329
329
|
cli
|
|
330
330
|
.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
331
331
|
.option('--follow', 'Follow log output')
|
|
332
332
|
.option('--lines <n>', z.number().default(100).describe('Number of lines'))
|
|
333
|
-
.action((deploymentId, options, { console }) => {
|
|
334
|
-
console.log(`Streaming logs for ${deploymentId}...`)
|
|
333
|
+
.action((deploymentId, options, { console, process }) => {
|
|
334
|
+
console.log(`Streaming logs for ${deploymentId} from ${process.cwd}...`)
|
|
335
335
|
})
|
|
336
336
|
|
|
337
337
|
cli.help()
|
|
@@ -352,10 +352,72 @@ deploy --help # shows all commands
|
|
|
352
352
|
|
|
353
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.
|
|
354
354
|
|
|
355
|
-
Prefer the injected `{ console, process }` argument over global `console
|
|
355
|
+
Prefer the injected `{ fs, console, process }` argument over global `console`, `process.exit`, or direct `node:fs/promises` imports. It keeps commands easier to test and lets the same command code run inside alternate runtimes like JustBash.
|
|
356
|
+
|
|
357
|
+
`process.cwd`, `process.stdin`, and `process.env` come from the active runtime:
|
|
358
|
+
|
|
359
|
+
- In normal Node.js runs, `process.cwd` and `process.env` reflect the host process, while `process.stdin` defaults to an empty string unless you inject it yourself.
|
|
360
|
+
- In JustBash runs, those same fields are populated from the sandbox execution context.
|
|
356
361
|
|
|
357
362
|
Middleware runs in registration order, after option parsing and validation, but before the matched command's `.action()` callback.
|
|
358
363
|
|
|
364
|
+
### Filesystem Access
|
|
365
|
+
|
|
366
|
+
The injected `fs` object is the recommended way to read or write CLI state.
|
|
367
|
+
|
|
368
|
+
- In normal Node.js runs, `fs` defaults to `node:fs/promises`
|
|
369
|
+
- In JustBash runs, `goke` swaps in a compatible adapter over the JustBash virtual filesystem
|
|
370
|
+
|
|
371
|
+
This makes storage-style commands work in both environments without branching on runtime details.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
cli
|
|
375
|
+
.command('login', 'Save auth token')
|
|
376
|
+
.option('--token <token>', z.string().describe('Auth token'))
|
|
377
|
+
.action(async (options, { fs, console, process }) => {
|
|
378
|
+
await fs.mkdir('.mycli', { recursive: true })
|
|
379
|
+
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
|
|
380
|
+
console.log('saved credentials in', process.cwd)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
cli
|
|
384
|
+
.command('whoami', 'Read saved auth token')
|
|
385
|
+
.action(async (options, { fs, console, process }) => {
|
|
386
|
+
const auth = await fs.readFile('.mycli/auth.json', 'utf8')
|
|
387
|
+
console.log(auth, process.env.USER)
|
|
388
|
+
})
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Prefer injected `fs` for CLI storage instead of importing `node:fs/promises` directly inside actions. That keeps the command portable to JustBash and easier to test.
|
|
392
|
+
|
|
393
|
+
`goke` also exports the runtime types, so helper functions can use dependency injection without reaching for globals:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { goke } from 'goke'
|
|
397
|
+
import type { GokeFs, GokeProcess } from 'goke'
|
|
398
|
+
|
|
399
|
+
async function saveAuthToken(args: {
|
|
400
|
+
fs: GokeFs
|
|
401
|
+
process: GokeProcess
|
|
402
|
+
token: string
|
|
403
|
+
}) {
|
|
404
|
+
await args.fs.mkdir('.mycli', { recursive: true })
|
|
405
|
+
await args.fs.writeFile('.mycli/auth.json', JSON.stringify({
|
|
406
|
+
token,
|
|
407
|
+
cwd: args.process.cwd,
|
|
408
|
+
}), 'utf8')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const cli = goke('mycli')
|
|
412
|
+
|
|
413
|
+
cli
|
|
414
|
+
.command('login <token>', 'Save auth token')
|
|
415
|
+
.action(async (token, options, { fs, process, console }) => {
|
|
416
|
+
await saveAuthToken({ fs, process, token })
|
|
417
|
+
console.log('saved credentials')
|
|
418
|
+
})
|
|
419
|
+
```
|
|
420
|
+
|
|
359
421
|
```ts
|
|
360
422
|
import { goke } from 'goke'
|
|
361
423
|
import { z } from 'zod'
|
|
@@ -365,25 +427,25 @@ const cli = goke('mycli')
|
|
|
365
427
|
cli
|
|
366
428
|
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
367
429
|
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
368
|
-
.use((options, { console }) => {
|
|
430
|
+
.use((options, { console, process }) => {
|
|
369
431
|
// options.verbose and options.apiUrl are fully typed here
|
|
370
432
|
if (options.verbose) {
|
|
371
|
-
console.log('verbose mode enabled')
|
|
433
|
+
console.log('verbose mode enabled in', process.cwd)
|
|
372
434
|
}
|
|
373
435
|
})
|
|
374
436
|
|
|
375
437
|
cli
|
|
376
438
|
.command('deploy <env>', 'Deploy to an environment')
|
|
377
439
|
.option('--dry-run', 'Preview without deploying')
|
|
378
|
-
.action((env, options, { console }) => {
|
|
440
|
+
.action((env, options, { console, process }) => {
|
|
379
441
|
// options includes both command options (dryRun) and global options (verbose, apiUrl)
|
|
380
|
-
console.log(`Deploying to ${env} via ${options.apiUrl}`)
|
|
442
|
+
console.log(`Deploying to ${env} via ${options.apiUrl} from ${process.cwd}`)
|
|
381
443
|
})
|
|
382
444
|
|
|
383
445
|
cli
|
|
384
446
|
.command('status', 'Show deployment status')
|
|
385
|
-
.action((options, { console }) => {
|
|
386
|
-
console.log('Checking status...')
|
|
447
|
+
.action((options, { console, process }) => {
|
|
448
|
+
console.log('Checking status...', process.stdin)
|
|
387
449
|
})
|
|
388
450
|
|
|
389
451
|
cli.help()
|
|
@@ -398,13 +460,16 @@ cli
|
|
|
398
460
|
.use((options, { process }) => {
|
|
399
461
|
options.verbose // boolean — typed
|
|
400
462
|
process.argv // string[] — typed
|
|
463
|
+
process.cwd // string — typed
|
|
464
|
+
process.env // Record<string, string> — typed
|
|
465
|
+
process.stdin // string — typed
|
|
401
466
|
options.port // TypeScript error — not declared yet
|
|
402
467
|
})
|
|
403
468
|
.option('--port <port>', z.number().describe('Port'))
|
|
404
|
-
.use((options, { console }) => {
|
|
469
|
+
.use((options, { console, process }) => {
|
|
405
470
|
options.verbose // boolean — still visible
|
|
406
471
|
options.port // number — now visible
|
|
407
|
-
console.error('ready')
|
|
472
|
+
console.error('ready', process.cwd)
|
|
408
473
|
})
|
|
409
474
|
```
|
|
410
475
|
|
|
@@ -413,10 +478,10 @@ Middleware supports async functions. If any middleware is async, the remaining m
|
|
|
413
478
|
```ts
|
|
414
479
|
cli
|
|
415
480
|
.option('--token <token>', z.string().describe('API token'))
|
|
416
|
-
.use(async (options, { console }) => {
|
|
481
|
+
.use(async (options, { console, process }) => {
|
|
417
482
|
const client = await connectToApi(options.token)
|
|
418
483
|
globalState.client = client
|
|
419
|
-
console.log('connected')
|
|
484
|
+
console.log('connected', process.env.NODE_ENV)
|
|
420
485
|
})
|
|
421
486
|
```
|
|
422
487
|
|
|
@@ -432,8 +497,8 @@ const cli = goke()
|
|
|
432
497
|
cli
|
|
433
498
|
.command('rm <dir>', 'Remove a dir')
|
|
434
499
|
.option('-r, --recursive', 'Remove recursively')
|
|
435
|
-
.action((dir, options, { console }) => {
|
|
436
|
-
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''))
|
|
500
|
+
.action((dir, options, { console, process }) => {
|
|
501
|
+
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''), process.cwd)
|
|
437
502
|
})
|
|
438
503
|
|
|
439
504
|
cli.help()
|
|
@@ -450,18 +515,18 @@ import { goke } from 'goke'
|
|
|
450
515
|
|
|
451
516
|
const cli = goke('mycli')
|
|
452
517
|
|
|
453
|
-
cli.command('mcp login <url>', 'Login to MCP server').action((url, options, { console }) => {
|
|
454
|
-
console.log('Logging in to', url)
|
|
518
|
+
cli.command('mcp login <url>', 'Login to MCP server').action((url, options, { console, process }) => {
|
|
519
|
+
console.log('Logging in to', url, 'from', process.cwd)
|
|
455
520
|
})
|
|
456
521
|
|
|
457
|
-
cli.command('mcp logout', 'Logout from MCP server').action((options, { console }) => {
|
|
458
|
-
console.log('Logged out')
|
|
522
|
+
cli.command('mcp logout', 'Logout from MCP server').action((options, { console, process }) => {
|
|
523
|
+
console.log('Logged out', process.env.USER)
|
|
459
524
|
})
|
|
460
525
|
|
|
461
526
|
cli
|
|
462
527
|
.command('git remote add <name> <url>', 'Add a git remote')
|
|
463
|
-
.action((name, url, options, { console }) => {
|
|
464
|
-
console.log('Adding remote', name, url)
|
|
528
|
+
.action((name, url, options, { console, process }) => {
|
|
529
|
+
console.log('Adding remote', name, url, 'from', process.cwd)
|
|
465
530
|
})
|
|
466
531
|
|
|
467
532
|
cli.help()
|
|
@@ -485,9 +550,9 @@ cli
|
|
|
485
550
|
.option('--workers <workers>', z.int().describe('Worker count'))
|
|
486
551
|
.option('--tags <tag>', z.array(z.string()).describe('Tags (repeatable)'))
|
|
487
552
|
.option('--verbose', 'Verbose output')
|
|
488
|
-
.action((options, { console }) => {
|
|
553
|
+
.action((options, { console, process }) => {
|
|
489
554
|
// options.port is number, options.host is string, etc.
|
|
490
|
-
console.log(options)
|
|
555
|
+
console.log(options, process.env.NODE_ENV)
|
|
491
556
|
})
|
|
492
557
|
|
|
493
558
|
cli.parse()
|
|
@@ -516,9 +581,9 @@ cli
|
|
|
516
581
|
.option('--old-port <port>', z.number().meta({ deprecated: true, description: 'Use --port instead' }))
|
|
517
582
|
// Current option: visible in help
|
|
518
583
|
.option('--port <port>', z.number().describe('Port number'))
|
|
519
|
-
.action((options, { console }) => {
|
|
584
|
+
.action((options, { console, process }) => {
|
|
520
585
|
const port = options.port ?? options.oldPort
|
|
521
|
-
console.log('Starting on port', port)
|
|
586
|
+
console.log('Starting on port', port, 'from', process.cwd)
|
|
522
587
|
})
|
|
523
588
|
|
|
524
589
|
cli.help()
|
|
@@ -554,10 +619,10 @@ The last argument of a command can be variadic. To make an argument variadic you
|
|
|
554
619
|
cli
|
|
555
620
|
.command('build <entry> [...otherFiles]', 'Build your app')
|
|
556
621
|
.option('--foo', 'Foo option')
|
|
557
|
-
.action((entry, otherFiles, options, { console }) => {
|
|
622
|
+
.action((entry, otherFiles, options, { console, process }) => {
|
|
558
623
|
console.log(entry)
|
|
559
624
|
console.log(otherFiles)
|
|
560
|
-
console.log(options)
|
|
625
|
+
console.log(options, process.stdin)
|
|
561
626
|
})
|
|
562
627
|
```
|
|
563
628
|
|
|
@@ -612,8 +677,8 @@ cli
|
|
|
612
677
|
.command('build', 'desc')
|
|
613
678
|
.option('--env <env>', 'Set envs')
|
|
614
679
|
.example('--env.API_SECRET xxx')
|
|
615
|
-
.action((options, { console }) => {
|
|
616
|
-
console.log(options)
|
|
680
|
+
.action((options, { console, process }) => {
|
|
681
|
+
console.log(options, process.env.API_SECRET)
|
|
617
682
|
})
|
|
618
683
|
```
|
|
619
684
|
|
|
@@ -625,9 +690,9 @@ Register a command that will be used when no other command is matched.
|
|
|
625
690
|
cli
|
|
626
691
|
.command('[...files]', 'Build files')
|
|
627
692
|
.option('--minimize', 'Minimize output')
|
|
628
|
-
.action((files, options, { console }) => {
|
|
693
|
+
.action((files, options, { console, process }) => {
|
|
629
694
|
console.log(files)
|
|
630
|
-
console.log(options.minimize)
|
|
695
|
+
console.log(options.minimize, process.cwd)
|
|
631
696
|
})
|
|
632
697
|
```
|
|
633
698
|
|
|
@@ -648,7 +713,7 @@ try {
|
|
|
648
713
|
|
|
649
714
|
### Testing with mocked console and exit
|
|
650
715
|
|
|
651
|
-
Because goke derives its injected `{ console, process }` from the CLI's configured
|
|
716
|
+
Because goke derives its injected `{ fs, console, process }` from the CLI's configured runtime dependencies, tests can override them directly and assert on the calls.
|
|
652
717
|
|
|
653
718
|
```ts
|
|
654
719
|
import { describe, expect, test, vi } from 'vitest'
|
|
@@ -700,11 +765,11 @@ cli
|
|
|
700
765
|
.command('serve <entry>', 'Start the app')
|
|
701
766
|
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
702
767
|
.option('--watch', 'Watch files')
|
|
703
|
-
.action((entry, options, { console }) => {
|
|
768
|
+
.action((entry, options, { console, process }) => {
|
|
704
769
|
// entry: string
|
|
705
770
|
// options.port: number
|
|
706
771
|
// options.watch: boolean
|
|
707
|
-
console.log(entry, options.port, options.watch)
|
|
772
|
+
console.log(entry, options.port, options.watch, process.cwd)
|
|
708
773
|
})
|
|
709
774
|
```
|
|
710
775
|
|
|
@@ -732,8 +797,8 @@ const cli = goke('parent')
|
|
|
732
797
|
cli
|
|
733
798
|
.command('child commandwithspaces', 'Run nested command')
|
|
734
799
|
.option('--name <name>', z.string().describe('Name'))
|
|
735
|
-
.action((options, { console }) => {
|
|
736
|
-
console.log(`hello ${options.name}`)
|
|
800
|
+
.action((options, { console, process }) => {
|
|
801
|
+
console.log(`hello ${options.name} from ${process.cwd}`)
|
|
737
802
|
})
|
|
738
803
|
|
|
739
804
|
const bash = new Bash({
|
|
@@ -743,7 +808,7 @@ const bash = new Bash({
|
|
|
743
808
|
await bash.exec('parent child commandwithspaces --name Tommy')
|
|
744
809
|
```
|
|
745
810
|
|
|
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.
|
|
811
|
+
Prefer the injected `{ fs, console, process }` helpers in command implementations so the same command code works cleanly both in the regular CLI runtime and through the JustBash bridge. The injected `fs` defaults to Node `fs/promises`, and `process.cwd` / `process.env` / `process.stdin` reflect host values in Node but sandbox values inside `createJustBashCommand()`.
|
|
747
812
|
|
|
748
813
|
## References
|
|
749
814
|
|
|
@@ -780,9 +845,9 @@ Add a global option. The second argument is either:
|
|
|
780
845
|
|
|
781
846
|
#### cli.use(callback)
|
|
782
847
|
|
|
783
|
-
- Type: `(callback: (options: Opts, { console, process }) => void | Promise<void>) => CLI`
|
|
848
|
+
- Type: `(callback: (options: Opts, { fs, console, process }) => void | Promise<void>) => CLI`
|
|
784
849
|
|
|
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.
|
|
850
|
+
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 `{ fs, console, process }` helper object.
|
|
786
851
|
|
|
787
852
|
#### cli.parse(argv?)
|
|
788
853
|
|
|
@@ -836,7 +901,7 @@ Basically the same as `cli.option` but this adds the option to specific command.
|
|
|
836
901
|
|
|
837
902
|
- Type: `(callback: ActionCallback) => Command`
|
|
838
903
|
|
|
839
|
-
Command callbacks receive positional args first, then parsed options, then an injected `{ console, process }` object. Prefer those injected helpers over global `console
|
|
904
|
+
Command callbacks receive positional args first, then parsed options, then an injected `{ fs, console, process }` object. Prefer those injected helpers over global `console`, `process.exit`, and direct `node:fs/promises` imports so commands stay easier to test and can run inside alternate runtimes like JustBash.
|
|
840
905
|
|
|
841
906
|
#### command.alias(name)
|
|
842
907
|
|
|
@@ -2,6 +2,9 @@ import { describe, test, expect } from 'vitest';
|
|
|
2
2
|
import goke, { createConsole } from '../index.js';
|
|
3
3
|
import { coerceBySchema } from '../coerce.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
5
8
|
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
6
9
|
const stripAnsi = (text) => text.replace(ANSI_RE, '');
|
|
7
10
|
/**
|
|
@@ -129,6 +132,83 @@ describe('error formatting', () => {
|
|
|
129
132
|
expect(text).toMatch(/at /);
|
|
130
133
|
});
|
|
131
134
|
});
|
|
135
|
+
describe('injected fs', () => {
|
|
136
|
+
test('command actions can use the default node fs for cli storage', async () => {
|
|
137
|
+
const stdout = createTestOutputStream();
|
|
138
|
+
const cli = gokeTestable('mycli', { stdout });
|
|
139
|
+
const originalCwd = process.cwd();
|
|
140
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'goke-fs-'));
|
|
141
|
+
try {
|
|
142
|
+
process.chdir(tempDir);
|
|
143
|
+
cli
|
|
144
|
+
.command('login', 'Persist login state')
|
|
145
|
+
.option('--token <token>', z.string().describe('Token'))
|
|
146
|
+
.action(async (options, { fs, console }) => {
|
|
147
|
+
await fs.mkdir('.mycli', { recursive: true });
|
|
148
|
+
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8');
|
|
149
|
+
console.log('saved credentials');
|
|
150
|
+
});
|
|
151
|
+
cli.parse(['node', 'bin', 'login', '--token', 'abc123'], { run: false });
|
|
152
|
+
await cli.runMatchedCommand();
|
|
153
|
+
expect(stdout.text).toBe('saved credentials\n');
|
|
154
|
+
expect(await readFile(join(tempDir, '.mycli/auth.json'), 'utf8')).toBe('{"token":"abc123"}');
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
process.chdir(originalCwd);
|
|
158
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('injected process context', () => {
|
|
163
|
+
test('command actions receive host cwd, env, and stdin defaults', async () => {
|
|
164
|
+
const stdout = createTestOutputStream();
|
|
165
|
+
const cli = gokeTestable('mycli', { stdout });
|
|
166
|
+
const originalCwd = process.cwd();
|
|
167
|
+
const originalEnv = process.env.GOKE_TEST_TOKEN;
|
|
168
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'goke-process-'));
|
|
169
|
+
try {
|
|
170
|
+
process.chdir(tempDir);
|
|
171
|
+
process.env.GOKE_TEST_TOKEN = 'abc123';
|
|
172
|
+
cli
|
|
173
|
+
.command('context', 'Inspect process context')
|
|
174
|
+
.action((options, { console, process }) => {
|
|
175
|
+
console.log(JSON.stringify({
|
|
176
|
+
cwd: process.cwd,
|
|
177
|
+
stdin: process.stdin,
|
|
178
|
+
token: process.env.GOKE_TEST_TOKEN,
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
181
|
+
cli.parse(['node', 'bin', 'context'], { run: false });
|
|
182
|
+
await cli.runMatchedCommand();
|
|
183
|
+
expect(stdout.text).toBe(`${JSON.stringify({ cwd: process.cwd(), stdin: '', token: 'abc123' })}\n`);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
process.chdir(originalCwd);
|
|
187
|
+
if (originalEnv === undefined) {
|
|
188
|
+
delete process.env.GOKE_TEST_TOKEN;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
process.env.GOKE_TEST_TOKEN = originalEnv;
|
|
192
|
+
}
|
|
193
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
test('custom injected env stays mutable inside command actions', async () => {
|
|
197
|
+
const stdout = createTestOutputStream();
|
|
198
|
+
const env = { TOKEN: 'before' };
|
|
199
|
+
const cli = gokeTestable('mycli', { env, stdout });
|
|
200
|
+
cli
|
|
201
|
+
.command('context', 'Mutate process env')
|
|
202
|
+
.action((options, { console, process }) => {
|
|
203
|
+
process.env.TOKEN = 'after';
|
|
204
|
+
console.log(process.env.TOKEN);
|
|
205
|
+
});
|
|
206
|
+
cli.parse(['node', 'bin', 'context'], { run: false });
|
|
207
|
+
await cli.runMatchedCommand();
|
|
208
|
+
expect(stdout.text).toBe('after\n');
|
|
209
|
+
expect(env.TOKEN).toBe('after');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
132
212
|
test('double dashes', () => {
|
|
133
213
|
const cli = goke();
|
|
134
214
|
const { args, options } = cli.parse([
|
|
@@ -79,6 +79,134 @@ describe('createJustBashCommand', () => {
|
|
|
79
79
|
exitCode: 0,
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
|
+
test('works through real just-bash exec with a goke custom command', async () => {
|
|
83
|
+
const { Bash } = await import('just-bash');
|
|
84
|
+
const cli = gokeTestable('parent');
|
|
85
|
+
cli
|
|
86
|
+
.command('child commandwithspaces', 'Run nested command')
|
|
87
|
+
.option('--name <name>', z.string().describe('Name'))
|
|
88
|
+
.action((options, { console }) => {
|
|
89
|
+
console.log(`hello ${options.name}`);
|
|
90
|
+
});
|
|
91
|
+
const bash = new Bash({
|
|
92
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
93
|
+
});
|
|
94
|
+
const result = await bash.exec('parent child commandwithspaces --name Tommy');
|
|
95
|
+
expect(result.stdout).toBe('hello Tommy\n');
|
|
96
|
+
expect(result.stderr).toBe('');
|
|
97
|
+
expect(result.exitCode).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
test('maps injected fs to the just-bash virtual filesystem', async () => {
|
|
100
|
+
const { Bash } = await import('just-bash');
|
|
101
|
+
const cli = gokeTestable('parent');
|
|
102
|
+
cli
|
|
103
|
+
.command('login', 'Persist login state')
|
|
104
|
+
.option('--token <token>', z.string().describe('Token'))
|
|
105
|
+
.action(async (options, { fs, console }) => {
|
|
106
|
+
await fs.mkdir('.mycli', { recursive: true });
|
|
107
|
+
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8');
|
|
108
|
+
console.log('saved credentials');
|
|
109
|
+
});
|
|
110
|
+
const bash = new Bash({
|
|
111
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
112
|
+
});
|
|
113
|
+
const loginResult = await bash.exec('mkdir project && cd project && parent login --token Tommy');
|
|
114
|
+
const catResult = await bash.exec('cd project && cat .mycli/auth.json');
|
|
115
|
+
expect(loginResult.stdout).toBe('saved credentials\n');
|
|
116
|
+
expect(loginResult.stderr).toBe('');
|
|
117
|
+
expect(loginResult.exitCode).toBe(0);
|
|
118
|
+
expect(catResult.stdout).toBe('{"token":"Tommy"}');
|
|
119
|
+
expect(catResult.stderr).toBe('');
|
|
120
|
+
expect(catResult.exitCode).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
test('real just-bash exec passes the configured in-memory fs to the goke command', async () => {
|
|
123
|
+
const { Bash, InMemoryFs } = await import('just-bash');
|
|
124
|
+
const cli = gokeTestable('parent');
|
|
125
|
+
cli
|
|
126
|
+
.command('login', 'Persist login state')
|
|
127
|
+
.option('--token <token>', z.string().describe('Token'))
|
|
128
|
+
.action(async (options, { fs, console }) => {
|
|
129
|
+
await fs.mkdir('.mycli', { recursive: true });
|
|
130
|
+
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8');
|
|
131
|
+
console.log('saved credentials');
|
|
132
|
+
});
|
|
133
|
+
const virtualFs = new InMemoryFs();
|
|
134
|
+
await virtualFs.mkdir('/project', { recursive: true });
|
|
135
|
+
const bash = new Bash({
|
|
136
|
+
fs: virtualFs,
|
|
137
|
+
cwd: '/project',
|
|
138
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
139
|
+
});
|
|
140
|
+
const result = await bash.exec('parent login --token Tommy');
|
|
141
|
+
expect(result.stdout).toBe('saved credentials\n');
|
|
142
|
+
expect(result.stderr).toBe('');
|
|
143
|
+
expect(result.exitCode).toBe(0);
|
|
144
|
+
expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe('{"token":"Tommy"}');
|
|
145
|
+
});
|
|
146
|
+
test('real just-bash exec passes sandbox cwd, stdin, and env through process context', async () => {
|
|
147
|
+
const { Bash, InMemoryFs } = await import('just-bash');
|
|
148
|
+
const cli = gokeTestable('parent');
|
|
149
|
+
cli
|
|
150
|
+
.command('context', 'Inspect process context')
|
|
151
|
+
.action((options, { console, process }) => {
|
|
152
|
+
console.log(JSON.stringify({
|
|
153
|
+
cwd: process.cwd,
|
|
154
|
+
stdin: process.stdin,
|
|
155
|
+
token: process.env.TOKEN,
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
const virtualFs = new InMemoryFs();
|
|
159
|
+
await virtualFs.mkdir('/project', { recursive: true });
|
|
160
|
+
const bash = new Bash({
|
|
161
|
+
fs: virtualFs,
|
|
162
|
+
cwd: '/project',
|
|
163
|
+
env: { TOKEN: 'Tommy' },
|
|
164
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
165
|
+
});
|
|
166
|
+
const result = await bash.exec('parent context', { stdin: 'hello from stdin' });
|
|
167
|
+
expect(result.stdout).toBe(`${JSON.stringify({ cwd: '/project', stdin: 'hello from stdin', token: 'Tommy' })}\n`);
|
|
168
|
+
expect(result.stderr).toBe('');
|
|
169
|
+
expect(result.exitCode).toBe(0);
|
|
170
|
+
});
|
|
171
|
+
test('explicit just-bash context exposes a mutable env object backed by the sandbox env', async () => {
|
|
172
|
+
const { InMemoryFs } = await import('just-bash');
|
|
173
|
+
const cli = gokeTestable('parent');
|
|
174
|
+
cli
|
|
175
|
+
.command('mutate-env', 'Mutate sandbox env')
|
|
176
|
+
.action((options, { console, process }) => {
|
|
177
|
+
process.env.TOKEN = 'updated';
|
|
178
|
+
console.log(process.env.TOKEN);
|
|
179
|
+
});
|
|
180
|
+
const virtualFs = new InMemoryFs();
|
|
181
|
+
await virtualFs.mkdir('/project', { recursive: true });
|
|
182
|
+
const env = new Map([['TOKEN', 'before']]);
|
|
183
|
+
const customCommand = await cli.createJustBashCommand();
|
|
184
|
+
const result = await customCommand.execute(['mutate-env'], { fs: virtualFs, cwd: '/project', env, stdin: '' });
|
|
185
|
+
expect(result.stdout).toBe('updated\n');
|
|
186
|
+
expect(result.stderr).toBe('');
|
|
187
|
+
expect(result.exitCode).toBe(0);
|
|
188
|
+
expect(env.get('TOKEN')).toBe('updated');
|
|
189
|
+
});
|
|
190
|
+
test('accepts an explicit just-bash fs context when executing the custom command', async () => {
|
|
191
|
+
const { InMemoryFs } = await import('just-bash');
|
|
192
|
+
const cli = gokeTestable('parent');
|
|
193
|
+
cli
|
|
194
|
+
.command('login', 'Persist login state')
|
|
195
|
+
.option('--token <token>', z.string().describe('Token'))
|
|
196
|
+
.action(async (options, { fs, console }) => {
|
|
197
|
+
await fs.mkdir('.mycli', { recursive: true });
|
|
198
|
+
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8');
|
|
199
|
+
console.log('saved credentials');
|
|
200
|
+
});
|
|
201
|
+
const customCommand = await cli.createJustBashCommand();
|
|
202
|
+
const virtualFs = new InMemoryFs();
|
|
203
|
+
await virtualFs.mkdir('/project', { recursive: true });
|
|
204
|
+
const result = await customCommand.execute(['login', '--token', 'Tommy'], { fs: virtualFs, cwd: '/project', env: new Map(), stdin: '' });
|
|
205
|
+
expect(result.stdout).toBe('saved credentials\n');
|
|
206
|
+
expect(result.stderr).toBe('');
|
|
207
|
+
expect(result.exitCode).toBe(0);
|
|
208
|
+
expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe('{"token":"Tommy"}');
|
|
209
|
+
});
|
|
82
210
|
test('maps injected process.exit to a command exit code', async () => {
|
|
83
211
|
const cli = gokeTestable('parent');
|
|
84
212
|
cli
|
|
@@ -73,10 +73,14 @@ 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, { console, process }) => {
|
|
76
|
+
.use((options, { console, fs, process }) => {
|
|
77
77
|
expectTypeOf(options.port).toEqualTypeOf();
|
|
78
78
|
expectTypeOf(options.host).toEqualTypeOf();
|
|
79
|
+
expectTypeOf(fs.mkdir).toBeFunction();
|
|
79
80
|
expectTypeOf(process.argv).toEqualTypeOf();
|
|
81
|
+
expectTypeOf(process.cwd).toEqualTypeOf();
|
|
82
|
+
expectTypeOf(process.env).toEqualTypeOf();
|
|
83
|
+
expectTypeOf(process.stdin).toEqualTypeOf();
|
|
80
84
|
expectTypeOf(process.stdout.write).toEqualTypeOf();
|
|
81
85
|
expectTypeOf(console.log).toBeFunction();
|
|
82
86
|
});
|
|
@@ -86,8 +90,9 @@ describe('type-level: middleware use() callback inference', () => {
|
|
|
86
90
|
const schema2 = {};
|
|
87
91
|
goke('test')
|
|
88
92
|
.option('--verbose', schema1)
|
|
89
|
-
.use((options, { process }) => {
|
|
93
|
+
.use((options, { fs, process }) => {
|
|
90
94
|
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
95
|
+
expectTypeOf(fs.writeFile).toBeFunction();
|
|
91
96
|
expectTypeOf(process.exit).toEqualTypeOf();
|
|
92
97
|
// @ts-expect-error port is not declared yet
|
|
93
98
|
options.port;
|
|
@@ -104,7 +109,8 @@ describe('type-level: middleware use() callback inference', () => {
|
|
|
104
109
|
const schema = {};
|
|
105
110
|
goke('test')
|
|
106
111
|
.option('--port <port>', schema)
|
|
107
|
-
.use((options, { process }) => {
|
|
112
|
+
.use((options, { fs, process }) => {
|
|
113
|
+
expectTypeOf(fs.readFile).toBeFunction();
|
|
108
114
|
expectTypeOf(process.stderr.write).toEqualTypeOf();
|
|
109
115
|
// @ts-expect-error nonExistent was never defined
|
|
110
116
|
options.nonExistent;
|