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.
Files changed (42) 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 +188 -0
  5. package/dist/__test__/just-bash.test.js +19 -0
  6. package/dist/__test__/readme-examples.test.js +141 -5
  7. package/dist/__test__/types.test-d.js +64 -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 +115 -2
  15. package/dist/goke.d.ts.map +1 -1
  16. package/dist/goke.js +487 -25
  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 +1 -1
  21. package/dist/just-bash.d.ts.map +1 -1
  22. package/dist/just-bash.js +80 -15
  23. package/dist/runtime-browser.d.ts +1 -1
  24. package/dist/runtime-browser.d.ts.map +1 -1
  25. package/dist/runtime-browser.js +1 -1
  26. package/dist/runtime-node.d.ts +1 -1
  27. package/dist/runtime-node.d.ts.map +1 -1
  28. package/dist/runtime-node.js +22 -13
  29. package/package.json +1 -1
  30. package/src/__test__/completions.test.ts +902 -0
  31. package/src/__test__/index.test.ts +241 -0
  32. package/src/__test__/just-bash.test.ts +24 -0
  33. package/src/__test__/readme-examples.test.ts +153 -5
  34. package/src/__test__/types.test-d.ts +68 -0
  35. package/src/agents.ts +101 -0
  36. package/src/completions.ts +363 -0
  37. package/src/goke.ts +564 -3
  38. package/src/index.ts +11 -2
  39. package/src/just-bash.ts +92 -18
  40. package/src/runtime-browser.ts +1 -1
  41. package/src/runtime-node.ts +19 -11
  42. package/README.md +0 -1254
