goke 6.9.0 → 6.11.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.
Files changed (40) hide show
  1. package/dist/__test__/completions.test.d.ts +9 -0
  2. package/dist/__test__/completions.test.d.ts.map +1 -0
  3. package/dist/__test__/completions.test.js +774 -0
  4. package/dist/__test__/index.test.js +436 -308
  5. package/dist/__test__/just-bash.test.js +7 -7
  6. package/dist/__test__/readme-examples.test.js +149 -13
  7. package/dist/__test__/types.test-d.js +27 -0
  8. package/dist/agents.d.ts +38 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +63 -0
  11. package/dist/completions.d.ts +88 -0
  12. package/dist/completions.d.ts.map +1 -0
  13. package/dist/completions.js +315 -0
  14. package/dist/goke.d.ts +95 -5
  15. package/dist/goke.d.ts.map +1 -1
  16. package/dist/goke.js +487 -4
  17. package/dist/index.d.ts +9 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -1
  20. package/dist/just-bash.d.ts.map +1 -1
  21. package/dist/just-bash.js +1 -2
  22. package/dist/runtime-browser.d.ts +1 -1
  23. package/dist/runtime-browser.d.ts.map +1 -1
  24. package/dist/runtime-browser.js +1 -1
  25. package/dist/runtime-node.d.ts +1 -1
  26. package/dist/runtime-node.d.ts.map +1 -1
  27. package/dist/runtime-node.js +22 -13
  28. package/package.json +1 -1
  29. package/src/__test__/completions.test.ts +902 -0
  30. package/src/__test__/index.test.ts +471 -308
  31. package/src/__test__/just-bash.test.ts +7 -7
  32. package/src/__test__/readme-examples.test.ts +161 -13
  33. package/src/__test__/types.test-d.ts +27 -0
  34. package/src/agents.ts +101 -0
  35. package/src/completions.ts +363 -0
  36. package/src/goke.ts +540 -8
  37. package/src/index.ts +11 -2
  38. package/src/just-bash.ts +1 -2
  39. package/src/runtime-browser.ts +1 -1
  40. package/src/runtime-node.ts +19 -11
@@ -54,7 +54,7 @@ function gokeTestable(name = '', options?: Partial<GokeOptions>) {
54
54
  }
55
55
 
