goke 6.3.2 → 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 +188 -53
- package/dist/__test__/index.test.js +80 -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 +225 -0
- package/dist/__test__/types.test-d.js +16 -4
- 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 +52 -13
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +130 -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 +25 -0
- package/dist/just-bash.d.ts.map +1 -0
- package/dist/just-bash.js +227 -0
- package/dist/runtime-browser.d.ts +35 -0
- package/dist/runtime-browser.d.ts.map +1 -0
- package/dist/runtime-browser.js +74 -0
- package/dist/runtime-node.d.ts +10 -0
- package/dist/runtime-node.d.ts.map +1 -0
- package/dist/runtime-node.js +29 -0
- package/package.json +10 -1
- package/src/__test__/index.test.ts +93 -0
- package/src/__test__/just-bash.test.ts +292 -0
- package/src/__test__/types.test-d.ts +16 -4
- package/src/goke-fs.ts +26 -0
- package/src/goke.ts +173 -45
- package/src/index.ts +2 -2
- package/src/just-bash.ts +275 -0
- package/src/runtime-browser.ts +93 -0
- package/src/runtime-node.ts +32 -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 `{ fs, console, process }` in actions and middleware for portable storage, output, and runtime metadata across Node.js, tests, and 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,8 +44,8 @@ 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
|
-
console.log(files, options)
|
|
47
|
+
cli.command('lint [...files]', 'Lint files').action((files, options, { console, process }) => {
|
|
48
|
+
console.log(files, options, process.cwd)
|
|
48
49
|
})
|
|
49
50
|
|
|
50
51
|
cli
|
|
@@ -52,8 +53,8 @@ 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
|
-
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)
|
|
57
58
|
})
|
|
58
59
|
|
|
59
60
|
cli.example((bin) => `${bin} lint src/**/*.ts`)
|
|
@@ -132,8 +133,8 @@ 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
|
-
console.log('release', version, options)
|
|
136
|
+
.action((version, options, { console, process }) => {
|
|
137
|
+
console.log('release', version, options, process.cwd)
|
|
137
138
|
})
|
|
138
139
|
|
|
139
140
|
cli
|
|
@@ -158,8 +159,8 @@ 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
|
-
console.log('migrate', options)
|
|
162
|
+
.action((options, { console, process }) => {
|
|
163
|
+
console.log('migrate', options, process.stdin)
|
|
163
164
|
})
|
|
164
165
|
|
|
165
166
|
cli.help()
|
|
@@ -298,39 +299,39 @@ cli
|
|
|
298
299
|
z.string().default('production').describe('Target environment'),
|
|
299
300
|
)
|
|
300
301
|
.option('--dry-run', 'Preview without deploying')
|
|
301
|
-
.action((options) => {
|
|
302
|
-
console.log(`Deploying to ${options.env}...`)
|
|
302
|
+
.action((options, { console, process }) => {
|
|
303
|
+
console.log(`Deploying to ${options.env} from ${process.cwd}...`)
|
|
303
304
|
})
|
|
304
305
|
|
|
305
306
|
// Subcommands
|
|
306
307
|
cli
|
|
307
308
|
.command('init', 'Initialize a new project')
|
|
308
309
|
.option('--template <template>', 'Project template')
|
|
309
|
-
.action((options) => {
|
|
310
|
-
console.log('Initializing project
|
|
310
|
+
.action((options, { console, process }) => {
|
|
311
|
+
console.log('Initializing project in', process.cwd)
|
|
311
312
|
})
|
|
312
313
|
|
|
313
|
-
cli.command('login', 'Authenticate with the server').action(() => {
|
|
314
|
-
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)
|
|
315
316
|
})
|
|
316
317
|
|
|
317
|
-
cli.command('logout', 'Clear saved credentials').action(() => {
|
|
318
|
-
console.log('Logged out')
|
|
318
|
+
cli.command('logout', 'Clear saved credentials').action((options, { console, process }) => {
|
|
319
|
+
console.log('Logged out', process.env.USER)
|
|
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
|
-
console.log('Fetching status
|
|
325
|
+
.action((options, { console, process }) => {
|
|
326
|
+
console.log('Fetching status from', process.cwd)
|
|
326
327
|
})
|
|
327
328
|
|
|
328
329
|
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
|
-
console.log(`Streaming logs for ${deploymentId}...`)
|
|
333
|
+
.action((deploymentId, options, { console, process }) => {
|
|
334
|
+
console.log(`Streaming logs for ${deploymentId} from ${process.cwd}...`)
|
|
334
335
|
})
|
|
335
336
|
|
|
336
337
|
cli.help()
|
|
@@ -351,8 +352,72 @@ 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 `{ 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.
|
|
361
|
+
|
|
354
362
|
Middleware runs in registration order, after option parsing and validation, but before the matched command's `.action()` callback.
|
|
355
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
|
+
|
|
356
421
|
```ts
|
|
357
422
|
import { goke } from 'goke'
|
|
358
423
|
import { z } from 'zod'
|
|
@@ -362,25 +427,25 @@ const cli = goke('mycli')
|
|
|
362
427
|
cli
|
|
363
428
|
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
364
429
|
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
365
|
-
.use((options) => {
|
|
430
|
+
.use((options, { console, process }) => {
|
|
366
431
|
// options.verbose and options.apiUrl are fully typed here
|
|
367
432
|
if (options.verbose) {
|
|
368
|
-
|
|
433
|
+
console.log('verbose mode enabled in', process.cwd)
|
|
369
434
|
}
|
|
370
435
|
})
|
|
371
436
|
|
|
372
437
|
cli
|
|
373
438
|
.command('deploy <env>', 'Deploy to an environment')
|
|
374
439
|
.option('--dry-run', 'Preview without deploying')
|
|
375
|
-
.action((env, options) => {
|
|
440
|
+
.action((env, options, { console, process }) => {
|
|
376
441
|
// options includes both command options (dryRun) and global options (verbose, apiUrl)
|
|
377
|
-
console.log(`Deploying to ${env} via ${options.apiUrl}`)
|
|
442
|
+
console.log(`Deploying to ${env} via ${options.apiUrl} from ${process.cwd}`)
|
|
378
443
|
})
|
|
379
444
|
|
|
380
445
|
cli
|
|
381
446
|
.command('status', 'Show deployment status')
|
|
382
|
-
.action((options) => {
|
|
383
|
-
console.log('Checking status...')
|
|
447
|
+
.action((options, { console, process }) => {
|
|
448
|
+
console.log('Checking status...', process.stdin)
|
|
384
449
|
})
|
|
385
450
|
|
|
386
451
|
cli.help()
|
|
@@ -392,14 +457,19 @@ Type safety is positional — each `.use()` callback only sees options declared
|
|
|
392
457
|
```ts
|
|
393
458
|
cli
|
|
394
459
|
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
395
|
-
.use((options) => {
|
|
460
|
+
.use((options, { process }) => {
|
|
396
461
|
options.verbose // boolean — typed
|
|
462
|
+
process.argv // string[] — typed
|
|
463
|
+
process.cwd // string — typed
|
|
464
|
+
process.env // Record<string, string> — typed
|
|
465
|
+
process.stdin // string — typed
|
|
397
466
|
options.port // TypeScript error — not declared yet
|
|
398
467
|
})
|
|
399
468
|
.option('--port <port>', z.number().describe('Port'))
|
|
400
|
-
.use((options) => {
|
|
469
|
+
.use((options, { console, process }) => {
|
|
401
470
|
options.verbose // boolean — still visible
|
|
402
471
|
options.port // number — now visible
|
|
472
|
+
console.error('ready', process.cwd)
|
|
403
473
|
})
|
|
404
474
|
```
|
|
405
475
|
|
|
@@ -408,9 +478,10 @@ Middleware supports async functions. If any middleware is async, the remaining m
|
|
|
408
478
|
```ts
|
|
409
479
|
cli
|
|
410
480
|
.option('--token <token>', z.string().describe('API token'))
|
|
411
|
-
.use(async (options) => {
|
|
481
|
+
.use(async (options, { console, process }) => {
|
|
412
482
|
const client = await connectToApi(options.token)
|
|
413
483
|
globalState.client = client
|
|
484
|
+
console.log('connected', process.env.NODE_ENV)
|
|
414
485
|
})
|
|
415
486
|
```
|
|
416
487
|
|
|
@@ -426,8 +497,8 @@ const cli = goke()
|
|
|
426
497
|
cli
|
|
427
498
|
.command('rm <dir>', 'Remove a dir')
|
|
428
499
|
.option('-r, --recursive', 'Remove recursively')
|
|
429
|
-
.action((dir, options) => {
|
|
430
|
-
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''))
|
|
500
|
+
.action((dir, options, { console, process }) => {
|
|
501
|
+
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''), process.cwd)
|
|
431
502
|
})
|
|
432
503
|
|
|
433
504
|
cli.help()
|
|
@@ -444,18 +515,18 @@ import { goke } from 'goke'
|
|
|
444
515
|
|
|
445
516
|
const cli = goke('mycli')
|
|
446
517
|
|
|
447
|
-
cli.command('mcp login <url>', 'Login to MCP server').action((url) => {
|
|
448
|
-
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)
|
|
449
520
|
})
|
|
450
521
|
|
|
451
|
-
cli.command('mcp logout', 'Logout from MCP server').action(() => {
|
|
452
|
-
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)
|
|
453
524
|
})
|
|
454
525
|
|
|
455
526
|
cli
|
|
456
527
|
.command('git remote add <name> <url>', 'Add a git remote')
|
|
457
|
-
.action((name, url) => {
|
|
458
|
-
console.log('Adding remote', name, url)
|
|
528
|
+
.action((name, url, options, { console, process }) => {
|
|
529
|
+
console.log('Adding remote', name, url, 'from', process.cwd)
|
|
459
530
|
})
|
|
460
531
|
|
|
461
532
|
cli.help()
|
|
@@ -479,9 +550,9 @@ cli
|
|
|
479
550
|
.option('--workers <workers>', z.int().describe('Worker count'))
|
|
480
551
|
.option('--tags <tag>', z.array(z.string()).describe('Tags (repeatable)'))
|
|
481
552
|
.option('--verbose', 'Verbose output')
|
|
482
|
-
.action((options) => {
|
|
553
|
+
.action((options, { console, process }) => {
|
|
483
554
|
// options.port is number, options.host is string, etc.
|
|
484
|
-
console.log(options)
|
|
555
|
+
console.log(options, process.env.NODE_ENV)
|
|
485
556
|
})
|
|
486
557
|
|
|
487
558
|
cli.parse()
|
|
@@ -510,9 +581,9 @@ cli
|
|
|
510
581
|
.option('--old-port <port>', z.number().meta({ deprecated: true, description: 'Use --port instead' }))
|
|
511
582
|
// Current option: visible in help
|
|
512
583
|
.option('--port <port>', z.number().describe('Port number'))
|
|
513
|
-
.action((options) => {
|
|
584
|
+
.action((options, { console, process }) => {
|
|
514
585
|
const port = options.port ?? options.oldPort
|
|
515
|
-
console.log('Starting on port', port)
|
|
586
|
+
console.log('Starting on port', port, 'from', process.cwd)
|
|
516
587
|
})
|
|
517
588
|
|
|
518
589
|
cli.help()
|
|
@@ -548,10 +619,10 @@ The last argument of a command can be variadic. To make an argument variadic you
|
|
|
548
619
|
cli
|
|
549
620
|
.command('build <entry> [...otherFiles]', 'Build your app')
|
|
550
621
|
.option('--foo', 'Foo option')
|
|
551
|
-
.action((entry, otherFiles, options) => {
|
|
622
|
+
.action((entry, otherFiles, options, { console, process }) => {
|
|
552
623
|
console.log(entry)
|
|
553
624
|
console.log(otherFiles)
|
|
554
|
-
console.log(options)
|
|
625
|
+
console.log(options, process.stdin)
|
|
555
626
|
})
|
|
556
627
|
```
|
|
557
628
|
|
|
@@ -606,8 +677,8 @@ cli
|
|
|
606
677
|
.command('build', 'desc')
|
|
607
678
|
.option('--env <env>', 'Set envs')
|
|
608
679
|
.example('--env.API_SECRET xxx')
|
|
609
|
-
.action((options) => {
|
|
610
|
-
console.log(options)
|
|
680
|
+
.action((options, { console, process }) => {
|
|
681
|
+
console.log(options, process.env.API_SECRET)
|
|
611
682
|
})
|
|
612
683
|
```
|
|
613
684
|
|
|
@@ -619,9 +690,9 @@ Register a command that will be used when no other command is matched.
|
|
|
619
690
|
cli
|
|
620
691
|
.command('[...files]', 'Build files')
|
|
621
692
|
.option('--minimize', 'Minimize output')
|
|
622
|
-
.action((files, options) => {
|
|
693
|
+
.action((files, options, { console, process }) => {
|
|
623
694
|
console.log(files)
|
|
624
|
-
console.log(options.minimize)
|
|
695
|
+
console.log(options.minimize, process.cwd)
|
|
625
696
|
})
|
|
626
697
|
```
|
|
627
698
|
|
|
@@ -634,11 +705,46 @@ try {
|
|
|
634
705
|
cli.parse(process.argv, { run: false })
|
|
635
706
|
await cli.runMatchedCommand()
|
|
636
707
|
} catch (error) {
|
|
637
|
-
|
|
708
|
+
const message = error instanceof Error ? error.stack : String(error)
|
|
709
|
+
process.stderr.write(String(message) + '\n')
|
|
638
710
|
process.exit(1)
|
|
639
711
|
}
|
|
640
712
|
```
|
|
641
713
|
|
|
714
|
+
### Testing with mocked console and exit
|
|
715
|
+
|
|
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.
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
720
|
+
import { goke, GokeProcessExit } from 'goke'
|
|
721
|
+
|
|
722
|
+
describe('deploy command', () => {
|
|
723
|
+
test('writes output and exits with injected mocks', () => {
|
|
724
|
+
const stdout = { write: vi.fn<(data: string) => void>() }
|
|
725
|
+
const stderr = { write: vi.fn<(data: string) => void>() }
|
|
726
|
+
const exit = vi.fn<(code: number) => void>()
|
|
727
|
+
|
|
728
|
+
const cli = goke('acme', { stdout, stderr, exit })
|
|
729
|
+
|
|
730
|
+
cli
|
|
731
|
+
.command('deploy', 'Deploy the project')
|
|
732
|
+
.action((options, { console, process }) => {
|
|
733
|
+
console.log('deploying')
|
|
734
|
+
process.exit(2)
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
expect(() => {
|
|
738
|
+
cli.parse(['node', 'acme', 'deploy'], { run: true })
|
|
739
|
+
}).toThrow(GokeProcessExit)
|
|
740
|
+
|
|
741
|
+
expect(stdout.write).toHaveBeenCalledWith('deploying\n')
|
|
742
|
+
expect(exit).toHaveBeenCalledWith(2)
|
|
743
|
+
expect(stderr.write).not.toHaveBeenCalled()
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
```
|
|
747
|
+
|
|
642
748
|
### With TypeScript
|
|
643
749
|
|
|
644
750
|
```ts
|
|
@@ -659,11 +765,11 @@ cli
|
|
|
659
765
|
.command('serve <entry>', 'Start the app')
|
|
660
766
|
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
661
767
|
.option('--watch', 'Watch files')
|
|
662
|
-
.action((entry, options) => {
|
|
768
|
+
.action((entry, options, { console, process }) => {
|
|
663
769
|
// entry: string
|
|
664
770
|
// options.port: number
|
|
665
771
|
// options.watch: boolean
|
|
666
|
-
console.log(entry, options.port, options.watch)
|
|
772
|
+
console.log(entry, options.port, options.watch, process.cwd)
|
|
667
773
|
})
|
|
668
774
|
```
|
|
669
775
|
|
|
@@ -677,6 +783,33 @@ import { openInBrowser } from 'goke'
|
|
|
677
783
|
openInBrowser('https://example.com/dashboard')
|
|
678
784
|
```
|
|
679
785
|
|
|
786
|
+
### Expose a goke CLI to JustBash
|
|
787
|
+
|
|
788
|
+
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`.
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
import { goke } from 'goke'
|
|
792
|
+
import { z } from 'zod'
|
|
793
|
+
import { Bash } from 'just-bash'
|
|
794
|
+
|
|
795
|
+
const cli = goke('parent')
|
|
796
|
+
|
|
797
|
+
cli
|
|
798
|
+
.command('child commandwithspaces', 'Run nested command')
|
|
799
|
+
.option('--name <name>', z.string().describe('Name'))
|
|
800
|
+
.action((options, { console, process }) => {
|
|
801
|
+
console.log(`hello ${options.name} from ${process.cwd}`)
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
const bash = new Bash({
|
|
805
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
await bash.exec('parent child commandwithspaces --name Tommy')
|
|
809
|
+
```
|
|
810
|
+
|
|
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()`.
|
|
812
|
+
|
|
680
813
|
## References
|
|
681
814
|
|
|
682
815
|
### CLI Instance
|
|
@@ -712,9 +845,9 @@ Add a global option. The second argument is either:
|
|
|
712
845
|
|
|
713
846
|
#### cli.use(callback)
|
|
714
847
|
|
|
715
|
-
- Type: `(callback: (options: Opts) => void | Promise<void>) => CLI`
|
|
848
|
+
- Type: `(callback: (options: Opts, { fs, console, process }) => void | Promise<void>) => CLI`
|
|
716
849
|
|
|
717
|
-
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.
|
|
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.
|
|
718
851
|
|
|
719
852
|
#### cli.parse(argv?)
|
|
720
853
|
|
|
@@ -768,6 +901,8 @@ Basically the same as `cli.option` but this adds the option to specific command.
|
|
|
768
901
|
|
|
769
902
|
- Type: `(callback: ActionCallback) => Command`
|
|
770
903
|
|
|
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.
|
|
905
|
+
|
|
771
906
|
#### command.alias(name)
|
|
772
907
|
|
|
773
908
|
- Type: `(name: string) => Command`
|
|
@@ -798,7 +933,7 @@ cli.on('command:!', () => {
|
|
|
798
933
|
})
|
|
799
934
|
|
|
800
935
|
cli.on('command:*', () => {
|
|
801
|
-
|
|
936
|
+
process.stderr.write(`Invalid command: ${cli.args.join(' ')}\n`)
|
|
802
937
|
process.exit(1)
|
|
803
938
|
})
|
|
804
939
|
```
|
|
@@ -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([
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"just-bash.test.d.ts","sourceRoot":"","sources":["../../src/__test__/just-bash.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|