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