56
56
  describe('injected execution context', () => {
57
- test('command action receives injected console and process', () => {
57
+ test('command action receives injected console and process', async () => {
58
58
  const stdout = createTestOutputStream()
59
59
  const cli = gokeTestable('mycli', { stdout })
60
60
  let seenArgv: string[] | undefined
@@ -66,13 +66,13 @@ describe('injected execution context', () => {
66
66
  seenArgv = process.argv
67
67
  })
68
68
 
69
- cli.parse(['node', 'bin', 'status'], { run: true })
69
+ await cli.parse(['node', 'bin', 'status'], { run: true })
70
70
 
71
71
  expect(stdout.text).toBe('ready\n')
72
72
  expect(seenArgv).toEqual(['node', 'bin', 'status'])
73
73
  })
74
74
 
75
- test('middleware receives injected console and process', () => {
75
+ test('middleware receives injected console and process', async () => {
76
76
  const stdout = createTestOutputStream()
77
77
  const cli = gokeTestable('mycli', { stdout })
78
78
  let seenArgv: string[] | undefined
@@ -85,7 +85,7 @@ describe('injected execution context', () => {
85
85
  .command('build', 'Build')
86
86
  .action(() => {})
87
87
 
88
- cli.parse(['node', 'bin', 'build'], { run: true })
88
+ await cli.parse(['node', 'bin', 'build'], { run: true })
89
89
 
90
90
  expect(stdout.text).toBe('middleware\n')
91
91
  expect(seenArgv).toEqual(['node', 'bin', 'build'])
@@ -93,14 +93,14 @@ describe('injected execution context', () => {
93
93
  })
94
94
 
95
95
  describe('clone', () => {
96
- test('clone creates isolated parse state', () => {
96
+ test('clone creates isolated parse state', async () => {
97
97
  const cli = gokeTestable('mycli')
98
98
 
99
99
  cli.command('build', 'Build').action(() => {})
100
100
 
101
101
  const cloned = cli.clone({ exit: () => {} })
102
102
 
103
- cloned.parse(['node', 'bin', 'build'], { run: false })
103
+ await cloned.parse(['node', 'bin', 'build'], { run: false })
104
104
 
105
105
  expect(cloned).not.toBe(cli)
106
106
  expect(cloned.matchedCommandName).toBe('build')
@@ -109,7 +109,7 @@ describe('clone', () => {
109
109
  })
110
110
 
111
111
  describe('createExecutionContext', () => {
112
- test('returns a context that mirrors the cli defaults when called with no override', () => {
112
+ test('returns a context that mirrors the cli defaults when called with no override', async () => {
113
113
  const stdout = createTestOutputStream()
114
114
  const stderr = createTestOutputStream()
115
115
  const cli = gokeTestable('mycli', {
@@ -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
@@ -52,7 +52,7 @@ describe('README smoke tests', () => {
52
52
  console.log(`logs ${deploymentId} ${options.lines}`)
53
53
  })
54
54
 
55
- cli.parse(['node', 'bin', '--env', 'production', 'up', '--dry-run'], { run: false })
55
+ await cli.parse(['node', 'bin', '--env', 'production', 'up', '--dry-run'], { run: false })
56
56
  await cli.runMatchedCommand()
57
57
 
58
58
  expect(stdout.text).toBe(
@@ -61,7 +61,7 @@ describe('README smoke tests', () => {
61
61
 
62
62
  stdout.lines.length = 0
63
63
 
64
- cli.parse(['node', 'bin', 'logs', 'dep_123'], { run: false })
64
+ await cli.parse(['node', 'bin', 'logs', 'dep_123'], { run: false })
65
65
  await cli.runMatchedCommand()
66
66
 
67
67
  expect(stdout.text).toBe('Environment: staging\nlogs dep_123 100\n')
@@ -96,7 +96,7 @@ describe('README smoke tests', () => {
96
96
 
97
97
  expect(stripAnsi(cli.helpText())).toContain('mycli lint src/**/*.ts')
98
98
 
99
- cli.parse(['node', 'bin', '--type', 'bun', '--name', 'Tommy', 'build', 'src/index.ts', '--minify'], { run: false })
99
+ await cli.parse(['node', 'bin', '--type', 'bun', '--name', 'Tommy', 'build', 'src/index.ts', '--minify'], { run: false })
100
100
  await cli.runMatchedCommand()
101
101
 
102
102
  expect(stdout.text).toBe(
@@ -135,7 +135,7 @@ describe('README smoke tests', () => {
135
135
  )
136
136
  })
137
137
 
138
- cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: false })
138
+ await cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: false })
139
139
  await cli.runMatchedCommand()
140
140
 
141
141
  expect(stdout.text).toBe(
@@ -144,7 +144,7 @@ describe('README smoke tests', () => {
144
144
 
145
145
  stdout.lines.length = 0
146
146
 
147
- cli.parse(['node', 'bin', 'logs', 'abc123', '--follow'], { run: false })
147
+ await cli.parse(['node', 'bin', 'logs', 'abc123', '--follow'], { run: false })
148
148
  await cli.runMatchedCommand()
149
149
 
150
150
  expect(stdout.text).toBe(
@@ -154,7 +154,7 @@ describe('README smoke tests', () => {
154
154
  })
155
155
 
156
156
  describe('documented command APIs', () => {
157
- test('alias runs the same command through a short name', () => {
157
+ test('alias runs the same command through a short name', async () => {
158
158
  const cli = gokeTestable('mycli')
159
159
  let seen = ''
160
160
 
@@ -162,12 +162,12 @@ describe('documented command APIs', () => {
162
162
  seen = 'install'
163
163
  })
164
164
 
165
- cli.parse(['node', 'bin', 'i'], { run: true })
165
+ await cli.parse(['node', 'bin', 'i'], { run: true })
166
166
 
167
167
  expect(seen).toBe('install')
168
168
  })
169
169
 
170
- test('command helpText returns command-specific help without printing', () => {
170
+ test('command helpText returns command-specific help without printing', async () => {
171
171
  const stdout = createTestOutputStream()
172
172
  const cli = goke('mycli', { stdout })
173
173
 
@@ -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(`${url}\n`)
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', async () => {
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', async () => {
356
+ const cli = gokeTestable('empty')
357
+ const pages = generateDocs({ cli })
358
+ expect(pages).toEqual([])
359
+ })
360
+
361
+ test('skips deprecated options', async () => {
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
  })
@@ -522,6 +522,33 @@ describe('type-level: README TypeScript examples', () => {
522
522
  })
523
523
  })
524
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
+
525
552
  test('README global options and middleware example stays typed end-to-end', () => {
526
553
  // `z.boolean().default(false)` and `z.string().default(...)` are
527
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