goke 6.8.0 → 6.10.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__/completions.test.d.ts +9 -0
- package/dist/__test__/completions.test.d.ts.map +1 -0
- package/dist/__test__/completions.test.js +774 -0
- package/dist/__test__/index.test.js +188 -0
- package/dist/__test__/just-bash.test.js +19 -0
- package/dist/__test__/readme-examples.test.js +141 -5
- package/dist/__test__/types.test-d.js +64 -0
- package/dist/agents.d.ts +38 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +63 -0
- package/dist/completions.d.ts +88 -0
- package/dist/completions.d.ts.map +1 -0
- package/dist/completions.js +315 -0
- package/dist/goke.d.ts +115 -2
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +487 -25
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -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/dist/runtime-browser.d.ts +1 -1
- package/dist/runtime-browser.d.ts.map +1 -1
- package/dist/runtime-browser.js +1 -1
- package/dist/runtime-node.d.ts +1 -1
- package/dist/runtime-node.d.ts.map +1 -1
- package/dist/runtime-node.js +22 -13
- package/package.json +1 -1
- package/src/__test__/completions.test.ts +902 -0
- package/src/__test__/index.test.ts +241 -0
- package/src/__test__/just-bash.test.ts +24 -0
- package/src/__test__/readme-examples.test.ts +153 -5
- package/src/__test__/types.test-d.ts +68 -0
- package/src/agents.ts +101 -0
- package/src/completions.ts +363 -0
- package/src/goke.ts +564 -3
- package/src/index.ts +11 -2
- package/src/just-bash.ts +92 -18
- package/src/runtime-browser.ts +1 -1
- package/src/runtime-node.ts +19 -11
- package/README.md +0 -1254
|
@@ -160,6 +160,39 @@ describe('error formatting', () => {
|
|
|
160
160
|
})
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
+
describe('anonymous action naming', () => {
|
|
164
|
+
test('inline anonymous function gets named after the command', () => {
|
|
165
|
+
const cli = gokeTestable('mycli')
|
|
166
|
+
const cmd = cli.command('deploy', 'Deploy app')
|
|
167
|
+
// Inline arrow functions passed directly to .action() have no name,
|
|
168
|
+
// so goke assigns one based on the command name for better stack traces.
|
|
169
|
+
cmd.action(() => {})
|
|
170
|
+
expect(cmd.commandAction!.name).toBe('command:deploy')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('inline anonymous function on multi-word command gets full name', () => {
|
|
174
|
+
const cli = gokeTestable('mycli')
|
|
175
|
+
const cmd = cli.command('db migrate', 'Run migrations')
|
|
176
|
+
cmd.action(() => {})
|
|
177
|
+
expect(cmd.commandAction!.name).toBe('command:db migrate')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('named function keeps its original name', () => {
|
|
181
|
+
const cli = gokeTestable('mycli')
|
|
182
|
+
const cmd = cli.command('build', 'Build app')
|
|
183
|
+
function myBuildAction() {}
|
|
184
|
+
cmd.action(myBuildAction)
|
|
185
|
+
expect(cmd.commandAction!.name).toBe('myBuildAction')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('default command action gets "command:default" name', () => {
|
|
189
|
+
const cli = gokeTestable('mycli')
|
|
190
|
+
const cmd = cli.command('', 'Default command')
|
|
191
|
+
cmd.action(() => {})
|
|
192
|
+
expect(cmd.commandAction!.name).toBe('command:default')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
163
196
|
describe('injected fs', () => {
|
|
164
197
|
test('command actions can use the default node fs for cli storage', async () => {
|
|
165
198
|
const stdout = createTestOutputStream()
|
|
@@ -2336,3 +2369,211 @@ describe('middleware', () => {
|
|
|
2336
2369
|
expect(order).toEqual(['sync1', 'async', 'sync2', 'action'])
|
|
2337
2370
|
})
|
|
2338
2371
|
})
|
|
2372
|
+
|
|
2373
|
+
describe('use() with sub-CLI composition', () => {
|
|
2374
|
+
test('basic composition: sub-CLI command runs via parent', () => {
|
|
2375
|
+
const parent = goke('mycli')
|
|
2376
|
+
const sub = goke()
|
|
2377
|
+
let matched = ''
|
|
2378
|
+
|
|
2379
|
+
sub
|
|
2380
|
+
.command('deploy', 'Deploy the app')
|
|
2381
|
+
.action(() => { matched = 'deploy' })
|
|
2382
|
+
|
|
2383
|
+
parent.use(sub)
|
|
2384
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true })
|
|
2385
|
+
expect(matched).toBe('deploy')
|
|
2386
|
+
})
|
|
2387
|
+
|
|
2388
|
+
test('multiple sub-CLIs composed together', () => {
|
|
2389
|
+
const parent = goke('mycli')
|
|
2390
|
+
const subA = goke()
|
|
2391
|
+
const subB = goke()
|
|
2392
|
+
let matched = ''
|
|
2393
|
+
|
|
2394
|
+
subA.command('login', 'Login').action(() => { matched = 'login' })
|
|
2395
|
+
subB.command('deploy', 'Deploy').action(() => { matched = 'deploy' })
|
|
2396
|
+
|
|
2397
|
+
parent.use(subA).use(subB)
|
|
2398
|
+
|
|
2399
|
+
parent.parse(['node', 'bin', 'login'], { run: true })
|
|
2400
|
+
expect(matched).toBe('login')
|
|
2401
|
+
|
|
2402
|
+
matched = ''
|
|
2403
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true })
|
|
2404
|
+
expect(matched).toBe('deploy')
|
|
2405
|
+
})
|
|
2406
|
+
|
|
2407
|
+
test('sub-CLI command with options and schema coercion', () => {
|
|
2408
|
+
const parent = goke('mycli')
|
|
2409
|
+
const sub = goke()
|
|
2410
|
+
let result: any = {}
|
|
2411
|
+
|
|
2412
|
+
sub
|
|
2413
|
+
.command('serve', 'Start server')
|
|
2414
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
2415
|
+
.option('--host <host>', z.string().describe('Host'))
|
|
2416
|
+
.action((options) => { result = options })
|
|
2417
|
+
|
|
2418
|
+
parent.use(sub)
|
|
2419
|
+
parent.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true })
|
|
2420
|
+
|
|
2421
|
+
expect(result.port).toBe(3000)
|
|
2422
|
+
expect(typeof result.port).toBe('number')
|
|
2423
|
+
expect(result.host).toBe('localhost')
|
|
2424
|
+
})
|
|
2425
|
+
|
|
2426
|
+
test('sub-CLI command with positional args', () => {
|
|
2427
|
+
const parent = goke('mycli')
|
|
2428
|
+
const sub = goke()
|
|
2429
|
+
let receivedId = ''
|
|
2430
|
+
|
|
2431
|
+
sub
|
|
2432
|
+
.command('get <id>', 'Get a resource')
|
|
2433
|
+
.action((id) => { receivedId = id })
|
|
2434
|
+
|
|
2435
|
+
parent.use(sub)
|
|
2436
|
+
parent.parse(['node', 'bin', 'get', 'abc123'], { run: true })
|
|
2437
|
+
|
|
2438
|
+
expect(receivedId).toBe('abc123')
|
|
2439
|
+
})
|
|
2440
|
+
|
|
2441
|
+
test('sub-CLI with multi-word commands', () => {
|
|
2442
|
+
const parent = goke('mycli')
|
|
2443
|
+
const sub = goke()
|
|
2444
|
+
let matched = ''
|
|
2445
|
+
|
|
2446
|
+
sub.command('mcp login', 'Login to MCP').action(() => { matched = 'mcp login' })
|
|
2447
|
+
sub.command('mcp logout', 'Logout from MCP').action(() => { matched = 'mcp logout' })
|
|
2448
|
+
|
|
2449
|
+
parent.use(sub)
|
|
2450
|
+
|
|
2451
|
+
parent.parse(['node', 'bin', 'mcp', 'login'], { run: true })
|
|
2452
|
+
expect(matched).toBe('mcp login')
|
|
2453
|
+
|
|
2454
|
+
matched = ''
|
|
2455
|
+
parent.parse(['node', 'bin', 'mcp', 'logout'], { run: true })
|
|
2456
|
+
expect(matched).toBe('mcp logout')
|
|
2457
|
+
})
|
|
2458
|
+
|
|
2459
|
+
test('help output includes composed commands', () => {
|
|
2460
|
+
const stdout = createTestOutputStream()
|
|
2461
|
+
const parent = goke('mycli', { stdout })
|
|
2462
|
+
const sub = goke()
|
|
2463
|
+
|
|
2464
|
+
sub.command('selfhost', 'Set up on your own workspace')
|
|
2465
|
+
.option('-t, --token [token]', 'Admin token')
|
|
2466
|
+
|
|
2467
|
+
parent.command('init', 'Initialize project')
|
|
2468
|
+
parent.use(sub)
|
|
2469
|
+
parent.help()
|
|
2470
|
+
parent.parse(['node', 'bin', '--help'], { run: false })
|
|
2471
|
+
|
|
2472
|
+
expect(stdout.text).toContain('init')
|
|
2473
|
+
expect(stdout.text).toContain('selfhost')
|
|
2474
|
+
expect(stdout.text).toContain('Set up on your own workspace')
|
|
2475
|
+
})
|
|
2476
|
+
|
|
2477
|
+
test('sub-CLI middlewares are NOT copied to parent', () => {
|
|
2478
|
+
const parent = goke('mycli')
|
|
2479
|
+
const sub = goke()
|
|
2480
|
+
let subMiddlewareCalled = false
|
|
2481
|
+
const order: string[] = []
|
|
2482
|
+
|
|
2483
|
+
sub.use(() => { subMiddlewareCalled = true })
|
|
2484
|
+
sub.command('deploy', 'Deploy').action(() => { order.push('deploy') })
|
|
2485
|
+
|
|
2486
|
+
parent.use(() => { order.push('parent-mw') })
|
|
2487
|
+
parent.use(sub)
|
|
2488
|
+
|
|
2489
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true })
|
|
2490
|
+
|
|
2491
|
+
expect(subMiddlewareCalled).toBe(false)
|
|
2492
|
+
expect(order).toEqual(['parent-mw', 'deploy'])
|
|
2493
|
+
})
|
|
2494
|
+
|
|
2495
|
+
test('parent global options are available to composed commands', () => {
|
|
2496
|
+
const parent = goke('mycli')
|
|
2497
|
+
const sub = goke()
|
|
2498
|
+
let result: any = {}
|
|
2499
|
+
|
|
2500
|
+
parent.option('--verbose', 'Verbose output')
|
|
2501
|
+
|
|
2502
|
+
sub
|
|
2503
|
+
.command('build', 'Build')
|
|
2504
|
+
.option('--target <target>', 'Build target')
|
|
2505
|
+
.action((options) => { result = options })
|
|
2506
|
+
|
|
2507
|
+
parent.use(sub)
|
|
2508
|
+
parent.parse('node bin build --verbose --target production'.split(' '), { run: true })
|
|
2509
|
+
|
|
2510
|
+
expect(result.verbose).toBe(true)
|
|
2511
|
+
expect(result.target).toBe('production')
|
|
2512
|
+
})
|
|
2513
|
+
|
|
2514
|
+
test('composed commands coexist with inline commands', () => {
|
|
2515
|
+
const parent = goke('mycli')
|
|
2516
|
+
const sub = goke()
|
|
2517
|
+
let matched = ''
|
|
2518
|
+
|
|
2519
|
+
parent.command('init', 'Initialize').action(() => { matched = 'init' })
|
|
2520
|
+
|
|
2521
|
+
sub.command('deploy', 'Deploy').action(() => { matched = 'deploy' })
|
|
2522
|
+
sub.command('rollback', 'Rollback').action(() => { matched = 'rollback' })
|
|
2523
|
+
|
|
2524
|
+
parent.use(sub)
|
|
2525
|
+
|
|
2526
|
+
parent.parse(['node', 'bin', 'init'], { run: true })
|
|
2527
|
+
expect(matched).toBe('init')
|
|
2528
|
+
|
|
2529
|
+
matched = ''
|
|
2530
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true })
|
|
2531
|
+
expect(matched).toBe('deploy')
|
|
2532
|
+
|
|
2533
|
+
matched = ''
|
|
2534
|
+
parent.parse(['node', 'bin', 'rollback'], { run: true })
|
|
2535
|
+
expect(matched).toBe('rollback')
|
|
2536
|
+
})
|
|
2537
|
+
})
|
|
2538
|
+
|
|
2539
|
+
describe('getAction()', () => {
|
|
2540
|
+
test('returns the action callable with correct behavior', () => {
|
|
2541
|
+
const stdout = createTestOutputStream()
|
|
2542
|
+
const cli = goke('mycli', { stdout, exit: () => {} })
|
|
2543
|
+
|
|
2544
|
+
const cmd = cli
|
|
2545
|
+
.command('deploy', 'Deploy the app')
|
|
2546
|
+
.option('--env <env>', z.enum(['staging', 'production']).describe('Target environment'))
|
|
2547
|
+
.action((options, { console }) => {
|
|
2548
|
+
console.log(`Deploying to ${options.env}`)
|
|
2549
|
+
})
|
|
2550
|
+
|
|
2551
|
+
const action = cmd.getAction()
|
|
2552
|
+
const ctx = cli.createExecutionContext()
|
|
2553
|
+
action({ env: 'staging' as const, '--': [] }, ctx)
|
|
2554
|
+
expect(stdout.text).toBe('Deploying to staging\n')
|
|
2555
|
+
})
|
|
2556
|
+
|
|
2557
|
+
test('works with positional args', () => {
|
|
2558
|
+
const stdout = createTestOutputStream()
|
|
2559
|
+
const cli = goke('mycli', { stdout, exit: () => {} })
|
|
2560
|
+
|
|
2561
|
+
const cmd = cli
|
|
2562
|
+
.command('get <id>', 'Fetch by id')
|
|
2563
|
+
.option('--format <format>', z.string().describe('Output format'))
|
|
2564
|
+
.action((id, options, { console }) => {
|
|
2565
|
+
console.log(`${id}:${options.format}`)
|
|
2566
|
+
})
|
|
2567
|
+
|
|
2568
|
+
const action = cmd.getAction()
|
|
2569
|
+
const ctx = cli.createExecutionContext()
|
|
2570
|
+
action('abc123', { format: 'json', '--': [] }, ctx)
|
|
2571
|
+
expect(stdout.text).toBe('abc123:json\n')
|
|
2572
|
+
})
|
|
2573
|
+
|
|
2574
|
+
test('throws when no action is registered', () => {
|
|
2575
|
+
const cli = goke('mycli')
|
|
2576
|
+
const cmd = cli.command('noop', 'No action')
|
|
2577
|
+
expect(() => cmd.getAction()).toThrow(/No action registered/)
|
|
2578
|
+
})
|
|
2579
|
+
})
|
|
@@ -389,4 +389,28 @@ describe('createJustBashCommand', () => {
|
|
|
389
389
|
exitCode: 7,
|
|
390
390
|
})
|
|
391
391
|
})
|
|
392
|
+
|
|
393
|
+
test('truncates captured output to just-bash maxOutputSize and appends a notice', async () => {
|
|
394
|
+
const { Bash } = await import('just-bash')
|
|
395
|
+
const cli = gokeTestable('parent')
|
|
396
|
+
|
|
397
|
+
cli
|
|
398
|
+
.command('spam-stderr', 'Write too much stderr')
|
|
399
|
+
.action((options, { console }) => {
|
|
400
|
+
console.error('x'.repeat(60))
|
|
401
|
+
console.error('y'.repeat(60))
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const bash = new Bash({
|
|
405
|
+
executionLimits: { maxOutputSize: 80 },
|
|
406
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const result = await bash.exec('parent spam-stderr')
|
|
410
|
+
|
|
411
|
+
expect(result.exitCode).toBe(0)
|
|
412
|
+
expect(result.stdout).toBe('')
|
|
413
|
+
expect(result.stderr.length).toBeLessThanOrEqual(80)
|
|
414
|
+
expect(result.stderr).toBe(`${'x'.repeat(60)}\n[output truncated]\n`)
|
|
415
|
+
})
|
|
392
416
|
})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from 'vitest'
|
|
6
6
|
import { z } from 'zod'
|
|
7
|
-
import goke, { openInBrowser } from '../index.js'
|
|
7
|
+
import goke, { openInBrowser, generateDocs } from '../index.js'
|
|
8
8
|
import type { GokeOptions, GokeOutputStream } from '../index.js'
|
|
9
9
|
|
|
10
10
|
const ANSI_RE = /\x1B\[[0-9;]*m/g
|
|
@@ -187,7 +187,7 @@ describe('documented command APIs', () => {
|
|
|
187
187
|
expect(stdout.text).toBe('')
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
-
test('openInBrowser prints the URL to stdout in non-tty environments', () => {
|
|
190
|
+
test('openInBrowser prints the URL to stdout in non-tty environments', async () => {
|
|
191
191
|
const url = 'https://example.com/dashboard'
|
|
192
192
|
const originalStdoutWrite = process.stdout.write
|
|
193
193
|
const originalStderrWrite = process.stderr.write
|
|
@@ -209,7 +209,7 @@ describe('documented command APIs', () => {
|
|
|
209
209
|
}) as typeof process.stderr.write
|
|
210
210
|
|
|
211
211
|
try {
|
|
212
|
-
openInBrowser(url)
|
|
212
|
+
await openInBrowser(url)
|
|
213
213
|
} finally {
|
|
214
214
|
process.stdout.write = originalStdoutWrite
|
|
215
215
|
process.stderr.write = originalStderrWrite
|
|
@@ -219,7 +219,155 @@ describe('documented command APIs', () => {
|
|
|
219
219
|
})
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
expect(stdout).toBe(
|
|
223
|
-
expect(stderr).toBe(
|
|
222
|
+
expect(stdout).toBe('')
|
|
223
|
+
expect(stderr).toBe(`${url}\n`)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('generateDocs', () => {
|
|
228
|
+
test('generates pages for CLI with multiple commands', () => {
|
|
229
|
+
const cli = gokeTestable('sentry')
|
|
230
|
+
.version('1.0.0')
|
|
231
|
+
.help()
|
|
232
|
+
|
|
233
|
+
cli
|
|
234
|
+
.command('event view <id>', 'View details of a specific event')
|
|
235
|
+
.option('-w, --web', 'Open in browser')
|
|
236
|
+
.option('--spans <spans>', z.string().default('3').describe('Span tree depth limit'))
|
|
237
|
+
.example('```\nsentry event view abc123\n```')
|
|
238
|
+
|
|
239
|
+
cli
|
|
240
|
+
.command('event list <issue>', 'List events for an issue')
|
|
241
|
+
.option('-n, --limit <limit>', z.number().default(25).describe('Number of events'))
|
|
242
|
+
.option('-q, --query <query>', 'Search query')
|
|
243
|
+
|
|
244
|
+
cli
|
|
245
|
+
.command('hidden-cmd', 'Should not appear')
|
|
246
|
+
.hidden()
|
|
247
|
+
|
|
248
|
+
const pages = generateDocs({ cli })
|
|
249
|
+
|
|
250
|
+
expect(pages.map((p) => p.slug)).toMatchInlineSnapshot(`
|
|
251
|
+
[
|
|
252
|
+
"index",
|
|
253
|
+
"event-view",
|
|
254
|
+
"event-list",
|
|
255
|
+
]
|
|
256
|
+
`)
|
|
257
|
+
|
|
258
|
+
// Index page
|
|
259
|
+
expect(pages[0].content).toMatchInlineSnapshot(`
|
|
260
|
+
"# sentry
|
|
261
|
+
|
|
262
|
+
Version: 1.0.0
|
|
263
|
+
|
|
264
|
+
## Commands
|
|
265
|
+
|
|
266
|
+
| Command | Description |
|
|
267
|
+
|---------|-------------|
|
|
268
|
+
| [\`event view\`](./event-view.md) | View details of a specific event |
|
|
269
|
+
| [\`event list\`](./event-list.md) | List events for an issue |
|
|
270
|
+
|
|
271
|
+
## Global Options
|
|
272
|
+
|
|
273
|
+
| Option | Default | Description |
|
|
274
|
+
|--------|---------|-------------|
|
|
275
|
+
| \`-v, --version\` | - | Display version number |
|
|
276
|
+
| \`-h, --help\` | - | Display this message |
|
|
277
|
+
"
|
|
278
|
+
`)
|
|
279
|
+
|
|
280
|
+
// Command page with examples
|
|
281
|
+
expect(pages[1].content).toMatchInlineSnapshot(`
|
|
282
|
+
"# event view
|
|
283
|
+
|
|
284
|
+
View details of a specific event
|
|
285
|
+
|
|
286
|
+
## Usage
|
|
287
|
+
|
|
288
|
+
\`\`\`sh
|
|
289
|
+
sentry event view <id>
|
|
290
|
+
\`\`\`
|
|
291
|
+
|
|
292
|
+
## Arguments
|
|
293
|
+
|
|
294
|
+
| Argument | Required | Description |
|
|
295
|
+
|----------|----------|-------------|
|
|
296
|
+
| \`<id>\` | Yes | id |
|
|
297
|
+
|
|
298
|
+
## Options
|
|
299
|
+
|
|
300
|
+
| Option | Default | Description |
|
|
301
|
+
|--------|---------|-------------|
|
|
302
|
+
| \`-w, --web\` | - | Open in browser |
|
|
303
|
+
| \`--spans <spans>\` | \`3\` | Span tree depth limit |
|
|
304
|
+
|
|
305
|
+
## Global Options
|
|
306
|
+
|
|
307
|
+
| Option | Default | Description |
|
|
308
|
+
|--------|---------|-------------|
|
|
309
|
+
| \`-v, --version\` | - | Display version number |
|
|
310
|
+
| \`-h, --help\` | - | Display this message |
|
|
311
|
+
|
|
312
|
+
## Examples
|
|
313
|
+
|
|
314
|
+
\`\`\`
|
|
315
|
+
sentry event view abc123
|
|
316
|
+
\`\`\`
|
|
317
|
+
"
|
|
318
|
+
`)
|
|
319
|
+
|
|
320
|
+
// Command page without examples
|
|
321
|
+
expect(pages[2].content).toMatchInlineSnapshot(`
|
|
322
|
+
"# event list
|
|
323
|
+
|
|
324
|
+
List events for an issue
|
|
325
|
+
|
|
326
|
+
## Usage
|
|
327
|
+
|
|
328
|
+
\`\`\`sh
|
|
329
|
+
sentry event list <issue>
|
|
330
|
+
\`\`\`
|
|
331
|
+
|
|
332
|
+
## Arguments
|
|
333
|
+
|
|
334
|
+
| Argument | Required | Description |
|
|
335
|
+
|----------|----------|-------------|
|
|
336
|
+
| \`<issue>\` | Yes | issue |
|
|
337
|
+
|
|
338
|
+
## Options
|
|
339
|
+
|
|
340
|
+
| Option | Default | Description |
|
|
341
|
+
|--------|---------|-------------|
|
|
342
|
+
| \`-n, --limit <limit>\` | \`25\` | Number of events |
|
|
343
|
+
| \`-q, --query <query>\` | - | Search query |
|
|
344
|
+
|
|
345
|
+
## Global Options
|
|
346
|
+
|
|
347
|
+
| Option | Default | Description |
|
|
348
|
+
|--------|---------|-------------|
|
|
349
|
+
| \`-v, --version\` | - | Display version number |
|
|
350
|
+
| \`-h, --help\` | - | Display this message |
|
|
351
|
+
"
|
|
352
|
+
`)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('handles CLI with no commands', () => {
|
|
356
|
+
const cli = gokeTestable('empty')
|
|
357
|
+
const pages = generateDocs({ cli })
|
|
358
|
+
expect(pages).toEqual([])
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('skips deprecated options', () => {
|
|
362
|
+
const cli = gokeTestable('mycli')
|
|
363
|
+
cli
|
|
364
|
+
.command('deploy', 'Deploy the app')
|
|
365
|
+
.option('--force', 'Skip confirmation')
|
|
366
|
+
.option('--old-flag', z.boolean().meta({ deprecated: true }).describe('Use --force instead'))
|
|
367
|
+
|
|
368
|
+
const pages = generateDocs({ cli })
|
|
369
|
+
const deployPage = pages.find((p) => p.slug === 'deploy')!
|
|
370
|
+
// The deprecated option should not appear
|
|
371
|
+
expect(deployPage.content).not.toContain('old-flag')
|
|
224
372
|
})
|
|
225
373
|
})
|
|
@@ -481,6 +481,74 @@ describe('type-level: README TypeScript examples', () => {
|
|
|
481
481
|
})
|
|
482
482
|
})
|
|
483
483
|
|
|
484
|
+
test('use() with sub-CLI preserves parent middleware typing', () => {
|
|
485
|
+
const sub = goke()
|
|
486
|
+
sub
|
|
487
|
+
.command('deploy', 'Deploy the app')
|
|
488
|
+
.option('--force', z.boolean())
|
|
489
|
+
.action((options) => {
|
|
490
|
+
// Sub-CLI command's action sees its own options
|
|
491
|
+
expectTypeOf(options.force).toEqualTypeOf<boolean | undefined>()
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
goke('test')
|
|
495
|
+
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
496
|
+
.use(sub)
|
|
497
|
+
.use((options) => {
|
|
498
|
+
// Parent middleware still sees parent's accumulated options after .use(subCli)
|
|
499
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean>()
|
|
500
|
+
})
|
|
501
|
+
.command('build', 'Build')
|
|
502
|
+
.option('--target <target>', z.string())
|
|
503
|
+
.action((options) => {
|
|
504
|
+
// Parent's inline command still sees global options
|
|
505
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean>()
|
|
506
|
+
expectTypeOf(options.target).toEqualTypeOf<string>()
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('use() with sub-CLI does not leak sub-CLI types to parent', () => {
|
|
511
|
+
const sub = goke()
|
|
512
|
+
.option('--sub-only <val>', z.string())
|
|
513
|
+
sub.command('sub-cmd', 'Sub command').action(() => {})
|
|
514
|
+
|
|
515
|
+
goke('test')
|
|
516
|
+
.option('--parent-only <val>', z.number())
|
|
517
|
+
.use(sub)
|
|
518
|
+
.use((options) => {
|
|
519
|
+
expectTypeOf(options.parentOnly).toEqualTypeOf<number>()
|
|
520
|
+
// @ts-expect-error subOnly is not declared on the parent
|
|
521
|
+
options.subOnly
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test('getAction() returns correctly typed function', () => {
|
|
526
|
+
const cmd = goke('test')
|
|
527
|
+
.command('convert <input> <output>', 'Convert file format')
|
|
528
|
+
.option('--quality <quality>', z.number())
|
|
529
|
+
.option('--format <format>', z.enum(['png', 'jpg']))
|
|
530
|
+
.action((input, output, options, ctx) => {
|
|
531
|
+
void input; void output; void options; void ctx
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const action = cmd.getAction()
|
|
535
|
+
expectTypeOf(action).parameter(0).toEqualTypeOf<string>() // input
|
|
536
|
+
expectTypeOf(action).parameter(1).toEqualTypeOf<string>() // output
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test('getAction() with no positional args has (options, ctx) signature', () => {
|
|
540
|
+
const cmd = goke('test')
|
|
541
|
+
.command('deploy', 'Deploy')
|
|
542
|
+
.option('--env <env>', z.enum(['staging', 'production']))
|
|
543
|
+
.action((options, ctx) => {
|
|
544
|
+
void options; void ctx
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
const action = cmd.getAction()
|
|
548
|
+
// First param is options with env
|
|
549
|
+
expectTypeOf(action).parameter(0).toMatchTypeOf<{ env: 'staging' | 'production' }>()
|
|
550
|
+
})
|
|
551
|
+
|
|
484
552
|
test('README global options and middleware example stays typed end-to-end', () => {
|
|
485
553
|
// `z.boolean().default(false)` and `z.string().default(...)` are
|
|
486
554
|
// effectively required at runtime: the default applies when the flag is
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI coding agent detection for goke CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Detects whether the current process is running inside an AI coding agent
|
|
5
|
+
* (Claude, Cursor, Codex, Gemini, etc.) by checking environment variables.
|
|
6
|
+
* Ported from unjs/std-env with the same detection logic.
|
|
7
|
+
*
|
|
8
|
+
* CLI authors can use this to adjust behavior: skip interactive prompts,
|
|
9
|
+
* prefer structured output, avoid browser opens, etc.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const env: Record<string, string | undefined> =
|
|
13
|
+
globalThis.process?.env || Object.create(null)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Known AI coding agent names.
|
|
17
|
+
*/
|
|
18
|
+
export type AgentName =
|
|
19
|
+
| (string & {})
|
|
20
|
+
| 'cursor'
|
|
21
|
+
| 'claude'
|
|
22
|
+
| 'devin'
|
|
23
|
+
| 'replit'
|
|
24
|
+
| 'gemini'
|
|
25
|
+
| 'codex'
|
|
26
|
+
| 'auggie'
|
|
27
|
+
| 'opencode'
|
|
28
|
+
| 'kiro'
|
|
29
|
+
| 'goose'
|
|
30
|
+
| 'pi'
|
|
31
|
+
|
|
32
|
+
type EnvCheck = string | (() => boolean)
|
|
33
|
+
|
|
34
|
+
type InternalAgent = [agentName: AgentName, envChecks: EnvCheck[]]
|
|
35
|
+
|
|
36
|
+
function envMatcher(envKey: string, regex: RegExp) {
|
|
37
|
+
return () => {
|
|
38
|
+
const value = env[envKey]
|
|
39
|
+
return value ? regex.test(value) : false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Detection order matters: specific agents first, IDE-based agents last
|
|
44
|
+
// so that agents running inside those IDEs are detected by their own env vars first.
|
|
45
|
+
const agents: InternalAgent[] = [
|
|
46
|
+
// CLI agents
|
|
47
|
+
['claude', ['CLAUDECODE', 'CLAUDE_CODE']],
|
|
48
|
+
['replit', ['REPL_ID']],
|
|
49
|
+
['gemini', ['GEMINI_CLI']],
|
|
50
|
+
['codex', ['CODEX_SANDBOX', 'CODEX_THREAD_ID']],
|
|
51
|
+
['opencode', ['OPENCODE']],
|
|
52
|
+
['pi', [envMatcher('PATH', /\.pi[\\/]agent/)]],
|
|
53
|
+
['auggie', ['AUGMENT_AGENT']],
|
|
54
|
+
['goose', ['GOOSE_PROVIDER']],
|
|
55
|
+
|
|
56
|
+
// IDE-based agents (checked last)
|
|
57
|
+
['devin', [envMatcher('EDITOR', /devin/)]],
|
|
58
|
+
['cursor', ['CURSOR_AGENT']],
|
|
59
|
+
['kiro', [envMatcher('TERM_PROGRAM', /kiro/)]],
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Information about the detected AI coding agent.
|
|
64
|
+
*/
|
|
65
|
+
export type AgentInfo = {
|
|
66
|
+
/** The name of the detected agent, or undefined if no agent was detected. */
|
|
67
|
+
name?: AgentName
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detect the current AI coding agent from environment variables.
|
|
72
|
+
*
|
|
73
|
+
* Checks `AI_AGENT` env var first (explicit override), then scans for
|
|
74
|
+
* known agent-specific env vars in priority order.
|
|
75
|
+
*
|
|
76
|
+
* Supported agents: `cursor`, `claude`, `devin`, `replit`, `gemini`,
|
|
77
|
+
* `codex`, `auggie`, `opencode`, `kiro`, `goose`, `pi`
|
|
78
|
+
*/
|
|
79
|
+
export function detectAgent(): AgentInfo {
|
|
80
|
+
const aiAgent = env.AI_AGENT
|
|
81
|
+
if (aiAgent) {
|
|
82
|
+
return { name: aiAgent.toLowerCase() }
|
|
83
|
+
}
|
|
84
|
+
for (const [name, checks] of agents) {
|
|
85
|
+
for (const check of checks) {
|
|
86
|
+
if (typeof check === 'string' ? env[check] : check()) {
|
|
87
|
+
return { name }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Detected agent info, evaluated once at import time. */
|
|
95
|
+
export const agentInfo: AgentInfo = /* #__PURE__ */ detectAgent()
|
|
96
|
+
|
|
97
|
+
/** Name of the detected agent, or undefined if not running inside one. */
|
|
98
|
+
export const agent: AgentName | undefined = agentInfo.name
|
|
99
|
+
|
|
100
|
+
/** Whether the current process is running inside an AI coding agent. */
|
|
101
|
+
export const isAgent: boolean = !!agentInfo.name
|