@@ -0,0 +1,902 @@
1
+ /**
2
+ * Tests for shell completion support.
3
+ *
4
+ * Tests the getCompletions() method (which computes completions for given args),
5
+ * the --get-goke-completions flag interception in parse(), the script generation,
6
+ * and the completions install/uninstall/script commands.
7
+ */
8
+
9
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'
10
+ import { z } from 'zod'
11
+ import goke from '../index.js'
12
+ import { generateCompletionScript } from '../index.js'
13
+ import type { GokeOptions, GokeOutputStream } from '../index.js'
14
+
15
+ const ANSI_RE = /\x1B\[[0-9;]*m/g
16
+ const stripAnsi = (text: string) => text.replace(ANSI_RE, '')
17
+
18
+ function createTestOutputStream(): GokeOutputStream & { lines: string[]; readonly text: string } {
19
+ const lines: string[] = []
20
+ return {
21
+ lines,
22
+ get text() { return stripAnsi(lines.join('')) },
23
+ write(data: string) { lines.push(data) },
24
+ }
25
+ }
26
+
27
+ function gokeTestable(name = '', options?: Partial<GokeOptions>) {
28
+ return goke(name, {
29
+ ...options,
30
+ exit: () => {},
31
+ })
32
+ }
33
+
34
+ function buildTestCli(stdout?: GokeOutputStream) {
35
+ const out = stdout ?? createTestOutputStream()
36
+ const cli = gokeTestable('mycli', { stdout: out })
37
+ .help()
38
+ .completions()
39
+
40
+ cli.command('deploy', 'Deploy the app')
41
+ .option('--env <env>', z.enum(['staging', 'production']).describe('Target environment'))
42
+ .option('--dry-run', 'Preview without deploying')
43
+
44
+ cli.command('deploy rollback', 'Rollback a deployment')
45
+ .option('--to <version>', 'Target version')
46
+
47
+ cli.command('logs <deploymentId>', 'Stream deployment logs')
48
+ .option('--lines <n>', z.number().default(100).describe('Lines to tail'))
49
+
50
+ cli.command('status', 'Show current status')
51
+
52
+ cli.command('internal-debug', 'Debug command')
53
+ .hidden()
54
+
55
+ return { cli, stdout: out as GokeOutputStream & { lines: string[]; text: string } }
56
+ }
57
+
58
+ describe('getCompletions', () => {
59
+ let originalShell: string | undefined
60
+
61
+ beforeEach(() => {
62
+ originalShell = process.env.SHELL
63
+ })
64
+
65
+ afterEach(() => {
66
+ if (originalShell !== undefined) {
67
+ process.env.SHELL = originalShell
68
+ } else {
69
+ delete process.env.SHELL
70
+ }
71
+ })
72
+
73
+ test('returns all visible commands when current word is empty', () => {
74
+ process.env.SHELL = '/bin/bash'
75
+ const { cli } = buildTestCli()
76
+ const completions = cli.getCompletions(['mycli', ''])
77
+
78
+ expect(completions).toMatchInlineSnapshot(`
79
+ [
80
+ "deploy",
81
+ "logs",
82
+ "status",
83
+ "completions",
84
+ ]
85
+ `)
86
+ })
87
+
88
+ test('hidden commands are excluded', () => {
89
+ process.env.SHELL = '/bin/bash'
90
+ const { cli } = buildTestCli()
91
+ const completions = cli.getCompletions(['mycli', ''])
92
+
93
+ expect(completions).not.toContain('internal-debug')
94
+ })
95
+
96
+ test('filters commands by prefix', () => {
97
+ process.env.SHELL = '/bin/bash'
98
+ const { cli } = buildTestCli()
99
+ const completions = cli.getCompletions(['mycli', 'dep'])
100
+
101
+ expect(completions).toMatchInlineSnapshot(`
102
+ [
103
+ "deploy",
104
+ ]
105
+ `)
106
+ })
107
+
108
+ test('suggests options after matched command', () => {
109
+ process.env.SHELL = '/bin/bash'
110
+ const { cli } = buildTestCli()
111
+ const completions = cli.getCompletions(['mycli', 'deploy', '--'])
112
+
113
+ expect(completions).toContain('--env')
114
+ expect(completions).toContain('--dry-run')
115
+ expect(completions).toContain('--help')
116
+ })
117
+
118
+ test('suggests subcommands after matched command prefix', () => {
119
+ process.env.SHELL = '/bin/bash'
120
+ const { cli } = buildTestCli()
121
+ const completions = cli.getCompletions(['mycli', 'deploy', ''])
122
+
123
+ expect(completions).toContain('rollback')
124
+ })
125
+
126
+ test('includes descriptions in zsh format', () => {
127
+ process.env.SHELL = '/bin/zsh'
128
+ const { cli } = buildTestCli()
129
+ const completions = cli.getCompletions(['mycli', ''])
130
+
131
+ // zsh format is name:description
132
+ expect(completions.some((c) => c.includes(':Deploy the app'))).toBe(true)
133
+ expect(completions.some((c) => c.includes(':Stream deployment logs'))).toBe(true)
134
+ })
135
+
136
+ test('zsh option completions include descriptions', () => {
137
+ process.env.SHELL = '/bin/zsh'
138
+ const { cli } = buildTestCli()
139
+ const completions = cli.getCompletions(['mycli', 'deploy', '--'])
140
+
141
+ expect(completions.some((c) => c.includes('--env:Target environment'))).toBe(true)
142
+ expect(completions.some((c) => c.includes('--dry-run:Preview without deploying'))).toBe(true)
143
+ })
144
+
145
+ test('filters options by prefix', () => {
146
+ process.env.SHELL = '/bin/bash'
147
+ const { cli } = buildTestCli()
148
+ const completions = cli.getCompletions(['mycli', 'deploy', '--dr'])
149
+
150
+ expect(completions).toMatchInlineSnapshot(`
151
+ [
152
+ "--dry-run",
153
+ ]
154
+ `)
155
+ })
156
+
157
+ test('suggests global options at root level', () => {
158
+ process.env.SHELL = '/bin/bash'
159
+ const { cli } = buildTestCli()
160
+ const completions = cli.getCompletions(['mycli', '--'])
161
+
162
+ expect(completions).toContain('--help')
163
+ })
164
+
165
+ test('multi-word command completion', () => {
166
+ process.env.SHELL = '/bin/bash'
167
+ const { cli } = buildTestCli()
168
+ // User typed "mycli deploy " and hits tab
169
+ const completions = cli.getCompletions(['mycli', 'deploy', 'roll'])
170
+
171
+ expect(completions).toMatchInlineSnapshot(`
172
+ [
173
+ "rollback",
174
+ ]
175
+ `)
176
+ })
177
+ })
178
+
179
+ describe('--get-goke-completions flag in parse()', () => {
180
+ test('prints completions to stdout and exits', () => {
181
+ const stdout = createTestOutputStream()
182
+ const { cli } = buildTestCli(stdout)
183
+
184
+ // Simulate: mycli --get-goke-completions mycli dep
185
+ cli.parse(['node', 'bin', '--get-goke-completions', 'mycli', 'dep'])
186
+
187
+ // Should have printed completions to stdout
188
+ expect(stdout.text).toContain('deploy')
189
+ })
190
+
191
+ test('does not run any command action', () => {
192
+ const stdout = createTestOutputStream()
193
+ const actionSpy = vi.fn()
194
+ const cli = gokeTestable('mycli', { stdout })
195
+ .completions()
196
+ cli.command('deploy', 'Deploy').action(actionSpy)
197
+
198
+ cli.parse(['node', 'bin', '--get-goke-completions', 'mycli', 'deploy', ''])
199
+
200
+ expect(actionSpy).not.toHaveBeenCalled()
201
+ })
202
+ })
203
+
204
+ describe('generateCompletionScript', () => {
205
+ test('zsh template has #compdef header', () => {
206
+ const script = generateCompletionScript('zsh', 'my-cli', '/usr/local/bin/my-cli')
207
+
208
+ expect(script).toContain('#compdef my-cli')
209
+ expect(script).toContain('--get-goke-completions')
210
+ expect(script).toContain('/usr/local/bin/my-cli')
211
+ expect(script).toContain('_my_cli_completions')
212
+ })
213
+
214
+ test('bash template has complete command', () => {
215
+ const script = generateCompletionScript('bash', 'my-cli', '/usr/local/bin/my-cli')
216
+
217
+ expect(script).toContain('complete -o bashdefault')
218
+ expect(script).toContain('--get-goke-completions')
219
+ expect(script).toContain('/usr/local/bin/my-cli')
220
+ expect(script).toContain('_my_cli_completions')
221
+ })
222
+
223
+ test('uses cliName as fallback path when cliPath not provided', () => {
224
+ const script = generateCompletionScript('zsh', 'my-cli')
225
+
226
+ expect(script).toContain('my-cli --get-goke-completions')
227
+ })
228
+
229
+ test('escapes special characters in function names', () => {
230
+ const script = generateCompletionScript('zsh', 'my-cli.js', './my-cli.js')
231
+
232
+ // Function name should use underscores
233
+ expect(script).toContain('_my_cli_js_completions')
234
+ // But app_name (for compdef) should stay as-is
235
+ expect(script).toContain('#compdef my-cli.js')
236
+ })
237
+ })
238
+
239
+ describe('completions commands', () => {
240
+ test('completions script prints zsh script', () => {
241
+ process.env.SHELL = '/bin/zsh'
242
+ const stdout = createTestOutputStream()
243
+ const cli = gokeTestable('mycli', { stdout })
244
+ .completions()
245
+
246
+ cli.parse(['node', 'bin', 'completions', 'script'])
247
+
248
+ expect(stdout.text).toContain('#compdef mycli')
249
+ expect(stdout.text).toContain('--get-goke-completions')
250
+ })
251
+
252
+ test('completions script prints bash script with --shell', () => {
253
+ const stdout = createTestOutputStream()
254
+ const cli = gokeTestable('mycli', { stdout })
255
+ .completions()
256
+
257
+ cli.parse(['node', 'bin', 'completions', 'script', '--shell', 'bash'])
258
+
259
+ expect(stdout.text).toContain('complete -o bashdefault')
260
+ expect(stdout.text).toContain('--get-goke-completions')
261
+ })
262
+
263
+ test('completions script rejects invalid shell value', async () => {
264
+ const stderr = createTestOutputStream()
265
+ const cli = gokeTestable('mycli', { stderr })
266
+ .completions()
267
+
268
+ cli.parse(['node', 'bin', 'completions', 'script', '--shell', 'fish'])
269
+ // The error is caught by handleCliError and printed to stderr
270
+ // Wait a tick for the sync action to complete
271
+ await new Promise((r) => setTimeout(r, 10))
272
+ expect(stderr.text).toContain('Invalid shell "fish"')
273
+ })
274
+ })
275
+
276
+ describe('GOKE_COMPLETION_SHELL env var', () => {
277
+ let originalShell: string | undefined
278
+ let originalCompletionShell: string | undefined
279
+
280
+ beforeEach(() => {
281
+ originalShell = process.env.SHELL
282
+ originalCompletionShell = process.env.GOKE_COMPLETION_SHELL
283
+ })
284
+
285
+ afterEach(() => {
286
+ if (originalShell !== undefined) process.env.SHELL = originalShell
287
+ else delete process.env.SHELL
288
+ if (originalCompletionShell !== undefined) process.env.GOKE_COMPLETION_SHELL = originalCompletionShell
289
+ else delete process.env.GOKE_COMPLETION_SHELL
290
+ })
291
+
292
+ test('uses GOKE_COMPLETION_SHELL over SHELL for format detection', () => {
293
+ // Login shell is zsh but the bash shim sets GOKE_COMPLETION_SHELL=bash
294
+ process.env.SHELL = '/bin/zsh'
295
+ process.env.GOKE_COMPLETION_SHELL = 'bash'
296
+ const { cli } = buildTestCli()
297
+ const completions = cli.getCompletions(['mycli', ''])
298
+
299
+ // Should NOT include :description format (that's zsh-only)
300
+ for (const c of completions) {
301
+ expect(c).not.toContain(':')
302
+ }
303
+ })
304
+
305
+ test('zsh template sets GOKE_COMPLETION_SHELL=zsh', () => {
306
+ const script = generateCompletionScript('zsh', 'mycli')
307
+ expect(script).toContain('GOKE_COMPLETION_SHELL=zsh')
308
+ })
309
+
310
+ test('bash template sets GOKE_COMPLETION_SHELL=bash', () => {
311
+ const script = generateCompletionScript('bash', 'mycli')
312
+ expect(script).toContain('GOKE_COMPLETION_SHELL=bash')
313
+ })
314
+ })
315
+
316
+ describe('option value position', () => {
317
+ let originalShell: string | undefined
318
+
319
+ beforeEach(() => {
320
+ originalShell = process.env.SHELL
321
+ process.env.SHELL = '/bin/bash'
322
+ delete process.env.GOKE_COMPLETION_SHELL
323
+ })
324
+
325
+ afterEach(() => {
326
+ if (originalShell !== undefined) process.env.SHELL = originalShell
327
+ else delete process.env.SHELL
328
+ })
329
+
330
+ test('returns empty when previous token is a value-taking option', () => {
331
+ const { cli } = buildTestCli()
332
+ // mycli deploy --env <TAB> — should not suggest flags
333
+ const completions = cli.getCompletions(['mycli', 'deploy', '--env', ''])
334
+
335
+ expect(completions).toEqual([])
336
+ })
337
+
338
+ test('still suggests flags when previous token is a boolean option', () => {
339
+ const { cli } = buildTestCli()
340
+ // mycli deploy --dry-run <TAB> — boolean flag, should still suggest
341
+ const completions = cli.getCompletions(['mycli', 'deploy', '--dry-run', '--'])
342
+
343
+ expect(completions).toContain('--env')
344
+ })
345
+ })
346
+
347
+ describe('default command options', () => {
348
+ let originalShell: string | undefined
349
+
350
+ beforeEach(() => {
351
+ originalShell = process.env.SHELL
352
+ process.env.SHELL = '/bin/bash'
353
+ delete process.env.GOKE_COMPLETION_SHELL
354
+ })
355
+
356
+ afterEach(() => {
357
+ if (originalShell !== undefined) process.env.SHELL = originalShell
358
+ else delete process.env.SHELL
359
+ })
360
+
361
+ test('includes default command options at root level', () => {
362
+ const cli = gokeTestable('mycli')
363
+ .help()
364
+ .completions()
365
+
366
+ cli.command('', 'Default action')
367
+ .option('--env <env>', 'Target environment')
368
+ .option('--dry-run', 'Preview')
369
+
370
+ const completions = cli.getCompletions(['mycli', '--'])
371
+
372
+ expect(completions).toContain('--env')
373
+ expect(completions).toContain('--dry-run')
374
+ expect(completions).toContain('--help')
375
+ })
376
+ })
377
+
378
+ describe('alias suppression', () => {
379
+ let originalShell: string | undefined
380
+
381
+ beforeEach(() => {
382
+ originalShell = process.env.SHELL
383
+ process.env.SHELL = '/bin/bash'
384
+ delete process.env.GOKE_COMPLETION_SHELL
385
+ })
386
+
387
+ afterEach(() => {
388
+ if (originalShell !== undefined) process.env.SHELL = originalShell
389
+ else delete process.env.SHELL
390
+ })
391
+
392
+ test('suppresses --dry-run when -d alias was already used', () => {
393
+ const cli = gokeTestable('mycli')
394
+ .completions()
395
+
396
+ cli.command('deploy', 'Deploy')
397
+ .option('-d, --dry-run', 'Preview')
398
+ .option('--env <env>', 'Environment')
399
+
400
+ const completions = cli.getCompletions(['mycli', 'deploy', '-d', '--'])
401
+
402
+ expect(completions).not.toContain('--dry-run')
403
+ expect(completions).toContain('--env')
404
+ })
405
+ })
406
+
407
+ // ─── Snapshot-based completion scenarios ───
408
+ //
409
+ // These tests document exactly what completions are returned at every
410
+ // cursor position in two realistic CLI shapes. Each test title describes
411
+ // what the user typed before pressing Tab.
412
+
413
+ describe('completion snapshots: CLI with root default command', () => {
414
+ let originalShell: string | undefined
415
+ let originalCompletionShell: string | undefined
416
+
417
+ beforeEach(() => {
418
+ originalShell = process.env.SHELL
419
+ originalCompletionShell = process.env.GOKE_COMPLETION_SHELL
420
+ process.env.SHELL = '/bin/bash'
421
+ delete process.env.GOKE_COMPLETION_SHELL
422
+ })
423
+
424
+ afterEach(() => {
425
+ if (originalShell !== undefined) process.env.SHELL = originalShell
426
+ else delete process.env.SHELL
427
+ if (originalCompletionShell !== undefined) process.env.GOKE_COMPLETION_SHELL = originalCompletionShell
428
+ else delete process.env.GOKE_COMPLETION_SHELL
429
+ })
430
+
431
+ function buildRootCli() {
432
+ const cli = gokeTestable('app')
433
+ .help()
434
+ .completions()
435
+
436
+ // Root/default command with its own options
437
+ cli.command('', 'Run the app')
438
+ .option('--port <port>', z.number().default(3000).describe('Port number'))
439
+ .option('--host [host]', 'Hostname to bind')
440
+ .option('--verbose', 'Enable verbose logging')
441
+
442
+ // Named commands alongside the default
443
+ cli.command('init', 'Initialize a new project')
444
+ .option('--template <name>', 'Project template')
445
+ .option('--force', 'Overwrite existing files')
446
+
447
+ cli.command('config set <key> <value>', 'Set a config value')
448
+ cli.command('config get <key>', 'Get a config value')
449
+ cli.command('config list', 'List all config values')
450
+
451
+ cli.command('secret', 'Secret command').hidden()
452
+
453
+ return cli
454
+ }
455
+
456
+ test('app <TAB> — empty after CLI name', () => {
457
+ const cli = buildRootCli()
458
+ expect(cli.getCompletions(['app', ''])).toMatchInlineSnapshot(`
459
+ [
460
+ "init",
461
+ "completions",
462
+ "config",
463
+ ]
464
+ `)
465
+ })
466
+
467
+ test('app i<TAB> — partial command', () => {
468
+ const cli = buildRootCli()
469
+ expect(cli.getCompletions(['app', 'i'])).toMatchInlineSnapshot(`
470
+ [
471
+ "init",
472
+ ]
473
+ `)
474
+ })
475
+
476
+ test('app --<TAB> — flags at root level', () => {
477
+ const cli = buildRootCli()
478
+ expect(cli.getCompletions(['app', '--'])).toMatchInlineSnapshot(`
479
+ [
480
+ "--help",
481
+ "--port",
482
+ "--host",
483
+ "--verbose",
484
+ ]
485
+ `)
486
+ })
487
+
488
+ test('app --p<TAB> — partial flag', () => {
489
+ const cli = buildRootCli()
490
+ expect(cli.getCompletions(['app', '--p'])).toMatchInlineSnapshot(`
491
+ [
492
+ "--port",
493
+ ]
494
+ `)
495
+ })
496
+
497
+ test('app --port <TAB> — after value-taking option', () => {
498
+ const cli = buildRootCli()
499
+ expect(cli.getCompletions(['app', '--port', ''])).toMatchInlineSnapshot(`[]`)
500
+ })
501
+
502
+ test('app --verbose <TAB> — after boolean flag', () => {
503
+ const cli = buildRootCli()
504
+ expect(cli.getCompletions(['app', '--verbose', ''])).toMatchInlineSnapshot(`
505
+ [
506
+ "--help",
507
+ ]
508
+ `)
509
+ })
510
+
511
+ test('app --verbose --<TAB> — more flags after boolean', () => {
512
+ const cli = buildRootCli()
513
+ expect(cli.getCompletions(['app', '--verbose', '--'])).toMatchInlineSnapshot(`
514
+ [
515
+ "--help",
516
+ "--port",
517
+ "--host",
518
+ "--verbose",
519
+ ]
520
+ `)
521
+ })
522
+
523
+ test('app init <TAB> — after named command', () => {
524
+ const cli = buildRootCli()
525
+ expect(cli.getCompletions(['app', 'init', ''])).toMatchInlineSnapshot(`
526
+ [
527
+ "--help",
528
+ "--template",
529
+ "--force",
530
+ ]
531
+ `)
532
+ })
533
+
534
+ test('app init --<TAB> — flags for named command', () => {
535
+ const cli = buildRootCli()
536
+ expect(cli.getCompletions(['app', 'init', '--'])).toMatchInlineSnapshot(`
537
+ [
538
+ "--help",
539
+ "--template",
540
+ "--force",
541
+ ]
542
+ `)
543
+ })
544
+
545
+ test('app init --force --<TAB> — remaining flags after used boolean', () => {
546
+ const cli = buildRootCli()
547
+ expect(cli.getCompletions(['app', 'init', '--force', '--'])).toMatchInlineSnapshot(`
548
+ [
549
+ "--help",
550
+ "--template",
551
+ ]
552
+ `)
553
+ })
554
+
555
+ test('app init --template <TAB> — after value-taking flag', () => {
556
+ const cli = buildRootCli()
557
+ expect(cli.getCompletions(['app', 'init', '--template', ''])).toMatchInlineSnapshot(`[]`)
558
+ })
559
+
560
+ test('app config <TAB> — namespace with subcommands', () => {
561
+ const cli = buildRootCli()
562
+ expect(cli.getCompletions(['app', 'config', ''])).toMatchInlineSnapshot(`
563
+ [
564
+ "set",
565
+ "get",
566
+ "list",
567
+ ]
568
+ `)
569
+ })
570
+
571
+ test('app config s<TAB> — partial subcommand', () => {
572
+ const cli = buildRootCli()
573
+ expect(cli.getCompletions(['app', 'config', 's'])).toMatchInlineSnapshot(`
574
+ [
575
+ "set",
576
+ ]
577
+ `)
578
+ })
579
+
580
+ test('app config list --<TAB> — flags for nested subcommand', () => {
581
+ const cli = buildRootCli()
582
+ expect(cli.getCompletions(['app', 'config', 'list', '--'])).toMatchInlineSnapshot(`
583
+ [
584
+ "--help",
585
+ ]
586
+ `)
587
+ })
588
+
589
+ test('app x<TAB> — no matching command', () => {
590
+ const cli = buildRootCli()
591
+ expect(cli.getCompletions(['app', 'x'])).toMatchInlineSnapshot(`[]`)
592
+ })
593
+
594
+ test('hidden commands never appear', () => {
595
+ const cli = buildRootCli()
596
+ const all = cli.getCompletions(['app', ''])
597
+ expect(all).not.toContain('secret')
598
+ })
599
+ })
600
+
601
+ describe('completion snapshots: CLI with namespaced commands (no root)', () => {
602
+ let originalShell: string | undefined
603
+ let originalCompletionShell: string | undefined
604
+
605
+ beforeEach(() => {
606
+ originalShell = process.env.SHELL
607
+ originalCompletionShell = process.env.GOKE_COMPLETION_SHELL
608
+ process.env.SHELL = '/bin/bash'
609
+ delete process.env.GOKE_COMPLETION_SHELL
610
+ })
611
+
612
+ afterEach(() => {
613
+ if (originalShell !== undefined) process.env.SHELL = originalShell
614
+ else delete process.env.SHELL
615
+ if (originalCompletionShell !== undefined) process.env.GOKE_COMPLETION_SHELL = originalCompletionShell
616
+ else delete process.env.GOKE_COMPLETION_SHELL
617
+ })
618
+
619
+ function buildNamespacedCli() {
620
+ const cli = gokeTestable('kubectl')
621
+ .help()
622
+ .completions()
623
+ .option('--context <ctx>', 'Kubernetes context')
624
+ .option('-n, --namespace <ns>', 'Kubernetes namespace')
625
+
626
+ cli.command('get pods', 'List pods')
627
+ .option('-o, --output <format>', 'Output format')
628
+ .option('-l, --labels <selector>', 'Label selector')
629
+ .option('-A, --all-namespaces', 'All namespaces')
630
+
631
+ cli.command('get services', 'List services')
632
+ .option('-o, --output <format>', 'Output format')
633
+
634
+ cli.command('get nodes', 'List nodes')
635
+
636
+ cli.command('describe pod <name>', 'Describe a pod')
637
+ cli.command('describe service <name>', 'Describe a service')
638
+
639
+ cli.command('apply', 'Apply a configuration')
640
+ .option('-f, --file <path>', 'Config file path')
641
+ .option('--dry-run', 'Only print what would happen')
642
+
643
+ cli.command('delete pod <name>', 'Delete a pod')
644
+ .option('--force', 'Force delete')
645
+ .option('--grace-period <seconds>', z.number().describe('Grace period in seconds'))
646
+
647
+ cli.command('logs <pod>', 'View pod logs')
648
+ .option('-f, --follow', 'Follow log output')
649
+ .option('--tail <lines>', z.number().default(100).describe('Number of lines'))
650
+
651
+ return cli
652
+ }
653
+
654
+ test('kubectl <TAB> — top-level commands', () => {
655
+ const cli = buildNamespacedCli()
656
+ expect(cli.getCompletions(['kubectl', ''])).toMatchInlineSnapshot(`
657
+ [
658
+ "apply",
659
+ "logs",
660
+ "completions",
661
+ "get",
662
+ "describe",
663
+ "delete",
664
+ ]
665
+ `)
666
+ })
667
+
668
+ test('kubectl g<TAB> — partial match', () => {
669
+ const cli = buildNamespacedCli()
670
+ expect(cli.getCompletions(['kubectl', 'g'])).toMatchInlineSnapshot(`
671
+ [
672
+ "get",
673
+ ]
674
+ `)
675
+ })
676
+
677
+ test('kubectl --<TAB> — global options at root', () => {
678
+ const cli = buildNamespacedCli()
679
+ expect(cli.getCompletions(['kubectl', '--'])).toMatchInlineSnapshot(`
680
+ [
681
+ "--help",
682
+ "--context",
683
+ "--namespace",
684
+ ]
685
+ `)
686
+ })
687
+
688
+ test('kubectl --context <TAB> — after global value option', () => {
689
+ const cli = buildNamespacedCli()
690
+ expect(cli.getCompletions(['kubectl', '--context', ''])).toMatchInlineSnapshot(`[]`)
691
+ })
692
+
693
+ test('kubectl get <TAB> — subcommands under namespace', () => {
694
+ const cli = buildNamespacedCli()
695
+ expect(cli.getCompletions(['kubectl', 'get', ''])).toMatchInlineSnapshot(`
696
+ [
697
+ "pods",
698
+ "services",
699
+ "nodes",
700
+ ]
701
+ `)
702
+ })
703
+
704
+ test('kubectl get p<TAB> — partial subcommand', () => {
705
+ const cli = buildNamespacedCli()
706
+ expect(cli.getCompletions(['kubectl', 'get', 'p'])).toMatchInlineSnapshot(`
707
+ [
708
+ "pods",
709
+ ]
710
+ `)
711
+ })
712
+
713
+ test('kubectl get pods --<TAB> — options for nested command', () => {
714
+ const cli = buildNamespacedCli()
715
+ expect(cli.getCompletions(['kubectl', 'get', 'pods', '--'])).toMatchInlineSnapshot(`
716
+ [
717
+ "--help",
718
+ "--context",
719
+ "--namespace",
720
+ "--output",
721
+ "--labels",
722
+ "--all-namespaces",
723
+ ]
724
+ `)
725
+ })
726
+
727
+ test('kubectl get pods -A --<TAB> — remaining options after used flag', () => {
728
+ const cli = buildNamespacedCli()
729
+ expect(cli.getCompletions(['kubectl', 'get', 'pods', '-A', '--'])).toMatchInlineSnapshot(`
730
+ [
731
+ "--help",
732
+ "--context",
733
+ "--namespace",
734
+ "--output",
735
+ "--labels",
736
+ ]
737
+ `)
738
+ })
739
+
740
+ test('kubectl get pods --output <TAB> — after value option', () => {
741
+ const cli = buildNamespacedCli()
742
+ expect(cli.getCompletions(['kubectl', 'get', 'pods', '--output', ''])).toMatchInlineSnapshot(`[]`)
743
+ })
744
+
745
+ test('kubectl describe <TAB> — subcommands', () => {
746
+ const cli = buildNamespacedCli()
747
+ expect(cli.getCompletions(['kubectl', 'describe', ''])).toMatchInlineSnapshot(`
748
+ [
749
+ "pod",
750
+ "service",
751
+ ]
752
+ `)
753
+ })
754
+
755
+ test('kubectl apply --<TAB> — options for apply', () => {
756
+ const cli = buildNamespacedCli()
757
+ expect(cli.getCompletions(['kubectl', 'apply', '--'])).toMatchInlineSnapshot(`
758
+ [
759
+ "--help",
760
+ "--context",
761
+ "--namespace",
762
+ "--file",
763
+ "--dry-run",
764
+ ]
765
+ `)
766
+ })
767
+
768
+ test('kubectl apply --dry-run --<TAB> — after used boolean', () => {
769
+ const cli = buildNamespacedCli()
770
+ expect(cli.getCompletions(['kubectl', 'apply', '--dry-run', '--'])).toMatchInlineSnapshot(`
771
+ [
772
+ "--help",
773
+ "--context",
774
+ "--namespace",
775
+ "--file",
776
+ ]
777
+ `)
778
+ })
779
+
780
+ test('kubectl delete pod myapp --<TAB> — options after positional arg', () => {
781
+ const cli = buildNamespacedCli()
782
+ expect(cli.getCompletions(['kubectl', 'delete', 'pod', 'myapp', '--'])).toMatchInlineSnapshot(`
783
+ [
784
+ "--help",
785
+ "--context",
786
+ "--namespace",
787
+ "--force",
788
+ "--grace-period",
789
+ ]
790
+ `)
791
+ })
792
+
793
+ test('kubectl logs mypod --<TAB> — options for logs', () => {
794
+ const cli = buildNamespacedCli()
795
+ expect(cli.getCompletions(['kubectl', 'logs', 'mypod', '--'])).toMatchInlineSnapshot(`
796
+ [
797
+ "--help",
798
+ "--context",
799
+ "--namespace",
800
+ "--follow",
801
+ "--tail",
802
+ ]
803
+ `)
804
+ })
805
+
806
+ test('kubectl logs mypod -f --<TAB> — after short boolean alias', () => {
807
+ const cli = buildNamespacedCli()
808
+ expect(cli.getCompletions(['kubectl', 'logs', 'mypod', '-f', '--'])).toMatchInlineSnapshot(`
809
+ [
810
+ "--help",
811
+ "--context",
812
+ "--namespace",
813
+ "--tail",
814
+ ]
815
+ `)
816
+ })
817
+
818
+ test('kubectl logs mypod --tail <TAB> — after value option', () => {
819
+ const cli = buildNamespacedCli()
820
+ expect(cli.getCompletions(['kubectl', 'logs', 'mypod', '--tail', ''])).toMatchInlineSnapshot(`[]`)
821
+ })
822
+
823
+ test('kubectl nonexistent <TAB> — unknown command', () => {
824
+ const cli = buildNamespacedCli()
825
+ expect(cli.getCompletions(['kubectl', 'nonexistent', ''])).toMatchInlineSnapshot(`
826
+ [
827
+ "--help",
828
+ ]
829
+ `)
830
+ })
831
+ })
832
+
833
+ describe('completion snapshots: zsh format', () => {
834
+ let originalShell: string | undefined
835
+ let originalCompletionShell: string | undefined
836
+
837
+ beforeEach(() => {
838
+ originalShell = process.env.SHELL
839
+ originalCompletionShell = process.env.GOKE_COMPLETION_SHELL
840
+ delete process.env.SHELL
841
+ process.env.GOKE_COMPLETION_SHELL = 'zsh'
842
+ })
843
+
844
+ afterEach(() => {
845
+ if (originalShell !== undefined) process.env.SHELL = originalShell
846
+ else delete process.env.SHELL
847
+ if (originalCompletionShell !== undefined) process.env.GOKE_COMPLETION_SHELL = originalCompletionShell
848
+ else delete process.env.GOKE_COMPLETION_SHELL
849
+ })
850
+
851
+ function buildZshCli() {
852
+ const cli = gokeTestable('todo')
853
+ .help()
854
+ .completions()
855
+
856
+ cli.command('add <title>', 'Add a new todo item')
857
+ .option('--priority <level>', 'Priority level')
858
+ .option('--due <date>', 'Due date')
859
+
860
+ cli.command('list', 'List all todos')
861
+ .option('--done', 'Show only completed')
862
+ .option('--pending', 'Show only pending')
863
+
864
+ cli.command('done <id>', 'Mark a todo as done')
865
+
866
+ return cli
867
+ }
868
+
869
+ test('todo <TAB> — commands with descriptions', () => {
870
+ const cli = buildZshCli()
871
+ expect(cli.getCompletions(['todo', ''])).toMatchInlineSnapshot(`
872
+ [
873
+ "add:Add a new todo item",
874
+ "list:List all todos",
875
+ "done:Mark a todo as done",
876
+ "completions",
877
+ ]
878
+ `)
879
+ })
880
+
881
+ test('todo add myitem --<TAB> — options with descriptions', () => {
882
+ const cli = buildZshCli()
883
+ expect(cli.getCompletions(['todo', 'add', 'myitem', '--'])).toMatchInlineSnapshot(`
884
+ [
885
+ "--help:Display this message",
886
+ "--priority:Priority level",
887
+ "--due:Due date",
888
+ ]
889
+ `)
890
+ })
891
+
892
+ test('todo list --<TAB> — list options with descriptions', () => {
893
+ const cli = buildZshCli()
894
+ expect(cli.getCompletions(['todo', 'list', '--'])).toMatchInlineSnapshot(`
895
+ [
896
+ "--help:Display this message",
897
+ "--done:Show only completed",
898
+ "--pending:Show only pending",
899
+ ]
900
+ `)
901
+ })
902
+ })