goke 6.4.0 → 6.5.1
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 +261 -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 +25 -3
- 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/picocolors.d.ts +55 -0
- package/dist/picocolors.d.ts.map +1 -0
- package/dist/picocolors.js +78 -0
- 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 +3 -4
- 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 +40 -5
- package/src/index.ts +1 -1
- package/src/just-bash.ts +187 -2
- package/src/picocolors.ts +140 -0
- 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
|
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
npm install goke
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Install skill for AI agents
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y skills add remorses/goke
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This installs the repository skill for AI coding agents. In this repo the shipped skill lives at `skills/goke/SKILL.md`.
|
|
35
|
+
|
|
28
36
|
## Usage
|
|
29
37
|
|
|
30
38
|
### Simple Parsing
|
|
@@ -44,8 +52,8 @@ cli.option(
|
|
|
44
52
|
)
|
|
45
53
|
cli.option('--name <name>', 'Provide your name')
|
|
46
54
|
|
|
47
|
-
cli.command('lint [...files]', 'Lint files').action((files, options, { console }) => {
|
|
48
|
-
console.log(files, options)
|
|
55
|
+
cli.command('lint [...files]', 'Lint files').action((files, options, { console, process }) => {
|
|
56
|
+
console.log(files, options, process.cwd)
|
|
49
57
|
})
|
|
50
58
|
|
|
51
59
|
cli
|
|
@@ -53,8 +61,8 @@ cli
|
|
|
53
61
|
.option('--minify', 'Minify output')
|
|
54
62
|
.example('build src/index.ts')
|
|
55
63
|
.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)
|
|
64
|
+
.action(async (entry, options, { console, process }) => { // options is type safe! no need to type it
|
|
65
|
+
console.log(entry, options, process.env.NODE_ENV)
|
|
58
66
|
})
|
|
59
67
|
|
|
60
68
|
cli.example((bin) => `${bin} lint src/**/*.ts`)
|
|
@@ -133,8 +141,8 @@ cli
|
|
|
133
141
|
.option('--channel <name>', 'Target channel: stable, beta, alpha')
|
|
134
142
|
.option('--notes-file <path>', 'Markdown file used as release notes')
|
|
135
143
|
.option('--dry-run', 'Preview every step without publishing')
|
|
136
|
-
.action((version, options, { console }) => {
|
|
137
|
-
console.log('release', version, options)
|
|
144
|
+
.action((version, options, { console, process }) => {
|
|
145
|
+
console.log('release', version, options, process.cwd)
|
|
138
146
|
})
|
|
139
147
|
|
|
140
148
|
cli
|
|
@@ -159,8 +167,8 @@ cli
|
|
|
159
167
|
.option('--target <migration>', 'Apply up to a specific migration id')
|
|
160
168
|
.option('--dry-run', 'Print plan only, do not execute SQL')
|
|
161
169
|
.option('--verbose', 'Show each executed statement')
|
|
162
|
-
.action((options, { console }) => {
|
|
163
|
-
console.log('migrate', options)
|
|
170
|
+
.action((options, { console, process }) => {
|
|
171
|
+
console.log('migrate', options, process.stdin)
|
|
164
172
|
})
|
|
165
173
|
|
|
166
174
|
cli.help()
|
|
@@ -299,39 +307,39 @@ cli
|
|
|
299
307
|
z.string().default('production').describe('Target environment'),
|
|
300
308
|
)
|
|
301
309
|
.option('--dry-run', 'Preview without deploying')
|
|
302
|
-
.action((options, { console }) => {
|
|
303
|
-
console.log(`Deploying to ${options.env}...`)
|
|
310
|
+
.action((options, { console, process }) => {
|
|
311
|
+
console.log(`Deploying to ${options.env} from ${process.cwd}...`)
|
|
304
312
|
})
|
|
305
313
|
|
|
306
314
|
// Subcommands
|
|
307
315
|
cli
|
|
308
316
|
.command('init', 'Initialize a new project')
|
|
309
317
|
.option('--template <template>', 'Project template')
|
|
310
|
-
.action((options, { console }) => {
|
|
311
|
-
console.log('Initializing project
|
|
318
|
+
.action((options, { console, process }) => {
|
|
319
|
+
console.log('Initializing project in', process.cwd)
|
|
312
320
|
})
|
|
313
321
|
|
|
314
|
-
cli.command('login', 'Authenticate with the server').action((options, { console }) => {
|
|
315
|
-
console.log('Opening browser for login
|
|
322
|
+
cli.command('login', 'Authenticate with the server').action((options, { console, process }) => {
|
|
323
|
+
console.log('Opening browser for login from', process.cwd)
|
|
316
324
|
})
|
|
317
325
|
|
|
318
|
-
cli.command('logout', 'Clear saved credentials').action((options, { console }) => {
|
|
319
|
-
console.log('Logged out')
|
|
326
|
+
cli.command('logout', 'Clear saved credentials').action((options, { console, process }) => {
|
|
327
|
+
console.log('Logged out', process.env.USER)
|
|
320
328
|
})
|
|
321
329
|
|
|
322
330
|
cli
|
|
323
331
|
.command('status', 'Show deployment status')
|
|
324
332
|
.option('--json', 'Output as JSON')
|
|
325
|
-
.action((options, { console }) => {
|
|
326
|
-
console.log('Fetching status
|
|
333
|
+
.action((options, { console, process }) => {
|
|
334
|
+
console.log('Fetching status from', process.cwd)
|
|
327
335
|
})
|
|
328
336
|
|
|
329
337
|
cli
|
|
330
338
|
.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
331
339
|
.option('--follow', 'Follow log output')
|
|
332
340
|
.option('--lines <n>', z.number().default(100).describe('Number of lines'))
|
|
333
|
-
.action((deploymentId, options, { console }) => {
|
|
334
|
-
console.log(`Streaming logs for ${deploymentId}...`)
|
|
341
|
+
.action((deploymentId, options, { console, process }) => {
|
|
342
|
+
console.log(`Streaming logs for ${deploymentId} from ${process.cwd}...`)
|
|
335
343
|
})
|
|
336
344
|
|
|
337
345
|
cli.help()
|
|
@@ -352,10 +360,88 @@ deploy --help # shows all commands
|
|
|
352
360
|
|
|
353
361
|
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
362
|
|
|
355
|
-
Prefer the injected `{ console, process }` argument over global `console
|
|
363
|
+
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.
|
|
364
|
+
|
|
365
|
+
`process.cwd`, `process.stdin`, and `process.env` come from the active runtime:
|
|
366
|
+
|
|
367
|
+
- 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.
|
|
368
|
+
- In JustBash runs, those same fields are populated from the sandbox execution context.
|
|
356
369
|
|
|
357
370
|
Middleware runs in registration order, after option parsing and validation, but before the matched command's `.action()` callback.
|
|
358
371
|
|
|
372
|
+
### Filesystem Access
|
|
373
|
+
|
|
374
|
+
The injected `fs` object is the recommended way to read or write CLI state.
|
|
375
|
+
|
|
376
|
+
- In normal Node.js runs, `fs` defaults to `node:fs/promises`
|
|
377
|
+
- In JustBash runs, `goke` swaps in a compatible adapter over the JustBash virtual filesystem
|
|
378
|
+
|
|
379
|
+
This makes storage-style commands work in both environments without branching on runtime details.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
cli
|
|
383
|
+
.command('login', 'Save auth token')
|
|
384
|
+
.option('--token <token>', z.string().describe('Auth token'))
|
|
385
|
+
.action(async (options, { fs, console, process }) => {
|
|
386
|
+
await fs.mkdir('.mycli', { recursive: true })
|
|
387
|
+
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
|
|
388
|
+
console.log('saved credentials in', process.cwd)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
cli
|
|
392
|
+
.command('whoami', 'Read saved auth token')
|
|
393
|
+
.action(async (options, { fs, console, process }) => {
|
|
394
|
+
const auth = await fs.readFile('.mycli/auth.json', 'utf8')
|
|
395
|
+
console.log(auth, process.env.USER)
|
|
396
|
+
})
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
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.
|
|
400
|
+
|
|
401
|
+
### Path handling
|
|
402
|
+
|
|
403
|
+
Use relative paths with injected `fs` for routine CLI storage paths. When a helper needs to resolve from the current directory, pass injected `process.cwd` into that helper and resolve from there.
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
await fs.mkdir('.mycli', { recursive: true })
|
|
407
|
+
await fs.writeFile('.mycli/auth.json', json, 'utf8')
|
|
408
|
+
console.log('running from', process.cwd)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Why this works:
|
|
412
|
+
|
|
413
|
+
- In normal Node.js runs, relative paths resolve against the host cwd.
|
|
414
|
+
- In JustBash runs, the same relative paths resolve against the sandbox cwd.
|
|
415
|
+
- `process.cwd` mirrors that runtime-specific cwd in both environments.
|
|
416
|
+
|
|
417
|
+
`goke` also exports the runtime types, so helper functions can use dependency injection without reaching for globals:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { goke } from 'goke'
|
|
421
|
+
import type { GokeFs, GokeProcess } from 'goke'
|
|
422
|
+
|
|
423
|
+
async function saveAuthToken(args: {
|
|
424
|
+
fs: GokeFs
|
|
425
|
+
process: GokeProcess
|
|
426
|
+
token: string
|
|
427
|
+
}) {
|
|
428
|
+
await args.fs.mkdir('.mycli', { recursive: true })
|
|
429
|
+
await args.fs.writeFile('.mycli/auth.json', JSON.stringify({
|
|
430
|
+
token,
|
|
431
|
+
cwd: args.process.cwd,
|
|
432
|
+
}), 'utf8')
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const cli = goke('mycli')
|
|
436
|
+
|
|
437
|
+
cli
|
|
438
|
+
.command('login <token>', 'Save auth token')
|
|
439
|
+
.action(async (token, options, { fs, process, console }) => {
|
|
440
|
+
await saveAuthToken({ fs, process, token })
|
|
441
|
+
console.log('saved credentials')
|
|
442
|
+
})
|
|
443
|
+
```
|
|
444
|
+
|
|
359
445
|
```ts
|
|
360
446
|
import { goke } from 'goke'
|
|
361
447
|
import { z } from 'zod'
|
|
@@ -365,25 +451,25 @@ const cli = goke('mycli')
|
|
|
365
451
|
cli
|
|
366
452
|
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
367
453
|
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
368
|
-
.use((options, { console }) => {
|
|
454
|
+
.use((options, { console, process }) => {
|
|
369
455
|
// options.verbose and options.apiUrl are fully typed here
|
|
370
456
|
if (options.verbose) {
|
|
371
|
-
console.log('verbose mode enabled')
|
|
457
|
+
console.log('verbose mode enabled in', process.cwd)
|
|
372
458
|
}
|
|
373
459
|
})
|
|
374
460
|
|
|
375
461
|
cli
|
|
376
462
|
.command('deploy <env>', 'Deploy to an environment')
|
|
377
463
|
.option('--dry-run', 'Preview without deploying')
|
|
378
|
-
.action((env, options, { console }) => {
|
|
464
|
+
.action((env, options, { console, process }) => {
|
|
379
465
|
// options includes both command options (dryRun) and global options (verbose, apiUrl)
|
|
380
|
-
console.log(`Deploying to ${env} via ${options.apiUrl}`)
|
|
466
|
+
console.log(`Deploying to ${env} via ${options.apiUrl} from ${process.cwd}`)
|
|
381
467
|
})
|
|
382
468
|
|
|
383
469
|
cli
|
|
384
470
|
.command('status', 'Show deployment status')
|
|
385
|
-
.action((options, { console }) => {
|
|
386
|
-
console.log('Checking status...')
|
|
471
|
+
.action((options, { console, process }) => {
|
|
472
|
+
console.log('Checking status...', process.stdin)
|
|
387
473
|
})
|
|
388
474
|
|
|
389
475
|
cli.help()
|
|
@@ -398,13 +484,16 @@ cli
|
|
|
398
484
|
.use((options, { process }) => {
|
|
399
485
|
options.verbose // boolean — typed
|
|
400
486
|
process.argv // string[] — typed
|
|
487
|
+
process.cwd // string — typed
|
|
488
|
+
process.env // Record<string, string> — typed
|
|
489
|
+
process.stdin // string — typed
|
|
401
490
|
options.port // TypeScript error — not declared yet
|
|
402
491
|
})
|
|
403
492
|
.option('--port <port>', z.number().describe('Port'))
|
|
404
|
-
.use((options, { console }) => {
|
|
493
|
+
.use((options, { console, process }) => {
|
|
405
494
|
options.verbose // boolean — still visible
|
|
406
495
|
options.port // number — now visible
|
|
407
|
-
console.error('ready')
|
|
496
|
+
console.error('ready', process.cwd)
|
|
408
497
|
})
|
|
409
498
|
```
|
|
410
499
|
|
|
@@ -413,10 +502,10 @@ Middleware supports async functions. If any middleware is async, the remaining m
|
|
|
413
502
|
```ts
|
|
414
503
|
cli
|
|
415
504
|
.option('--token <token>', z.string().describe('API token'))
|
|
416
|
-
.use(async (options, { console }) => {
|
|
505
|
+
.use(async (options, { console, process }) => {
|
|
417
506
|
const client = await connectToApi(options.token)
|
|
418
507
|
globalState.client = client
|
|
419
|
-
console.log('connected')
|
|
508
|
+
console.log('connected', process.env.NODE_ENV)
|
|
420
509
|
})
|
|
421
510
|
```
|
|
422
511
|
|
|
@@ -432,8 +521,8 @@ const cli = goke()
|
|
|
432
521
|
cli
|
|
433
522
|
.command('rm <dir>', 'Remove a dir')
|
|
434
523
|
.option('-r, --recursive', 'Remove recursively')
|
|
435
|
-
.action((dir, options, { console }) => {
|
|
436
|
-
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''))
|
|
524
|
+
.action((dir, options, { console, process }) => {
|
|
525
|
+
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''), process.cwd)
|
|
437
526
|
})
|
|
438
527
|
|
|
439
528
|
cli.help()
|
|
@@ -450,18 +539,18 @@ import { goke } from 'goke'
|
|
|
450
539
|
|
|
451
540
|
const cli = goke('mycli')
|
|
452
541
|
|
|
453
|
-
cli.command('mcp login <url>', 'Login to MCP server').action((url, options, { console }) => {
|
|
454
|
-
console.log('Logging in to', url)
|
|
542
|
+
cli.command('mcp login <url>', 'Login to MCP server').action((url, options, { console, process }) => {
|
|
543
|
+
console.log('Logging in to', url, 'from', process.cwd)
|
|
455
544
|
})
|
|
456
545
|
|
|
457
|
-
cli.command('mcp logout', 'Logout from MCP server').action((options, { console }) => {
|
|
458
|
-
console.log('Logged out')
|
|
546
|
+
cli.command('mcp logout', 'Logout from MCP server').action((options, { console, process }) => {
|
|
547
|
+
console.log('Logged out', process.env.USER)
|
|
459
548
|
})
|
|
460
549
|
|
|
461
550
|
cli
|
|
462
551
|
.command('git remote add <name> <url>', 'Add a git remote')
|
|
463
|
-
.action((name, url, options, { console }) => {
|
|
464
|
-
console.log('Adding remote', name, url)
|
|
552
|
+
.action((name, url, options, { console, process }) => {
|
|
553
|
+
console.log('Adding remote', name, url, 'from', process.cwd)
|
|
465
554
|
})
|
|
466
555
|
|
|
467
556
|
cli.help()
|
|
@@ -485,9 +574,9 @@ cli
|
|
|
485
574
|
.option('--workers <workers>', z.int().describe('Worker count'))
|
|
486
575
|
.option('--tags <tag>', z.array(z.string()).describe('Tags (repeatable)'))
|
|
487
576
|
.option('--verbose', 'Verbose output')
|
|
488
|
-
.action((options, { console }) => {
|
|
577
|
+
.action((options, { console, process }) => {
|
|
489
578
|
// options.port is number, options.host is string, etc.
|
|
490
|
-
console.log(options)
|
|
579
|
+
console.log(options, process.env.NODE_ENV)
|
|
491
580
|
})
|
|
492
581
|
|
|
493
582
|
cli.parse()
|
|
@@ -516,9 +605,9 @@ cli
|
|
|
516
605
|
.option('--old-port <port>', z.number().meta({ deprecated: true, description: 'Use --port instead' }))
|
|
517
606
|
// Current option: visible in help
|
|
518
607
|
.option('--port <port>', z.number().describe('Port number'))
|
|
519
|
-
.action((options, { console }) => {
|
|
608
|
+
.action((options, { console, process }) => {
|
|
520
609
|
const port = options.port ?? options.oldPort
|
|
521
|
-
console.log('Starting on port', port)
|
|
610
|
+
console.log('Starting on port', port, 'from', process.cwd)
|
|
522
611
|
})
|
|
523
612
|
|
|
524
613
|
cli.help()
|
|
@@ -554,10 +643,10 @@ The last argument of a command can be variadic. To make an argument variadic you
|
|
|
554
643
|
cli
|
|
555
644
|
.command('build <entry> [...otherFiles]', 'Build your app')
|
|
556
645
|
.option('--foo', 'Foo option')
|
|
557
|
-
.action((entry, otherFiles, options, { console }) => {
|
|
646
|
+
.action((entry, otherFiles, options, { console, process }) => {
|
|
558
647
|
console.log(entry)
|
|
559
648
|
console.log(otherFiles)
|
|
560
|
-
console.log(options)
|
|
649
|
+
console.log(options, process.stdin)
|
|
561
650
|
})
|
|
562
651
|
```
|
|
563
652
|
|
|
@@ -612,8 +701,8 @@ cli
|
|
|
612
701
|
.command('build', 'desc')
|
|
613
702
|
.option('--env <env>', 'Set envs')
|
|
614
703
|
.example('--env.API_SECRET xxx')
|
|
615
|
-
.action((options, { console }) => {
|
|
616
|
-
console.log(options)
|
|
704
|
+
.action((options, { console, process }) => {
|
|
705
|
+
console.log(options, process.env.API_SECRET)
|
|
617
706
|
})
|
|
618
707
|
```
|
|
619
708
|
|
|
@@ -625,9 +714,9 @@ Register a command that will be used when no other command is matched.
|
|
|
625
714
|
cli
|
|
626
715
|
.command('[...files]', 'Build files')
|
|
627
716
|
.option('--minimize', 'Minimize output')
|
|
628
|
-
.action((files, options, { console }) => {
|
|
717
|
+
.action((files, options, { console, process }) => {
|
|
629
718
|
console.log(files)
|
|
630
|
-
console.log(options.minimize)
|
|
719
|
+
console.log(options.minimize, process.cwd)
|
|
631
720
|
})
|
|
632
721
|
```
|
|
633
722
|
|
|
@@ -648,7 +737,7 @@ try {
|
|
|
648
737
|
|
|
649
738
|
### Testing with mocked console and exit
|
|
650
739
|
|
|
651
|
-
Because goke derives its injected `{ console, process }` from the CLI's configured
|
|
740
|
+
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
741
|
|
|
653
742
|
```ts
|
|
654
743
|
import { describe, expect, test, vi } from 'vitest'
|
|
@@ -700,11 +789,11 @@ cli
|
|
|
700
789
|
.command('serve <entry>', 'Start the app')
|
|
701
790
|
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
702
791
|
.option('--watch', 'Watch files')
|
|
703
|
-
.action((entry, options, { console }) => {
|
|
792
|
+
.action((entry, options, { console, process }) => {
|
|
704
793
|
// entry: string
|
|
705
794
|
// options.port: number
|
|
706
795
|
// options.watch: boolean
|
|
707
|
-
console.log(entry, options.port, options.watch)
|
|
796
|
+
console.log(entry, options.port, options.watch, process.cwd)
|
|
708
797
|
})
|
|
709
798
|
```
|
|
710
799
|
|
|
@@ -732,8 +821,8 @@ const cli = goke('parent')
|
|
|
732
821
|
cli
|
|
733
822
|
.command('child commandwithspaces', 'Run nested command')
|
|
734
823
|
.option('--name <name>', z.string().describe('Name'))
|
|
735
|
-
.action((options, { console }) => {
|
|
736
|
-
console.log(`hello ${options.name}`)
|
|
824
|
+
.action((options, { console, process }) => {
|
|
825
|
+
console.log(`hello ${options.name} from ${process.cwd}`)
|
|
737
826
|
})
|
|
738
827
|
|
|
739
828
|
const bash = new Bash({
|
|
@@ -743,7 +832,79 @@ const bash = new Bash({
|
|
|
743
832
|
await bash.exec('parent child commandwithspaces --name Tommy')
|
|
744
833
|
```
|
|
745
834
|
|
|
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.
|
|
835
|
+
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()`.
|
|
836
|
+
|
|
837
|
+
### Test with real JustBash
|
|
838
|
+
|
|
839
|
+
When a command reads or writes files, test it through real `just-bash` using the existing app CLI. Do not define the CLI inside the test body.
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
import { describe, expect, test } from 'vitest'
|
|
843
|
+
import { Bash, InMemoryFs } from 'just-bash'
|
|
844
|
+
import { cli } from '../src/cli'
|
|
845
|
+
|
|
846
|
+
describe('login command', () => {
|
|
847
|
+
test('writes auth state through the sandbox fs', async () => {
|
|
848
|
+
const virtualFs = new InMemoryFs()
|
|
849
|
+
await virtualFs.mkdir('/project', { recursive: true })
|
|
850
|
+
|
|
851
|
+
const bash = new Bash({
|
|
852
|
+
fs: virtualFs,
|
|
853
|
+
cwd: '/project',
|
|
854
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
const result = await bash.exec('parent login --token Tommy')
|
|
858
|
+
|
|
859
|
+
expect(result.stdout).toBe('saved credentials\n')
|
|
860
|
+
expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe(
|
|
861
|
+
'{"token":"Tommy","cwd":"/project"}',
|
|
862
|
+
)
|
|
863
|
+
})
|
|
864
|
+
})
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
This is the recommended compatibility test whenever a CLI touches storage: run the same CLI once in normal tests and once through real `just-bash`.
|
|
868
|
+
|
|
869
|
+
### Exposing your CLI as a skill
|
|
870
|
+
|
|
871
|
+
If you build a CLI with goke, keep the skill minimal and point agents to the CLI help output. Put detailed usage in the CLI code and README, not in a duplicated skill file.
|
|
872
|
+
|
|
873
|
+
````markdown
|
|
874
|
+
<!-- skills/acme/SKILL.md -->
|
|
875
|
+
---
|
|
876
|
+
name: acme
|
|
877
|
+
description: >
|
|
878
|
+
acme is a deployment CLI. Always run `acme --help` before using it
|
|
879
|
+
to discover available commands, options, and usage examples.
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
# acme
|
|
883
|
+
|
|
884
|
+
Always run `acme --help` before using this CLI.
|
|
885
|
+
For subcommand details: `acme <command> --help`
|
|
886
|
+
````
|
|
887
|
+
|
|
888
|
+
## Contributor Notes
|
|
889
|
+
|
|
890
|
+
### Rules
|
|
891
|
+
|
|
892
|
+
1. Use schema-based options for typed values.
|
|
893
|
+
2. Do not repeat defaults in `.describe(...)` when using `.default()`.
|
|
894
|
+
3. Do not manually type action callback arguments; let goke infer them.
|
|
895
|
+
4. Prefer injected `{ fs, console, process }` over global `console`, `process.exit`, or direct `node:fs/promises` imports.
|
|
896
|
+
5. Use implicit cwd with injected `fs` for CLI storage. When a helper needs current-cwd semantics, pass `process.cwd` from the injected context into that helper.
|
|
897
|
+
6. Define the CLI in app code and import that same CLI in tests; do not construct a separate CLI inside compatibility tests.
|
|
898
|
+
|
|
899
|
+
### Version
|
|
900
|
+
|
|
901
|
+
Import `package.json` and use its version field so the CLI stays in sync automatically:
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
import pkg from './package.json' with { type: 'json' }
|
|
905
|
+
|
|
906
|
+
cli.version(pkg.version)
|
|
907
|
+
```
|
|
747
908
|
|
|
748
909
|
## References
|
|
749
910
|
|
|
@@ -780,9 +941,9 @@ Add a global option. The second argument is either:
|
|
|
780
941
|
|
|
781
942
|
#### cli.use(callback)
|
|
782
943
|
|
|
783
|
-
- Type: `(callback: (options: Opts, { console, process }) => void | Promise<void>) => CLI`
|
|
944
|
+
- Type: `(callback: (options: Opts, { fs, console, process }) => void | Promise<void>) => CLI`
|
|
784
945
|
|
|
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.
|
|
946
|
+
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
947
|
|
|
787
948
|
#### cli.parse(argv?)
|
|
788
949
|
|
|
@@ -802,6 +963,26 @@ Register a middleware function that runs before the matched command action. Midd
|
|
|
802
963
|
|
|
803
964
|
Print the help message to stdout.
|
|
804
965
|
|
|
966
|
+
#### cli.clone(options?)
|
|
967
|
+
|
|
968
|
+
- Type: `(options?: GokeOptions) => Goke`
|
|
969
|
+
|
|
970
|
+
Create a deep copy of the CLI instance with all commands, options, middleware, and event listeners. Override any `GokeOptions` (stdout, stderr, cwd, env, fs, argv, columns, exit) in the clone without affecting the original. Primarily useful in tests to run multiple isolated parses from the same CLI definition:
|
|
971
|
+
|
|
972
|
+
```ts
|
|
973
|
+
const cli = goke('mycli')
|
|
974
|
+
cli.command('build', 'Build project').action((options, { console }) => {
|
|
975
|
+
console.log('building')
|
|
976
|
+
})
|
|
977
|
+
cli.help()
|
|
978
|
+
|
|
979
|
+
// In tests: override streams without touching the original CLI
|
|
980
|
+
const stdout = { write: vi.fn<(data: string) => void>() }
|
|
981
|
+
const isolated = cli.clone({ stdout })
|
|
982
|
+
isolated.parse(['node', 'mycli', 'build'])
|
|
983
|
+
expect(stdout.write).toHaveBeenCalledWith('building\n')
|
|
984
|
+
```
|
|
985
|
+
|
|
805
986
|
#### cli.helpText()
|
|
806
987
|
|
|
807
988
|
- Type: `() => string`
|
|
@@ -836,7 +1017,7 @@ Basically the same as `cli.option` but this adds the option to specific command.
|
|
|
836
1017
|
|
|
837
1018
|
- Type: `(callback: ActionCallback) => Command`
|
|
838
1019
|
|
|
839
|
-
Command callbacks receive positional args first, then parsed options, then an injected `{ console, process }` object. Prefer those injected helpers over global `console
|
|
1020
|
+
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
1021
|
|
|
841
1022
|
#### command.alias(name)
|
|
842
1023
|
|
|
@@ -846,6 +1027,21 @@ Command callbacks receive positional args first, then parsed options, then an in
|
|
|
846
1027
|
|
|
847
1028
|
- Type: `() => Command`
|
|
848
1029
|
|
|
1030
|
+
#### command.hidden()
|
|
1031
|
+
|
|
1032
|
+
- Type: `() => Command`
|
|
1033
|
+
|
|
1034
|
+
Hide a command from the help output listing. The command still matches and runs when invoked directly — only the help display is suppressed. Useful for internal, deprecated, or experimental commands you don't want to advertise.
|
|
1035
|
+
|
|
1036
|
+
```ts
|
|
1037
|
+
cli
|
|
1038
|
+
.command('internal-reset', 'Reset internal state')
|
|
1039
|
+
.hidden()
|
|
1040
|
+
.action((options, { console }) => {
|
|
1041
|
+
console.log('reset done')
|
|
1042
|
+
})
|
|
1043
|
+
```
|
|
1044
|
+
|
|
849
1045
|
#### command.example(example)
|
|
850
1046
|
|
|
851
1047
|
- Type: `(example: CommandExample) => Command`
|
|
@@ -854,6 +1050,12 @@ Command callbacks receive positional args first, then parsed options, then an in
|
|
|
854
1050
|
|
|
855
1051
|
- Type: `(text: string) => Command`
|
|
856
1052
|
|
|
1053
|
+
#### command.helpText()
|
|
1054
|
+
|
|
1055
|
+
- Type: `() => string`
|
|
1056
|
+
|
|
1057
|
+
Return the formatted help string for this specific command without printing it. Useful for tests or embedding help text programmatically.
|
|
1058
|
+
|
|
857
1059
|
### Events
|
|
858
1060
|
|
|
859
1061
|
Listen to commands:
|
|
@@ -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([
|