goke 6.7.0 → 6.9.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/dist/__test__/index.test.js +124 -0
- package/dist/__test__/just-bash.test.js +109 -0
- package/dist/__test__/types.test-d.js +37 -0
- package/dist/goke.d.ts +99 -2
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +62 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/just-bash.d.ts +1 -1
- package/dist/just-bash.d.ts.map +1 -1
- package/dist/just-bash.js +80 -15
- package/package.json +1 -1
- package/src/__test__/index.test.ts +166 -0
- package/src/__test__/just-bash.test.ts +125 -1
- package/src/__test__/types.test-d.ts +41 -0
- package/src/goke.ts +130 -13
- package/src/index.ts +1 -1
- package/src/just-bash.ts +92 -18
- package/README.md +0 -1254
package/README.md
DELETED
|
@@ -1,1254 +0,0 @@
|
|
|
1
|
-
<div align='center'>
|
|
2
|
-
<br/>
|
|
3
|
-
<br/>
|
|
4
|
-
<h3>goke</h3>
|
|
5
|
-
<p>Build CLIs like you'd build an API. Type-safe, chainable, zero dependencies.</p>
|
|
6
|
-
<br/>
|
|
7
|
-
<br/>
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
goke is a TypeScript CLI framework with a [Hono](https://hono.dev)-like API. You chain `.use()` for middleware and `.command()` for routes — the same mental model as a REST API, applied to the terminal.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { goke } from 'goke'
|
|
14
|
-
import { z } from 'zod'
|
|
15
|
-
|
|
16
|
-
const cli = goke('deploy')
|
|
17
|
-
|
|
18
|
-
// middleware — runs before every command
|
|
19
|
-
cli
|
|
20
|
-
.option('--env <env>', z.enum(['staging', 'production']).default('staging').describe('Target environment'))
|
|
21
|
-
.use((options, { console }) => {
|
|
22
|
-
console.log(`Environment: ${options.env}`)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// commands — like route handlers
|
|
26
|
-
cli
|
|
27
|
-
.command('up', 'Deploy the app')
|
|
28
|
-
.option('--dry-run', 'Preview without deploying')
|
|
29
|
-
.action((options, { console, process }) => {
|
|
30
|
-
console.log(`Deploying from ${process.cwd}`)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
cli
|
|
34
|
-
.command('logs <deploymentId>', 'Stream logs')
|
|
35
|
-
.option('--lines <n>', z.number().default(100).describe('Lines to tail'))
|
|
36
|
-
.action((id, options) => streamLogs(id, options.lines))
|
|
37
|
-
|
|
38
|
-
cli.help()
|
|
39
|
-
cli.parse()
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Features
|
|
43
|
-
|
|
44
|
-
- **Hono-like chaining** — `.use()` for middleware, `.command()` for handlers. Build a CLI the same way you'd design a REST API.
|
|
45
|
-
- **Zod type safety** — pass a Zod schema to `.option()` and get automatic coercion, TypeScript inference, and help text for free. Works with Valibot, ArkType, or any Standard Schema library.
|
|
46
|
-
- **MCP server in 2 lines** — expose your entire CLI as an MCP server with `createMcpAction({ cli })`. Every command becomes a tool, ready for Claude Desktop, Cursor, VS Code, and [any MCP client](https://github.com/supermemoryai/install-mcp#supported-clients).
|
|
47
|
-
- **JustBash support** — `cli.createJustBashCommand()` exposes your CLI as a sandboxed JustBash command. Same action code, no changes needed.
|
|
48
|
-
- **Space-separated subcommands** — `git remote add`, `mcp login`, `db migrate` — multi-word commands work out of the box.
|
|
49
|
-
- **Injected `{ fs, console, process }`** — commands receive a portable runtime context. Swap it in tests, or let JustBash replace it with a sandbox. No global side effects.
|
|
50
|
-
- **Zero runtime dependencies** — install `goke` without pulling extra runtime packages into your CLI.
|
|
51
|
-
|
|
52
|
-
## Install
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
npm install goke
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Install skill for AI agents
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
npx -y skills add remorses/goke
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
This installs the repository skill for AI coding agents. In this repo the shipped skill lives at `skills/goke/SKILL.md`.
|
|
65
|
-
|
|
66
|
-
## Usage
|
|
67
|
-
|
|
68
|
-
### Simple Parsing
|
|
69
|
-
|
|
70
|
-
Use goke as simple argument parser:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import { goke } from 'goke'
|
|
75
|
-
import { z } from 'zod'
|
|
76
|
-
|
|
77
|
-
const cli = goke()
|
|
78
|
-
|
|
79
|
-
cli.option(
|
|
80
|
-
'--type [type]',
|
|
81
|
-
z.string().default('node').describe('Choose a project type'),
|
|
82
|
-
)
|
|
83
|
-
cli.option('--name <name>', 'Provide your name')
|
|
84
|
-
|
|
85
|
-
cli.command('lint [...files]', 'Lint files').action((files, options, { console, process }) => {
|
|
86
|
-
console.log(files, options, process.cwd)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
cli
|
|
90
|
-
.command('build [entry]', 'Build your app')
|
|
91
|
-
.option('--minify', 'Minify output')
|
|
92
|
-
.example('build src/index.ts')
|
|
93
|
-
.example('build src/index.ts --minify')
|
|
94
|
-
.action(async (entry, options, { console, process }) => { // options is type safe! no need to type it
|
|
95
|
-
console.log(entry, options, process.env.NODE_ENV)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
cli.example((bin) => `${bin} lint src/**/*.ts`)
|
|
99
|
-
|
|
100
|
-
// Display help message when `-h` or `--help` appears
|
|
101
|
-
cli.help()
|
|
102
|
-
// Display version number when `-v` or `--version` appears
|
|
103
|
-
cli.version('0.0.0')
|
|
104
|
-
|
|
105
|
-
cli.parse()
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
When examples are defined, help output includes an **Examples** section.
|
|
109
|
-
|
|
110
|
-
### Command Examples in Help
|
|
111
|
-
|
|
112
|
-
Use `.example(...)` on a command (or on `cli`) to show usage snippets in help:
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
import { goke } from 'goke'
|
|
116
|
-
|
|
117
|
-
const cli = goke('mycli')
|
|
118
|
-
|
|
119
|
-
cli
|
|
120
|
-
.command('deploy', 'Deploy current app')
|
|
121
|
-
.option('--env <env>', 'Target environment')
|
|
122
|
-
.example('mycli deploy --env production')
|
|
123
|
-
.example('mycli deploy --env staging')
|
|
124
|
-
.action(() => {})
|
|
125
|
-
|
|
126
|
-
cli.example((bin) => `${bin} deploy --env production`)
|
|
127
|
-
|
|
128
|
-
cli.help()
|
|
129
|
-
cli.parse()
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Rich Multi-line Command Descriptions (`string-dedent`)
|
|
133
|
-
|
|
134
|
-
When a command needs a long description (with bullets, quotes, inline code, and
|
|
135
|
-
multiple examples), use [`string-dedent`](https://www.npmjs.com/package/string-dedent)
|
|
136
|
-
to keep the source readable while preserving clean help output.
|
|
137
|
-
|
|
138
|
-
Install:
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
npm install string-dedent
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Example with detailed command descriptions:
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
import { goke } from 'goke'
|
|
148
|
-
import dedent from 'string-dedent'
|
|
149
|
-
|
|
150
|
-
const cli = goke('acme')
|
|
151
|
-
|
|
152
|
-
cli
|
|
153
|
-
.command(
|
|
154
|
-
'release <version>',
|
|
155
|
-
dedent`
|
|
156
|
-
Publish a versioned release to your distribution channels.
|
|
157
|
-
|
|
158
|
-
- **Validates** release metadata and changelog before publishing.
|
|
159
|
-
- **Builds** production artifacts with reproducible settings.
|
|
160
|
-
- **Tags** git history using semantic version format.
|
|
161
|
-
- **Publishes** to npm and creates release notes.
|
|
162
|
-
|
|
163
|
-
> Recommended flow: run with \`--dry-run\` first in CI to verify output.
|
|
164
|
-
|
|
165
|
-
Examples:
|
|
166
|
-
- \`acme release 2.4.0 --channel stable\`
|
|
167
|
-
- \`acme release 2.5.0-rc.1 --channel beta --dry-run\`
|
|
168
|
-
- \`acme release 3.0.0 --notes-file ./docs/releases/3.0.0.md\`
|
|
169
|
-
`,
|
|
170
|
-
)
|
|
171
|
-
.option('--channel <name>', 'Target channel: stable, beta, alpha')
|
|
172
|
-
.option('--notes-file <path>', 'Markdown file used as release notes')
|
|
173
|
-
.option('--dry-run', 'Preview every step without publishing')
|
|
174
|
-
.action((version, options, { console, process }) => {
|
|
175
|
-
console.log('release', version, options, process.cwd)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
cli
|
|
179
|
-
.command(
|
|
180
|
-
'db migrate',
|
|
181
|
-
dedent`
|
|
182
|
-
Apply pending database migrations in a controlled sequence.
|
|
183
|
-
|
|
184
|
-
- Runs migrations in timestamp order.
|
|
185
|
-
- Stops immediately on first failure.
|
|
186
|
-
- Prints SQL statements when \`--verbose\` is enabled.
|
|
187
|
-
- Supports smoke-testing with \`--dry-run\`.
|
|
188
|
-
|
|
189
|
-
> Safety: always run this command against staging before production.
|
|
190
|
-
|
|
191
|
-
Examples:
|
|
192
|
-
- \`acme db migrate\`
|
|
193
|
-
- \`acme db migrate --target 20260210120000_add_users\`
|
|
194
|
-
- \`acme db migrate --dry-run --verbose\`
|
|
195
|
-
`,
|
|
196
|
-
)
|
|
197
|
-
.option('--target <migration>', 'Apply up to a specific migration id')
|
|
198
|
-
.option('--dry-run', 'Print plan only, do not execute SQL')
|
|
199
|
-
.option('--verbose', 'Show each executed statement')
|
|
200
|
-
.action((options, { console, process }) => {
|
|
201
|
-
console.log('migrate', options, process.stdin)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
cli.help()
|
|
205
|
-
cli.parse()
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Why this pattern works well:
|
|
209
|
-
|
|
210
|
-
- `dedent` keeps template literals readable in source files.
|
|
211
|
-
- Help text stays aligned without extra leading whitespace.
|
|
212
|
-
- You can include rich formatting patterns users already recognize:
|
|
213
|
-
lists, quotes, and inline command snippets.
|
|
214
|
-
- Long descriptions remain maintainable as your CLI grows.
|
|
215
|
-
|
|
216
|
-
### Rich `.example(...)` Blocks with `dedent`
|
|
217
|
-
|
|
218
|
-
You can also use `dedent` in `.example(...)` so examples stay readable in code and
|
|
219
|
-
render nicely in help output. A useful pattern is to make the **first line a `#`
|
|
220
|
-
comment** that explains the scenario.
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
import { goke } from 'goke'
|
|
224
|
-
import dedent from 'string-dedent'
|
|
225
|
-
|
|
226
|
-
const cli = goke('tuistory')
|
|
227
|
-
|
|
228
|
-
cli
|
|
229
|
-
.command('start', 'Start an interactive session')
|
|
230
|
-
.example(dedent`
|
|
231
|
-
# Launch and immediately check what the app shows
|
|
232
|
-
tuistory launch "claude" -s ai && tuistory -s ai snapshot --trim
|
|
233
|
-
`)
|
|
234
|
-
.example(dedent`
|
|
235
|
-
# Start a focused coding session with explicit context
|
|
236
|
-
tuistory start --agent code --context "Fix OAuth callback timeout"
|
|
237
|
-
`)
|
|
238
|
-
.example(dedent`
|
|
239
|
-
# Recover recent activity and inspect the latest run details
|
|
240
|
-
tuistory runs list --limit 5 && tuistory runs show --latest
|
|
241
|
-
`)
|
|
242
|
-
.action(() => {
|
|
243
|
-
// command implementation
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
cli
|
|
247
|
-
.command('deploy', 'Deploy current workspace')
|
|
248
|
-
.example(dedent`
|
|
249
|
-
# Dry-run deployment first to validate plan
|
|
250
|
-
tuistory deploy --env staging --dry-run
|
|
251
|
-
`)
|
|
252
|
-
.example(dedent`
|
|
253
|
-
# Deploy production with release notes attached
|
|
254
|
-
tuistory deploy --env production --notes ./docs/release.md
|
|
255
|
-
`)
|
|
256
|
-
.action(() => {
|
|
257
|
-
// command implementation
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
cli.help()
|
|
261
|
-
cli.parse()
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Notes:
|
|
265
|
-
|
|
266
|
-
- Keep each example focused on one workflow.
|
|
267
|
-
- Use the first `#` line as a human-readable intent label.
|
|
268
|
-
- Keep command lines copy-pastable (avoid placeholder-heavy examples).
|
|
269
|
-
|
|
270
|
-
Where examples are rendered today:
|
|
271
|
-
|
|
272
|
-
- For root help (`deploy --help`), examples from the root/default command appear in an **Examples** section at the end.
|
|
273
|
-
- For subcommand help (`deploy logs --help`), examples from that specific subcommand appear in its own **Examples** section at the end.
|
|
274
|
-
|
|
275
|
-
Inline snapshot-style output (many commands):
|
|
276
|
-
|
|
277
|
-
```txt
|
|
278
|
-
deploy
|
|
279
|
-
|
|
280
|
-
Usage:
|
|
281
|
-
$ deploy [options]
|
|
282
|
-
|
|
283
|
-
Commands:
|
|
284
|
-
deploy Deploy the current project
|
|
285
|
-
init Initialize a new project
|
|
286
|
-
login Authenticate with the server
|
|
287
|
-
logout Clear saved credentials
|
|
288
|
-
status Show deployment status
|
|
289
|
-
logs <deploymentId> Stream logs for a deployment
|
|
290
|
-
|
|
291
|
-
Options:
|
|
292
|
-
--env <env> Target environment
|
|
293
|
-
--dry-run Preview without deploying
|
|
294
|
-
-h, --help Display this message
|
|
295
|
-
|
|
296
|
-
Examples:
|
|
297
|
-
# Deploy to staging first
|
|
298
|
-
deploy --env staging --dry-run
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
```txt
|
|
302
|
-
deploy
|
|
303
|
-
|
|
304
|
-
Usage:
|
|
305
|
-
$ deploy logs <deploymentId>
|
|
306
|
-
|
|
307
|
-
Options:
|
|
308
|
-
--follow Follow log output
|
|
309
|
-
--lines <n> Number of lines (default: 100)
|
|
310
|
-
-h, --help Display this message
|
|
311
|
-
|
|
312
|
-
Description:
|
|
313
|
-
Stream logs for a deployment
|
|
314
|
-
|
|
315
|
-
Examples:
|
|
316
|
-
# Stream last 200 lines for a deployment
|
|
317
|
-
deploy logs dep_123 --lines 200
|
|
318
|
-
# Keep following new log lines
|
|
319
|
-
deploy logs dep_123 --follow
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Many Commands with a Root Command
|
|
323
|
-
|
|
324
|
-
Use `''` as the command name to define a root command that runs when no subcommand is given. This is useful for CLIs that have a primary action alongside several subcommands:
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
import { goke } from 'goke'
|
|
328
|
-
import { z } from 'zod'
|
|
329
|
-
|
|
330
|
-
const cli = goke('deploy')
|
|
331
|
-
|
|
332
|
-
// Root command — runs when user types just `deploy`
|
|
333
|
-
cli
|
|
334
|
-
.command('', 'Deploy the current project')
|
|
335
|
-
.option(
|
|
336
|
-
'--env <env>',
|
|
337
|
-
z.string().default('production').describe('Target environment'),
|
|
338
|
-
)
|
|
339
|
-
.option('--dry-run', 'Preview without deploying')
|
|
340
|
-
.action((options, { console, process }) => {
|
|
341
|
-
console.log(`Deploying to ${options.env} from ${process.cwd}...`)
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
// Subcommands
|
|
345
|
-
cli
|
|
346
|
-
.command('init', 'Initialize a new project')
|
|
347
|
-
.option('--template <template>', 'Project template')
|
|
348
|
-
.action((options, { console, process }) => {
|
|
349
|
-
console.log('Initializing project in', process.cwd)
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
cli.command('login', 'Authenticate with the server').action((options, { console, process }) => {
|
|
353
|
-
console.log('Opening browser for login from', process.cwd)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
cli.command('logout', 'Clear saved credentials').action((options, { console, process }) => {
|
|
357
|
-
console.log('Logged out', process.env.USER)
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
cli
|
|
361
|
-
.command('status', 'Show deployment status')
|
|
362
|
-
.option('--json', 'Output as JSON')
|
|
363
|
-
.action((options, { console, process }) => {
|
|
364
|
-
console.log('Fetching status from', process.cwd)
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
cli
|
|
368
|
-
.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
369
|
-
.option('--follow', 'Follow log output')
|
|
370
|
-
.option('--lines <n>', z.number().default(100).describe('Number of lines'))
|
|
371
|
-
.action((deploymentId, options, { console, process }) => {
|
|
372
|
-
console.log(`Streaming logs for ${deploymentId} from ${process.cwd}...`)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
cli.help()
|
|
376
|
-
cli.version('1.0.0')
|
|
377
|
-
cli.parse()
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
```bash
|
|
381
|
-
deploy # runs root command (deploy to production)
|
|
382
|
-
deploy --env staging --dry-run # root command with options
|
|
383
|
-
deploy init --template react # subcommand
|
|
384
|
-
deploy login # subcommand
|
|
385
|
-
deploy logs abc123 --follow # subcommand with args + options
|
|
386
|
-
deploy --help # shows all commands
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### Global Options and Middleware
|
|
390
|
-
|
|
391
|
-
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.
|
|
392
|
-
|
|
393
|
-
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.
|
|
394
|
-
|
|
395
|
-
`process.cwd`, `process.stdin`, and `process.env` come from the active runtime:
|
|
396
|
-
|
|
397
|
-
- 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.
|
|
398
|
-
- In JustBash runs, those same fields are populated from the sandbox execution context.
|
|
399
|
-
|
|
400
|
-
Middleware runs in registration order, after option parsing and validation, but before the matched command's `.action()` callback.
|
|
401
|
-
|
|
402
|
-
### Filesystem Access
|
|
403
|
-
|
|
404
|
-
The injected `fs` object is the recommended way to read or write CLI state.
|
|
405
|
-
|
|
406
|
-
- In normal Node.js runs, `fs` defaults to `node:fs/promises`
|
|
407
|
-
- In JustBash runs, `goke` swaps in a compatible adapter over the JustBash virtual filesystem
|
|
408
|
-
|
|
409
|
-
This makes storage-style commands work in both environments without branching on runtime details.
|
|
410
|
-
|
|
411
|
-
```ts
|
|
412
|
-
cli
|
|
413
|
-
.command('login', 'Save auth token')
|
|
414
|
-
.option('--token <token>', z.string().describe('Auth token'))
|
|
415
|
-
.action(async (options, { fs, console, process }) => {
|
|
416
|
-
await fs.mkdir('.mycli', { recursive: true })
|
|
417
|
-
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
|
|
418
|
-
console.log('saved credentials in', process.cwd)
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
cli
|
|
422
|
-
.command('whoami', 'Read saved auth token')
|
|
423
|
-
.action(async (options, { fs, console, process }) => {
|
|
424
|
-
const auth = await fs.readFile('.mycli/auth.json', 'utf8')
|
|
425
|
-
console.log(auth, process.env.USER)
|
|
426
|
-
})
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
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.
|
|
430
|
-
|
|
431
|
-
### Path handling
|
|
432
|
-
|
|
433
|
-
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.
|
|
434
|
-
|
|
435
|
-
```ts
|
|
436
|
-
await fs.mkdir('.mycli', { recursive: true })
|
|
437
|
-
await fs.writeFile('.mycli/auth.json', json, 'utf8')
|
|
438
|
-
console.log('running from', process.cwd)
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
Why this works:
|
|
442
|
-
|
|
443
|
-
- In normal Node.js runs, relative paths resolve against the host cwd.
|
|
444
|
-
- In JustBash runs, the same relative paths resolve against the sandbox cwd.
|
|
445
|
-
- `process.cwd` mirrors that runtime-specific cwd in both environments.
|
|
446
|
-
|
|
447
|
-
`goke` also exports the runtime types, so helper functions can use dependency injection without reaching for globals:
|
|
448
|
-
|
|
449
|
-
```ts
|
|
450
|
-
import { goke } from 'goke'
|
|
451
|
-
import type { GokeFs, GokeProcess } from 'goke'
|
|
452
|
-
|
|
453
|
-
async function saveAuthToken(args: {
|
|
454
|
-
fs: GokeFs
|
|
455
|
-
process: GokeProcess
|
|
456
|
-
token: string
|
|
457
|
-
}) {
|
|
458
|
-
await args.fs.mkdir('.mycli', { recursive: true })
|
|
459
|
-
await args.fs.writeFile('.mycli/auth.json', JSON.stringify({
|
|
460
|
-
token,
|
|
461
|
-
cwd: args.process.cwd,
|
|
462
|
-
}), 'utf8')
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const cli = goke('mycli')
|
|
466
|
-
|
|
467
|
-
cli
|
|
468
|
-
.command('login <token>', 'Save auth token')
|
|
469
|
-
.action(async (token, options, { fs, process, console }) => {
|
|
470
|
-
await saveAuthToken({ fs, process, token })
|
|
471
|
-
console.log('saved credentials')
|
|
472
|
-
})
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
import { goke } from 'goke'
|
|
477
|
-
import { z } from 'zod'
|
|
478
|
-
|
|
479
|
-
const cli = goke('mycli')
|
|
480
|
-
|
|
481
|
-
cli
|
|
482
|
-
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
483
|
-
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
484
|
-
.use((options, { console, process }) => {
|
|
485
|
-
// options.verbose and options.apiUrl are fully typed here
|
|
486
|
-
if (options.verbose) {
|
|
487
|
-
console.log('verbose mode enabled in', process.cwd)
|
|
488
|
-
}
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
cli
|
|
492
|
-
.command('deploy <env>', 'Deploy to an environment')
|
|
493
|
-
.option('--dry-run', 'Preview without deploying')
|
|
494
|
-
.action((env, options, { console, process }) => {
|
|
495
|
-
// options includes both command options (dryRun) and global options (verbose, apiUrl)
|
|
496
|
-
console.log(`Deploying to ${env} via ${options.apiUrl} from ${process.cwd}`)
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
cli
|
|
500
|
-
.command('status', 'Show deployment status')
|
|
501
|
-
.action((options, { console, process }) => {
|
|
502
|
-
console.log('Checking status...', process.stdin)
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
cli.help()
|
|
506
|
-
cli.parse()
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
Type safety is positional — each `.use()` callback only sees options declared before it in the chain:
|
|
510
|
-
|
|
511
|
-
```ts
|
|
512
|
-
cli
|
|
513
|
-
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
514
|
-
.use((options, { process }) => {
|
|
515
|
-
options.verbose // boolean — typed
|
|
516
|
-
process.argv // string[] — typed
|
|
517
|
-
process.cwd // string — typed
|
|
518
|
-
process.env // Record<string, string> — typed
|
|
519
|
-
process.stdin // string — typed
|
|
520
|
-
options.port // TypeScript error — not declared yet
|
|
521
|
-
})
|
|
522
|
-
.option('--port <port>', z.number().describe('Port'))
|
|
523
|
-
.use((options, { console, process }) => {
|
|
524
|
-
options.verbose // boolean — still visible
|
|
525
|
-
options.port // number — now visible
|
|
526
|
-
console.error('ready', process.cwd)
|
|
527
|
-
})
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
Middleware supports async functions. If any middleware is async, the remaining middleware and command action are chained as promises:
|
|
531
|
-
|
|
532
|
-
```ts
|
|
533
|
-
cli
|
|
534
|
-
.option('--token <token>', z.string().describe('API token'))
|
|
535
|
-
.use(async (options, { console, process }) => {
|
|
536
|
-
const client = await connectToApi(options.token)
|
|
537
|
-
globalState.client = client
|
|
538
|
-
console.log('connected', process.env.NODE_ENV)
|
|
539
|
-
})
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
### Command-specific Options
|
|
543
|
-
|
|
544
|
-
You can attach options to a command.
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
import { goke } from 'goke'
|
|
548
|
-
|
|
549
|
-
const cli = goke()
|
|
550
|
-
|
|
551
|
-
cli
|
|
552
|
-
.command('rm <dir>', 'Remove a dir')
|
|
553
|
-
.option('-r, --recursive', 'Remove recursively')
|
|
554
|
-
.action((dir, options, { console, process }) => {
|
|
555
|
-
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''), process.cwd)
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
cli.help()
|
|
559
|
-
|
|
560
|
-
cli.parse()
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Space-separated Subcommands
|
|
564
|
-
|
|
565
|
-
goke supports multi-word command names for git-like nested subcommands:
|
|
566
|
-
|
|
567
|
-
```ts
|
|
568
|
-
import { goke } from 'goke'
|
|
569
|
-
|
|
570
|
-
const cli = goke('mycli')
|
|
571
|
-
|
|
572
|
-
cli.command('mcp login <url>', 'Login to MCP server').action((url, options, { console, process }) => {
|
|
573
|
-
console.log('Logging in to', url, 'from', process.cwd)
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
cli.command('mcp logout', 'Logout from MCP server').action((options, { console, process }) => {
|
|
577
|
-
console.log('Logged out', process.env.USER)
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
cli
|
|
581
|
-
.command('git remote add <name> <url>', 'Add a git remote')
|
|
582
|
-
.action((name, url, options, { console, process }) => {
|
|
583
|
-
console.log('Adding remote', name, url, 'from', process.cwd)
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
cli.help()
|
|
587
|
-
cli.parse()
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### Schema-based Type Coercion
|
|
591
|
-
|
|
592
|
-
Pass a Standard Schema (like Zod) as the second argument to `.option()` for automatic type coercion. Description and default values are extracted from the schema:
|
|
593
|
-
|
|
594
|
-
```ts
|
|
595
|
-
import { goke } from 'goke'
|
|
596
|
-
import { z } from 'zod'
|
|
597
|
-
|
|
598
|
-
const cli = goke()
|
|
599
|
-
|
|
600
|
-
cli
|
|
601
|
-
.command('serve', 'Start server')
|
|
602
|
-
.option('--port <port>', z.number().describe('Port number'))
|
|
603
|
-
.option('--host [host]', z.string().default('localhost').describe('Hostname'))
|
|
604
|
-
.option('--workers <workers>', z.int().describe('Worker count'))
|
|
605
|
-
.option('--tags <tag>', z.array(z.string()).describe('Tags (repeatable)'))
|
|
606
|
-
.option('--verbose', 'Verbose output')
|
|
607
|
-
.action((options, { console, process }) => {
|
|
608
|
-
// options.port is number, options.host is string, etc.
|
|
609
|
-
console.log(options, process.env.NODE_ENV)
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
cli.parse()
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
**Important:** When using a schema with `.default()`, do **not** repeat the default in the description string. The framework automatically appends `(default: <value>)` to help output from the schema default. Writing `.default(100).describe('Number of lines (default: 100)')` would display the default twice.
|
|
616
|
-
|
|
617
|
-
The second argument accepts any object implementing [Standard Schema](https://github.com/standard-schema/standard-schema), including:
|
|
618
|
-
|
|
619
|
-
- **Zod** v4.2+ (e.g. `z.number()`, `z.string()`, `z.array(z.number())`)
|
|
620
|
-
- **Valibot**, **ArkType**, and other Standard Schema-compatible libraries
|
|
621
|
-
|
|
622
|
-
### Hiding Deprecated Options
|
|
623
|
-
|
|
624
|
-
Mark options as deprecated using Zod's `.meta({ deprecated: true })`. Deprecated options are hidden from help output but still work for parsing — useful for backward compatibility.
|
|
625
|
-
|
|
626
|
-
```ts
|
|
627
|
-
import { goke } from 'goke'
|
|
628
|
-
import { z } from 'zod'
|
|
629
|
-
|
|
630
|
-
const cli = goke()
|
|
631
|
-
|
|
632
|
-
cli
|
|
633
|
-
.command('serve', 'Start server')
|
|
634
|
-
// Deprecated option: hidden from --help, still parses
|
|
635
|
-
.option('--old-port <port>', z.number().meta({ deprecated: true, description: 'Use --port instead' }))
|
|
636
|
-
// Current option: visible in help
|
|
637
|
-
.option('--port <port>', z.number().describe('Port number'))
|
|
638
|
-
.action((options, { console, process }) => {
|
|
639
|
-
const port = options.port ?? options.oldPort
|
|
640
|
-
console.log('Starting on port', port, 'from', process.cwd)
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
cli.help()
|
|
644
|
-
cli.parse()
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
When users run `--help`, deprecated options won't appear, but `--old-port 3000` still works.
|
|
648
|
-
|
|
649
|
-
### Brackets
|
|
650
|
-
|
|
651
|
-
When using brackets in command name, angled brackets indicate required command arguments, while square brackets indicate optional arguments.
|
|
652
|
-
|
|
653
|
-
When using brackets in option name, angled brackets indicate that a string / number value is required, while square brackets indicate that the value is optional.
|
|
654
|
-
|
|
655
|
-
**Optionality is determined solely by bracket syntax, not by the schema.** `[square brackets]` makes an option optional regardless of whether the schema is `z.string()` or `z.string().optional()`. The schema's `.optional()` is never consulted for this — it only affects type coercion. This means `z.string()` with `[--name]` is treated as optional: if the flag is omitted, `options.name` is `undefined` even though the schema has no `.optional()`.
|
|
656
|
-
|
|
657
|
-
### Optional-value flags — `--flag` vs `--flag value` vs omitted
|
|
658
|
-
|
|
659
|
-
A flag declared with square brackets (`--host [host]`) has **three distinct runtime states**, not two. The user can:
|
|
660
|
-
|
|
661
|
-
1. **Omit the flag entirely** — no `--host` on the command line at all
|
|
662
|
-
2. **Pass the flag bare** — `--host` by itself, with no value following it
|
|
663
|
-
3. **Pass the flag with a value** — `--host example.com`
|
|
664
|
-
|
|
665
|
-
goke surfaces all three cases through a single `string | undefined` type. There is no `boolean` in the union — bare flags are normalized to the **empty string `''`** so callers only ever deal with strings:
|
|
666
|
-
|
|
667
|
-
```ts
|
|
668
|
-
cli
|
|
669
|
-
.command('serve', 'Start the server')
|
|
670
|
-
.option('--host [host]', 'Optional host override')
|
|
671
|
-
.action((options) => {
|
|
672
|
-
// options.host: string | undefined
|
|
673
|
-
// --host → '' (flag present, no value)
|
|
674
|
-
// --host example.com → 'example.com'
|
|
675
|
-
// (omitted) → undefined
|
|
676
|
-
})
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
**Detecting each case:**
|
|
680
|
-
|
|
681
|
-
```ts
|
|
682
|
-
.action((options) => {
|
|
683
|
-
if (options.host === undefined) {
|
|
684
|
-
// Flag was not passed at all — use a sensible default
|
|
685
|
-
console.log('using default host: localhost')
|
|
686
|
-
} else if (options.host === '') {
|
|
687
|
-
// Flag was passed bare: `--host` with no value following it
|
|
688
|
-
// Treat this as an explicit "opt in, but use the default/automatic value"
|
|
689
|
-
console.log('host flag passed with no value — enabling auto-discovery')
|
|
690
|
-
} else {
|
|
691
|
-
// Flag was passed with an explicit value
|
|
692
|
-
console.log(`host = ${options.host}`)
|
|
693
|
-
}
|
|
694
|
-
})
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
**In most cases you don't need the three-way distinction** — a plain truthy check collapses "omitted" and "bare flag" into the same "fall back to default" branch:
|
|
698
|
-
|
|
699
|
-
```ts
|
|
700
|
-
.action((options) => {
|
|
701
|
-
// `--host` bare AND omitted both fall through to the default
|
|
702
|
-
const host = options.host || 'localhost'
|
|
703
|
-
startServer({ host })
|
|
704
|
-
})
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
Reserve the `=== ''` check for cases where "opt in without a value" is a meaningful signal distinct from "flag omitted" — for example, `--direct` meaning "auto-discover a Chrome instance" vs `--direct ws://…` meaning "connect to this specific endpoint" vs no `--direct` meaning "don't use direct mode".
|
|
708
|
-
|
|
709
|
-
> **Breaking change note (goke 6.6.0):** prior versions surfaced bare flags as `boolean` `true` inside a `string | boolean | undefined` union, forcing every call site to write `typeof options.host === 'string' ? options.host : undefined`. Code that used `options.host === true` to detect the bare-flag case must be updated to `options.host === ''`. Schema-based optional flags with `.default(...)` are unaffected — the default still kicks in when the flag is passed bare.
|
|
710
|
-
|
|
711
|
-
### Negated Options
|
|
712
|
-
|
|
713
|
-
To allow an option whose value is `false`, you need to manually specify a negated option:
|
|
714
|
-
|
|
715
|
-
```ts
|
|
716
|
-
cli
|
|
717
|
-
.command('build [project]', 'Build a project')
|
|
718
|
-
.option('--no-config', 'Disable config file')
|
|
719
|
-
.option('--config <path>', 'Use a custom config file')
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### Variadic Arguments
|
|
723
|
-
|
|
724
|
-
The last argument of a command can be variadic. To make an argument variadic you have to add `...` to the start of argument name:
|
|
725
|
-
|
|
726
|
-
```ts
|
|
727
|
-
cli
|
|
728
|
-
.command('build <entry> [...otherFiles]', 'Build your app')
|
|
729
|
-
.option('--foo', 'Foo option')
|
|
730
|
-
.action((entry, otherFiles, options, { console, process }) => {
|
|
731
|
-
console.log(entry)
|
|
732
|
-
console.log(otherFiles)
|
|
733
|
-
console.log(options, process.stdin)
|
|
734
|
-
})
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
### Double-dash `--` (end of options)
|
|
738
|
-
|
|
739
|
-
The `--` token signals the end of options. Everything after `--` is available via `options['--']` as a separate array, not mixed into positional args. This lets you distinguish between your command's own arguments and passthrough args — the same pattern used by `doppler`, `npm`, `pnpm`, and `docker`.
|
|
740
|
-
|
|
741
|
-
`options['--']` is **always present** on the inferred options type as `string[]`. When no `--` token appears on the command line, it's the empty array — you never need to guard with `||` or `?.` or an `Array.isArray` cast.
|
|
742
|
-
|
|
743
|
-
```ts
|
|
744
|
-
import { goke } from 'goke'
|
|
745
|
-
import { z } from 'zod'
|
|
746
|
-
import { execSync } from 'child_process'
|
|
747
|
-
|
|
748
|
-
const cli = goke('runner')
|
|
749
|
-
|
|
750
|
-
cli
|
|
751
|
-
.command('run <script>', 'Run a script with injected environment variables')
|
|
752
|
-
.option('--env <env>', z.enum(['dev', 'staging', 'production']).describe('Target environment'))
|
|
753
|
-
.example('# Pass extra flags to the child script via --')
|
|
754
|
-
.example('runner run --env staging server.js -- --port 3000 --verbose')
|
|
755
|
-
.action((script, options) => {
|
|
756
|
-
// runner run --env staging server.js -- --port 3000 --verbose
|
|
757
|
-
// script = 'server.js' (positional arg)
|
|
758
|
-
// options.env = 'staging' (runner's own option)
|
|
759
|
-
// options['--'] = ['--port', '3000', '--verbose'] (passthrough, always string[])
|
|
760
|
-
|
|
761
|
-
const secrets = loadSecrets(options.env)
|
|
762
|
-
const extraArgs = options['--'].join(' ')
|
|
763
|
-
execSync(`node ${script} ${extraArgs}`, {
|
|
764
|
-
env: { ...process.env, ...secrets },
|
|
765
|
-
stdio: 'inherit',
|
|
766
|
-
})
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
cli.help()
|
|
770
|
-
cli.parse()
|
|
771
|
-
```
|
|
772
|
-
|
|
773
|
-
```bash
|
|
774
|
-
runner run --env staging server.js -- --port 3000 --verbose
|
|
775
|
-
# ^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
776
|
-
# runner option positional passthrough (options['--'])
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
Without `--`, flags like `--port` would be parsed as runner options and fail with "Unknown option `--port`". The `--` tells goke to stop parsing and collect the rest separately.
|
|
780
|
-
|
|
781
|
-
### Dot-nested Options
|
|
782
|
-
|
|
783
|
-
Dot-nested options will be merged into a single option.
|
|
784
|
-
|
|
785
|
-
```ts
|
|
786
|
-
cli
|
|
787
|
-
.command('build', 'desc')
|
|
788
|
-
.option('--env <env>', 'Set envs')
|
|
789
|
-
.example('--env.API_SECRET xxx')
|
|
790
|
-
.action((options, { console, process }) => {
|
|
791
|
-
console.log(options, process.env.API_SECRET)
|
|
792
|
-
})
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
### Default Command
|
|
796
|
-
|
|
797
|
-
Register a command that will be used when no other command is matched.
|
|
798
|
-
|
|
799
|
-
```ts
|
|
800
|
-
cli
|
|
801
|
-
.command('[...files]', 'Build files')
|
|
802
|
-
.option('--minimize', 'Minimize output')
|
|
803
|
-
.action((files, options, { console, process }) => {
|
|
804
|
-
console.log(files)
|
|
805
|
-
console.log(options.minimize, process.cwd)
|
|
806
|
-
})
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
### Error Handling
|
|
810
|
-
|
|
811
|
-
To handle command errors globally:
|
|
812
|
-
|
|
813
|
-
```ts
|
|
814
|
-
try {
|
|
815
|
-
cli.parse(process.argv, { run: false })
|
|
816
|
-
await cli.runMatchedCommand()
|
|
817
|
-
} catch (error) {
|
|
818
|
-
const message = error instanceof Error ? error.stack : String(error)
|
|
819
|
-
process.stderr.write(String(message) + '\n')
|
|
820
|
-
process.exit(1)
|
|
821
|
-
}
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
### Testing with mocked console and exit
|
|
825
|
-
|
|
826
|
-
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.
|
|
827
|
-
|
|
828
|
-
```ts
|
|
829
|
-
import { describe, expect, test, vi } from 'vitest'
|
|
830
|
-
import { goke, GokeProcessExit } from 'goke'
|
|
831
|
-
|
|
832
|
-
describe('deploy command', () => {
|
|
833
|
-
test('writes output and exits with injected mocks', () => {
|
|
834
|
-
const stdout = { write: vi.fn<(data: string) => void>() }
|
|
835
|
-
const stderr = { write: vi.fn<(data: string) => void>() }
|
|
836
|
-
const exit = vi.fn<(code: number) => void>()
|
|
837
|
-
|
|
838
|
-
const cli = goke('acme', { stdout, stderr, exit })
|
|
839
|
-
|
|
840
|
-
cli
|
|
841
|
-
.command('deploy', 'Deploy the project')
|
|
842
|
-
.action((options, { console, process }) => {
|
|
843
|
-
console.log('deploying')
|
|
844
|
-
process.exit(2)
|
|
845
|
-
})
|
|
846
|
-
|
|
847
|
-
expect(() => {
|
|
848
|
-
cli.parse(['node', 'acme', 'deploy'], { run: true })
|
|
849
|
-
}).toThrow(GokeProcessExit)
|
|
850
|
-
|
|
851
|
-
expect(stdout.write).toHaveBeenCalledWith('deploying\n')
|
|
852
|
-
expect(exit).toHaveBeenCalledWith(2)
|
|
853
|
-
expect(stderr.write).not.toHaveBeenCalled()
|
|
854
|
-
})
|
|
855
|
-
})
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
### With TypeScript
|
|
859
|
-
|
|
860
|
-
```ts
|
|
861
|
-
import { goke } from 'goke'
|
|
862
|
-
|
|
863
|
-
const cli = goke('my-program')
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
Do not manually type `action` callback arguments. goke infers argument and option types automatically from the command signature and option schemas.
|
|
867
|
-
|
|
868
|
-
```ts
|
|
869
|
-
import { goke } from 'goke'
|
|
870
|
-
import { z } from 'zod'
|
|
871
|
-
|
|
872
|
-
const cli = goke('my-program')
|
|
873
|
-
|
|
874
|
-
cli
|
|
875
|
-
.command('serve <entry>', 'Start the app')
|
|
876
|
-
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
877
|
-
.option('--watch', 'Watch files')
|
|
878
|
-
.action((entry, options, { console, process }) => {
|
|
879
|
-
// entry: string
|
|
880
|
-
// options.port: number
|
|
881
|
-
// options.watch: boolean
|
|
882
|
-
console.log(entry, options.port, options.watch, process.cwd)
|
|
883
|
-
})
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
### Open in Browser
|
|
887
|
-
|
|
888
|
-
`openInBrowser` opens a URL in the default browser. In non-TTY environments (CI, piped output, agents), it prints the URL to stdout instead.
|
|
889
|
-
|
|
890
|
-
```ts
|
|
891
|
-
import { openInBrowser } from 'goke'
|
|
892
|
-
|
|
893
|
-
openInBrowser('https://example.com/dashboard')
|
|
894
|
-
```
|
|
895
|
-
|
|
896
|
-
### Expose a goke CLI to JustBash
|
|
897
|
-
|
|
898
|
-
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`.
|
|
899
|
-
|
|
900
|
-
```ts
|
|
901
|
-
import { goke } from 'goke'
|
|
902
|
-
import { z } from 'zod'
|
|
903
|
-
import { Bash } from 'just-bash'
|
|
904
|
-
|
|
905
|
-
const cli = goke('parent')
|
|
906
|
-
|
|
907
|
-
cli
|
|
908
|
-
.command('child commandwithspaces', 'Run nested command')
|
|
909
|
-
.option('--name <name>', z.string().describe('Name'))
|
|
910
|
-
.action((options, { console, process }) => {
|
|
911
|
-
console.log(`hello ${options.name} from ${process.cwd}`)
|
|
912
|
-
})
|
|
913
|
-
|
|
914
|
-
const bash = new Bash({
|
|
915
|
-
customCommands: [await cli.createJustBashCommand()],
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
await bash.exec('parent child commandwithspaces --name Tommy')
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
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()`.
|
|
922
|
-
|
|
923
|
-
### Test with real JustBash
|
|
924
|
-
|
|
925
|
-
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.
|
|
926
|
-
|
|
927
|
-
```ts
|
|
928
|
-
import { describe, expect, test } from 'vitest'
|
|
929
|
-
import { Bash, InMemoryFs } from 'just-bash'
|
|
930
|
-
import { cli } from '../src/cli'
|
|
931
|
-
|
|
932
|
-
describe('login command', () => {
|
|
933
|
-
test('writes auth state through the sandbox fs', async () => {
|
|
934
|
-
const virtualFs = new InMemoryFs()
|
|
935
|
-
await virtualFs.mkdir('/project', { recursive: true })
|
|
936
|
-
|
|
937
|
-
const bash = new Bash({
|
|
938
|
-
fs: virtualFs,
|
|
939
|
-
cwd: '/project',
|
|
940
|
-
customCommands: [await cli.createJustBashCommand()],
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
const result = await bash.exec('parent login --token Tommy')
|
|
944
|
-
|
|
945
|
-
expect(result.stdout).toBe('saved credentials\n')
|
|
946
|
-
expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe(
|
|
947
|
-
'{"token":"Tommy","cwd":"/project"}',
|
|
948
|
-
)
|
|
949
|
-
})
|
|
950
|
-
})
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
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`.
|
|
954
|
-
|
|
955
|
-
### Exposing your CLI as a skill
|
|
956
|
-
|
|
957
|
-
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.
|
|
958
|
-
|
|
959
|
-
````markdown
|
|
960
|
-
<!-- skills/acme/SKILL.md -->
|
|
961
|
-
---
|
|
962
|
-
name: acme
|
|
963
|
-
description: >
|
|
964
|
-
acme is a deployment CLI. Always run `acme --help` before using it
|
|
965
|
-
to discover available commands, options, and usage examples.
|
|
966
|
-
---
|
|
967
|
-
|
|
968
|
-
# acme
|
|
969
|
-
|
|
970
|
-
Always run `acme --help` before using this CLI.
|
|
971
|
-
For subcommand details: `acme <command> --help`
|
|
972
|
-
````
|
|
973
|
-
|
|
974
|
-
## YAML Output for Agent-Friendly CLIs
|
|
975
|
-
|
|
976
|
-
When a command returns structured data, print it as YAML on stdout. YAML is the best middle ground between human-readable output and machine-processable output:
|
|
977
|
-
|
|
978
|
-
- Humans can read it at a glance — no surrounding quotes on keys, less punctuation noise than JSON.
|
|
979
|
-
- Agents can process it with [`yq`](https://github.com/mikefarah/yq), the YAML equivalent of `jq`, to extract specific fields or filter results.
|
|
980
|
-
- It is more context-efficient than verbose prose: a compact YAML block conveys the same information in fewer tokens.
|
|
981
|
-
|
|
982
|
-
```ts
|
|
983
|
-
import { goke } from 'goke'
|
|
984
|
-
import { stringify } from 'yaml'
|
|
985
|
-
|
|
986
|
-
const cli = goke('deploy')
|
|
987
|
-
|
|
988
|
-
cli
|
|
989
|
-
.command('status', 'Show deployment status')
|
|
990
|
-
.action(async (options, { console }) => {
|
|
991
|
-
const status = await fetchStatus()
|
|
992
|
-
// Output structured data as YAML on stdout
|
|
993
|
-
console.log(stringify(status))
|
|
994
|
-
})
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
Example output:
|
|
998
|
-
|
|
999
|
-
```yaml
|
|
1000
|
-
deployment: prod-v2
|
|
1001
|
-
status: running
|
|
1002
|
-
replicas: 3
|
|
1003
|
-
lastDeploy: "2026-01-15T10:30:00Z"
|
|
1004
|
-
health:
|
|
1005
|
-
cpu: 42%
|
|
1006
|
-
memory: 1.2GB
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
### Processing YAML output with yq
|
|
1010
|
-
|
|
1011
|
-
Agents can pipe the output through `yq` to extract specific fields or filter results — the same way they would use `jq` with JSON, but with cleaner, more readable output:
|
|
1012
|
-
|
|
1013
|
-
```bash
|
|
1014
|
-
# Extract a single field
|
|
1015
|
-
deploy status | yq '.deployment'
|
|
1016
|
-
|
|
1017
|
-
# Access nested fields
|
|
1018
|
-
deploy status | yq '.health.cpu'
|
|
1019
|
-
|
|
1020
|
-
# Filter an array of results
|
|
1021
|
-
deploy list | yq '.[] | select(.status == "running")'
|
|
1022
|
-
|
|
1023
|
-
# Combine multiple fields
|
|
1024
|
-
deploy list | yq '.[] | {name: .name, status: .status}'
|
|
1025
|
-
|
|
1026
|
-
# Count items matching a condition
|
|
1027
|
-
deploy list | yq '[.[] | select(.status == "error")] | length'
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
### Keep stdout clean — send non-YAML to stderr
|
|
1031
|
-
|
|
1032
|
-
If a command outputs YAML on stdout, all unrelated content must go to stderr: error messages, progress indicators, informational logs, warnings. This keeps stdout pipeable through `yq` without breaking the YAML parse.
|
|
1033
|
-
|
|
1034
|
-
```ts
|
|
1035
|
-
cli
|
|
1036
|
-
.command('deploy <env>', 'Deploy to environment')
|
|
1037
|
-
.action(async (env, options, { console }) => {
|
|
1038
|
-
// Progress and logs → stderr (won't pollute yq pipes)
|
|
1039
|
-
console.error(`Deploying to ${env}...`)
|
|
1040
|
-
console.error('Building artifacts...')
|
|
1041
|
-
|
|
1042
|
-
const result = await deploy(env)
|
|
1043
|
-
|
|
1044
|
-
// Structured result → stdout as YAML
|
|
1045
|
-
console.log(stringify(result))
|
|
1046
|
-
})
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
Now agents can process the output cleanly:
|
|
1050
|
-
|
|
1051
|
-
```bash
|
|
1052
|
-
# Only the YAML result reaches yq — progress lines go to the terminal
|
|
1053
|
-
deploy deploy production | yq '.version'
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
If an error occurs, throw or write to `console.error` / `process.stderr`, and exit with a non-zero code. Never mix error text into stdout when the command is expected to output YAML.
|
|
1057
|
-
|
|
1058
|
-
## Contributor Notes
|
|
1059
|
-
|
|
1060
|
-
### Rules
|
|
1061
|
-
|
|
1062
|
-
1. Use schema-based options for typed values.
|
|
1063
|
-
2. Do not repeat defaults in `.describe(...)` when using `.default()`.
|
|
1064
|
-
3. Do not manually type action callback arguments; let goke infer them.
|
|
1065
|
-
4. Prefer injected `{ fs, console, process }` over global `console`, `process.exit`, or direct `node:fs/promises` imports.
|
|
1066
|
-
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.
|
|
1067
|
-
6. Define the CLI in app code and import that same CLI in tests; do not construct a separate CLI inside compatibility tests.
|
|
1068
|
-
|
|
1069
|
-
### Version
|
|
1070
|
-
|
|
1071
|
-
Import `package.json` and use its version field so the CLI stays in sync automatically:
|
|
1072
|
-
|
|
1073
|
-
```ts
|
|
1074
|
-
import pkg from './package.json' with { type: 'json' }
|
|
1075
|
-
|
|
1076
|
-
cli.version(pkg.version)
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
## References
|
|
1080
|
-
|
|
1081
|
-
### CLI Instance
|
|
1082
|
-
|
|
1083
|
-
CLI instance is created by invoking the `goke` function:
|
|
1084
|
-
|
|
1085
|
-
```ts
|
|
1086
|
-
import { goke } from 'goke'
|
|
1087
|
-
const cli = goke()
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
#### goke(name?)
|
|
1091
|
-
|
|
1092
|
-
Create a CLI instance, optionally specify the program name which will be used to display in help and version message. When not set we use the basename of `argv[1]`.
|
|
1093
|
-
|
|
1094
|
-
#### cli.command(name, description, config?)
|
|
1095
|
-
|
|
1096
|
-
- Type: `(name: string, description: string) => Command`
|
|
1097
|
-
|
|
1098
|
-
Create a command instance. Supports space-separated subcommands like `mcp login`.
|
|
1099
|
-
|
|
1100
|
-
- `config.allowUnknownOptions`: `boolean` Allow unknown options in this command.
|
|
1101
|
-
- `config.ignoreOptionDefaultValue`: `boolean` Don't use the options's default value in parsed options, only display them in help message.
|
|
1102
|
-
|
|
1103
|
-
#### cli.option(name, descriptionOrSchema?)
|
|
1104
|
-
|
|
1105
|
-
- Type: `(name: string, descriptionOrSchema?: string | StandardJSONSchemaV1) => CLI`
|
|
1106
|
-
|
|
1107
|
-
Add a global option. The second argument is either:
|
|
1108
|
-
|
|
1109
|
-
- A **string** used as the description text
|
|
1110
|
-
- A **Standard Schema** (e.g. `z.number().describe('Port')`) — description and default are extracted from the schema automatically
|
|
1111
|
-
|
|
1112
|
-
#### cli.use(callback)
|
|
1113
|
-
|
|
1114
|
-
- Type: `(callback: (options: Opts, { fs, console, process }) => void | Promise<void>) => CLI`
|
|
1115
|
-
|
|
1116
|
-
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.
|
|
1117
|
-
|
|
1118
|
-
#### cli.parse(argv?)
|
|
1119
|
-
|
|
1120
|
-
- Type: `(argv = process.argv) => ParsedArgv`
|
|
1121
|
-
|
|
1122
|
-
#### cli.version(version, customFlags?)
|
|
1123
|
-
|
|
1124
|
-
- Type: `(version: string, customFlags = '-v, --version') => CLI`
|
|
1125
|
-
|
|
1126
|
-
#### cli.help(callback?)
|
|
1127
|
-
|
|
1128
|
-
- Type: `(callback?: HelpCallback) => CLI`
|
|
1129
|
-
|
|
1130
|
-
#### cli.outputHelp()
|
|
1131
|
-
|
|
1132
|
-
- Type: `() => CLI`
|
|
1133
|
-
|
|
1134
|
-
Print the help message to stdout.
|
|
1135
|
-
|
|
1136
|
-
#### cli.clone(options?)
|
|
1137
|
-
|
|
1138
|
-
- Type: `(options?: GokeOptions) => Goke`
|
|
1139
|
-
|
|
1140
|
-
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:
|
|
1141
|
-
|
|
1142
|
-
```ts
|
|
1143
|
-
const cli = goke('mycli')
|
|
1144
|
-
cli.command('build', 'Build project').action((options, { console }) => {
|
|
1145
|
-
console.log('building')
|
|
1146
|
-
})
|
|
1147
|
-
cli.help()
|
|
1148
|
-
|
|
1149
|
-
// In tests: override streams without touching the original CLI
|
|
1150
|
-
const stdout = { write: vi.fn<(data: string) => void>() }
|
|
1151
|
-
const isolated = cli.clone({ stdout })
|
|
1152
|
-
isolated.parse(['node', 'mycli', 'build'])
|
|
1153
|
-
expect(stdout.write).toHaveBeenCalledWith('building\n')
|
|
1154
|
-
```
|
|
1155
|
-
|
|
1156
|
-
#### cli.helpText()
|
|
1157
|
-
|
|
1158
|
-
- Type: `() => string`
|
|
1159
|
-
|
|
1160
|
-
Return the formatted help string without printing it. Useful for embedding help text in documentation, tests, or other programmatic uses.
|
|
1161
|
-
|
|
1162
|
-
```ts
|
|
1163
|
-
const cli = goke('mycli')
|
|
1164
|
-
cli.command('build', 'Build project')
|
|
1165
|
-
cli.option('--watch', 'Watch mode')
|
|
1166
|
-
cli.help()
|
|
1167
|
-
|
|
1168
|
-
const help = cli.helpText()
|
|
1169
|
-
// => "mycli\n\nUsage:\n $ mycli ..."
|
|
1170
|
-
```
|
|
1171
|
-
|
|
1172
|
-
#### cli.usage(text)
|
|
1173
|
-
|
|
1174
|
-
- Type: `(text: string) => CLI`
|
|
1175
|
-
|
|
1176
|
-
#### cli.example(example)
|
|
1177
|
-
|
|
1178
|
-
- Type: `(example: CommandExample) => CLI`
|
|
1179
|
-
|
|
1180
|
-
### Command Instance
|
|
1181
|
-
|
|
1182
|
-
#### command.option()
|
|
1183
|
-
|
|
1184
|
-
Basically the same as `cli.option` but this adds the option to specific command.
|
|
1185
|
-
|
|
1186
|
-
#### command.action(callback)
|
|
1187
|
-
|
|
1188
|
-
- Type: `(callback: ActionCallback) => Command`
|
|
1189
|
-
|
|
1190
|
-
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.
|
|
1191
|
-
|
|
1192
|
-
#### command.alias(name)
|
|
1193
|
-
|
|
1194
|
-
- Type: `(name: string) => Command`
|
|
1195
|
-
|
|
1196
|
-
#### command.allowUnknownOptions()
|
|
1197
|
-
|
|
1198
|
-
- Type: `() => Command`
|
|
1199
|
-
|
|
1200
|
-
#### command.hidden()
|
|
1201
|
-
|
|
1202
|
-
- Type: `() => Command`
|
|
1203
|
-
|
|
1204
|
-
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.
|
|
1205
|
-
|
|
1206
|
-
```ts
|
|
1207
|
-
cli
|
|
1208
|
-
.command('internal-reset', 'Reset internal state')
|
|
1209
|
-
.hidden()
|
|
1210
|
-
.action((options, { console }) => {
|
|
1211
|
-
console.log('reset done')
|
|
1212
|
-
})
|
|
1213
|
-
```
|
|
1214
|
-
|
|
1215
|
-
#### command.example(example)
|
|
1216
|
-
|
|
1217
|
-
- Type: `(example: CommandExample) => Command`
|
|
1218
|
-
|
|
1219
|
-
#### command.usage(text)
|
|
1220
|
-
|
|
1221
|
-
- Type: `(text: string) => Command`
|
|
1222
|
-
|
|
1223
|
-
#### command.helpText()
|
|
1224
|
-
|
|
1225
|
-
- Type: `() => string`
|
|
1226
|
-
|
|
1227
|
-
Return the formatted help string for this specific command without printing it. Useful for tests or embedding help text programmatically.
|
|
1228
|
-
|
|
1229
|
-
### Events
|
|
1230
|
-
|
|
1231
|
-
Listen to commands:
|
|
1232
|
-
|
|
1233
|
-
```js
|
|
1234
|
-
cli.on('command:foo', () => {
|
|
1235
|
-
// Do something
|
|
1236
|
-
})
|
|
1237
|
-
|
|
1238
|
-
cli.on('command:!', () => {
|
|
1239
|
-
// Default command
|
|
1240
|
-
})
|
|
1241
|
-
|
|
1242
|
-
cli.on('command:*', () => {
|
|
1243
|
-
process.stderr.write(`Invalid command: ${cli.args.join(' ')}\n`)
|
|
1244
|
-
process.exit(1)
|
|
1245
|
-
})
|
|
1246
|
-
```
|
|
1247
|
-
|
|
1248
|
-
## Credits
|
|
1249
|
-
|
|
1250
|
-
goke is inspired by [cac](https://github.com/cacjs/cac) (Command And Conquer) by [EGOIST](https://github.com/egoist).
|
|
1251
|
-
|
|
1252
|
-
## License
|
|
1253
|
-
|
|
1254
|
-
MIT
|