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
|
@@ -132,6 +132,35 @@ describe('error formatting', () => {
|
|
|
132
132
|
expect(text).toMatch(/at /);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
describe('anonymous action naming', () => {
|
|
136
|
+
test('inline anonymous function gets named after the command', () => {
|
|
137
|
+
const cli = gokeTestable('mycli');
|
|
138
|
+
const cmd = cli.command('deploy', 'Deploy app');
|
|
139
|
+
// Inline arrow functions passed directly to .action() have no name,
|
|
140
|
+
// so goke assigns one based on the command name for better stack traces.
|
|
141
|
+
cmd.action(() => { });
|
|
142
|
+
expect(cmd.commandAction.name).toBe('command:deploy');
|
|
143
|
+
});
|
|
144
|
+
test('inline anonymous function on multi-word command gets full name', () => {
|
|
145
|
+
const cli = gokeTestable('mycli');
|
|
146
|
+
const cmd = cli.command('db migrate', 'Run migrations');
|
|
147
|
+
cmd.action(() => { });
|
|
148
|
+
expect(cmd.commandAction.name).toBe('command:db migrate');
|
|
149
|
+
});
|
|
150
|
+
test('named function keeps its original name', () => {
|
|
151
|
+
const cli = gokeTestable('mycli');
|
|
152
|
+
const cmd = cli.command('build', 'Build app');
|
|
153
|
+
function myBuildAction() { }
|
|
154
|
+
cmd.action(myBuildAction);
|
|
155
|
+
expect(cmd.commandAction.name).toBe('myBuildAction');
|
|
156
|
+
});
|
|
157
|
+
test('default command action gets "command:default" name', () => {
|
|
158
|
+
const cli = gokeTestable('mycli');
|
|
159
|
+
const cmd = cli.command('', 'Default command');
|
|
160
|
+
cmd.action(() => { });
|
|
161
|
+
expect(cmd.commandAction.name).toBe('command:default');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
135
164
|
describe('injected fs', () => {
|
|
136
165
|
test('command actions can use the default node fs for cli storage', async () => {
|
|
137
166
|
const stdout = createTestOutputStream();
|
|
@@ -1831,3 +1860,162 @@ describe('middleware', () => {
|
|
|
1831
1860
|
expect(order).toEqual(['sync1', 'async', 'sync2', 'action']);
|
|
1832
1861
|
});
|
|
1833
1862
|
});
|
|
1863
|
+
describe('use() with sub-CLI composition', () => {
|
|
1864
|
+
test('basic composition: sub-CLI command runs via parent', () => {
|
|
1865
|
+
const parent = goke('mycli');
|
|
1866
|
+
const sub = goke();
|
|
1867
|
+
let matched = '';
|
|
1868
|
+
sub
|
|
1869
|
+
.command('deploy', 'Deploy the app')
|
|
1870
|
+
.action(() => { matched = 'deploy'; });
|
|
1871
|
+
parent.use(sub);
|
|
1872
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1873
|
+
expect(matched).toBe('deploy');
|
|
1874
|
+
});
|
|
1875
|
+
test('multiple sub-CLIs composed together', () => {
|
|
1876
|
+
const parent = goke('mycli');
|
|
1877
|
+
const subA = goke();
|
|
1878
|
+
const subB = goke();
|
|
1879
|
+
let matched = '';
|
|
1880
|
+
subA.command('login', 'Login').action(() => { matched = 'login'; });
|
|
1881
|
+
subB.command('deploy', 'Deploy').action(() => { matched = 'deploy'; });
|
|
1882
|
+
parent.use(subA).use(subB);
|
|
1883
|
+
parent.parse(['node', 'bin', 'login'], { run: true });
|
|
1884
|
+
expect(matched).toBe('login');
|
|
1885
|
+
matched = '';
|
|
1886
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1887
|
+
expect(matched).toBe('deploy');
|
|
1888
|
+
});
|
|
1889
|
+
test('sub-CLI command with options and schema coercion', () => {
|
|
1890
|
+
const parent = goke('mycli');
|
|
1891
|
+
const sub = goke();
|
|
1892
|
+
let result = {};
|
|
1893
|
+
sub
|
|
1894
|
+
.command('serve', 'Start server')
|
|
1895
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
1896
|
+
.option('--host <host>', z.string().describe('Host'))
|
|
1897
|
+
.action((options) => { result = options; });
|
|
1898
|
+
parent.use(sub);
|
|
1899
|
+
parent.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true });
|
|
1900
|
+
expect(result.port).toBe(3000);
|
|
1901
|
+
expect(typeof result.port).toBe('number');
|
|
1902
|
+
expect(result.host).toBe('localhost');
|
|
1903
|
+
});
|
|
1904
|
+
test('sub-CLI command with positional args', () => {
|
|
1905
|
+
const parent = goke('mycli');
|
|
1906
|
+
const sub = goke();
|
|
1907
|
+
let receivedId = '';
|
|
1908
|
+
sub
|
|
1909
|
+
.command('get <id>', 'Get a resource')
|
|
1910
|
+
.action((id) => { receivedId = id; });
|
|
1911
|
+
parent.use(sub);
|
|
1912
|
+
parent.parse(['node', 'bin', 'get', 'abc123'], { run: true });
|
|
1913
|
+
expect(receivedId).toBe('abc123');
|
|
1914
|
+
});
|
|
1915
|
+
test('sub-CLI with multi-word commands', () => {
|
|
1916
|
+
const parent = goke('mycli');
|
|
1917
|
+
const sub = goke();
|
|
1918
|
+
let matched = '';
|
|
1919
|
+
sub.command('mcp login', 'Login to MCP').action(() => { matched = 'mcp login'; });
|
|
1920
|
+
sub.command('mcp logout', 'Logout from MCP').action(() => { matched = 'mcp logout'; });
|
|
1921
|
+
parent.use(sub);
|
|
1922
|
+
parent.parse(['node', 'bin', 'mcp', 'login'], { run: true });
|
|
1923
|
+
expect(matched).toBe('mcp login');
|
|
1924
|
+
matched = '';
|
|
1925
|
+
parent.parse(['node', 'bin', 'mcp', 'logout'], { run: true });
|
|
1926
|
+
expect(matched).toBe('mcp logout');
|
|
1927
|
+
});
|
|
1928
|
+
test('help output includes composed commands', () => {
|
|
1929
|
+
const stdout = createTestOutputStream();
|
|
1930
|
+
const parent = goke('mycli', { stdout });
|
|
1931
|
+
const sub = goke();
|
|
1932
|
+
sub.command('selfhost', 'Set up on your own workspace')
|
|
1933
|
+
.option('-t, --token [token]', 'Admin token');
|
|
1934
|
+
parent.command('init', 'Initialize project');
|
|
1935
|
+
parent.use(sub);
|
|
1936
|
+
parent.help();
|
|
1937
|
+
parent.parse(['node', 'bin', '--help'], { run: false });
|
|
1938
|
+
expect(stdout.text).toContain('init');
|
|
1939
|
+
expect(stdout.text).toContain('selfhost');
|
|
1940
|
+
expect(stdout.text).toContain('Set up on your own workspace');
|
|
1941
|
+
});
|
|
1942
|
+
test('sub-CLI middlewares are NOT copied to parent', () => {
|
|
1943
|
+
const parent = goke('mycli');
|
|
1944
|
+
const sub = goke();
|
|
1945
|
+
let subMiddlewareCalled = false;
|
|
1946
|
+
const order = [];
|
|
1947
|
+
sub.use(() => { subMiddlewareCalled = true; });
|
|
1948
|
+
sub.command('deploy', 'Deploy').action(() => { order.push('deploy'); });
|
|
1949
|
+
parent.use(() => { order.push('parent-mw'); });
|
|
1950
|
+
parent.use(sub);
|
|
1951
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1952
|
+
expect(subMiddlewareCalled).toBe(false);
|
|
1953
|
+
expect(order).toEqual(['parent-mw', 'deploy']);
|
|
1954
|
+
});
|
|
1955
|
+
test('parent global options are available to composed commands', () => {
|
|
1956
|
+
const parent = goke('mycli');
|
|
1957
|
+
const sub = goke();
|
|
1958
|
+
let result = {};
|
|
1959
|
+
parent.option('--verbose', 'Verbose output');
|
|
1960
|
+
sub
|
|
1961
|
+
.command('build', 'Build')
|
|
1962
|
+
.option('--target <target>', 'Build target')
|
|
1963
|
+
.action((options) => { result = options; });
|
|
1964
|
+
parent.use(sub);
|
|
1965
|
+
parent.parse('node bin build --verbose --target production'.split(' '), { run: true });
|
|
1966
|
+
expect(result.verbose).toBe(true);
|
|
1967
|
+
expect(result.target).toBe('production');
|
|
1968
|
+
});
|
|
1969
|
+
test('composed commands coexist with inline commands', () => {
|
|
1970
|
+
const parent = goke('mycli');
|
|
1971
|
+
const sub = goke();
|
|
1972
|
+
let matched = '';
|
|
1973
|
+
parent.command('init', 'Initialize').action(() => { matched = 'init'; });
|
|
1974
|
+
sub.command('deploy', 'Deploy').action(() => { matched = 'deploy'; });
|
|
1975
|
+
sub.command('rollback', 'Rollback').action(() => { matched = 'rollback'; });
|
|
1976
|
+
parent.use(sub);
|
|
1977
|
+
parent.parse(['node', 'bin', 'init'], { run: true });
|
|
1978
|
+
expect(matched).toBe('init');
|
|
1979
|
+
matched = '';
|
|
1980
|
+
parent.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1981
|
+
expect(matched).toBe('deploy');
|
|
1982
|
+
matched = '';
|
|
1983
|
+
parent.parse(['node', 'bin', 'rollback'], { run: true });
|
|
1984
|
+
expect(matched).toBe('rollback');
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
describe('getAction()', () => {
|
|
1988
|
+
test('returns the action callable with correct behavior', () => {
|
|
1989
|
+
const stdout = createTestOutputStream();
|
|
1990
|
+
const cli = goke('mycli', { stdout, exit: () => { } });
|
|
1991
|
+
const cmd = cli
|
|
1992
|
+
.command('deploy', 'Deploy the app')
|
|
1993
|
+
.option('--env <env>', z.enum(['staging', 'production']).describe('Target environment'))
|
|
1994
|
+
.action((options, { console }) => {
|
|
1995
|
+
console.log(`Deploying to ${options.env}`);
|
|
1996
|
+
});
|
|
1997
|
+
const action = cmd.getAction();
|
|
1998
|
+
const ctx = cli.createExecutionContext();
|
|
1999
|
+
action({ env: 'staging', '--': [] }, ctx);
|
|
2000
|
+
expect(stdout.text).toBe('Deploying to staging\n');
|
|
2001
|
+
});
|
|
2002
|
+
test('works with positional args', () => {
|
|
2003
|
+
const stdout = createTestOutputStream();
|
|
2004
|
+
const cli = goke('mycli', { stdout, exit: () => { } });
|
|
2005
|
+
const cmd = cli
|
|
2006
|
+
.command('get <id>', 'Fetch by id')
|
|
2007
|
+
.option('--format <format>', z.string().describe('Output format'))
|
|
2008
|
+
.action((id, options, { console }) => {
|
|
2009
|
+
console.log(`${id}:${options.format}`);
|
|
2010
|
+
});
|
|
2011
|
+
const action = cmd.getAction();
|
|
2012
|
+
const ctx = cli.createExecutionContext();
|
|
2013
|
+
action('abc123', { format: 'json', '--': [] }, ctx);
|
|
2014
|
+
expect(stdout.text).toBe('abc123:json\n');
|
|
2015
|
+
});
|
|
2016
|
+
test('throws when no action is registered', () => {
|
|
2017
|
+
const cli = goke('mycli');
|
|
2018
|
+
const cmd = cli.command('noop', 'No action');
|
|
2019
|
+
expect(() => cmd.getAction()).toThrow(/No action registered/);
|
|
2020
|
+
});
|
|
2021
|
+
});
|
|
@@ -312,4 +312,23 @@ describe('createJustBashCommand', () => {
|
|
|
312
312
|
exitCode: 7,
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
|
+
test('truncates captured output to just-bash maxOutputSize and appends a notice', async () => {
|
|
316
|
+
const { Bash } = await import('just-bash');
|
|
317
|
+
const cli = gokeTestable('parent');
|
|
318
|
+
cli
|
|
319
|
+
.command('spam-stderr', 'Write too much stderr')
|
|
320
|
+
.action((options, { console }) => {
|
|
321
|
+
console.error('x'.repeat(60));
|
|
322
|
+
console.error('y'.repeat(60));
|
|
323
|
+
});
|
|
324
|
+
const bash = new Bash({
|
|
325
|
+
executionLimits: { maxOutputSize: 80 },
|
|
326
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
327
|
+
});
|
|
328
|
+
const result = await bash.exec('parent spam-stderr');
|
|
329
|
+
expect(result.exitCode).toBe(0);
|
|
330
|
+
expect(result.stdout).toBe('');
|
|
331
|
+
expect(result.stderr.length).toBeLessThanOrEqual(80);
|
|
332
|
+
expect(result.stderr).toBe(`${'x'.repeat(60)}\n[output truncated]\n`);
|
|
333
|
+
});
|
|
315
334
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { describe, expect, test } from 'vitest';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import goke, { openInBrowser } from '../index.js';
|
|
6
|
+
import goke, { openInBrowser, generateDocs } from '../index.js';
|
|
7
7
|
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
8
8
|
const stripAnsi = (text) => text.replace(ANSI_RE, '');
|
|
9
9
|
function createTestOutputStream() {
|
|
@@ -133,7 +133,7 @@ describe('documented command APIs', () => {
|
|
|
133
133
|
expect(help).toContain('Deploy safely first');
|
|
134
134
|
expect(stdout.text).toBe('');
|
|
135
135
|
});
|
|
136
|
-
test('openInBrowser prints the URL to stdout in non-tty environments', () => {
|
|
136
|
+
test('openInBrowser prints the URL to stdout in non-tty environments', async () => {
|
|
137
137
|
const url = 'https://example.com/dashboard';
|
|
138
138
|
const originalStdoutWrite = process.stdout.write;
|
|
139
139
|
const originalStderrWrite = process.stderr.write;
|
|
@@ -153,7 +153,7 @@ describe('documented command APIs', () => {
|
|
|
153
153
|
return true;
|
|
154
154
|
});
|
|
155
155
|
try {
|
|
156
|
-
openInBrowser(url);
|
|
156
|
+
await openInBrowser(url);
|
|
157
157
|
}
|
|
158
158
|
finally {
|
|
159
159
|
process.stdout.write = originalStdoutWrite;
|
|
@@ -163,7 +163,143 @@ describe('documented command APIs', () => {
|
|
|
163
163
|
value: originalIsTTY,
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
|
-
expect(stdout).toBe(
|
|
167
|
-
expect(stderr).toBe(
|
|
166
|
+
expect(stdout).toBe('');
|
|
167
|
+
expect(stderr).toBe(`${url}\n`);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe('generateDocs', () => {
|
|
171
|
+
test('generates pages for CLI with multiple commands', () => {
|
|
172
|
+
const cli = gokeTestable('sentry')
|
|
173
|
+
.version('1.0.0')
|
|
174
|
+
.help();
|
|
175
|
+
cli
|
|
176
|
+
.command('event view <id>', 'View details of a specific event')
|
|
177
|
+
.option('-w, --web', 'Open in browser')
|
|
178
|
+
.option('--spans <spans>', z.string().default('3').describe('Span tree depth limit'))
|
|
179
|
+
.example('```\nsentry event view abc123\n```');
|
|
180
|
+
cli
|
|
181
|
+
.command('event list <issue>', 'List events for an issue')
|
|
182
|
+
.option('-n, --limit <limit>', z.number().default(25).describe('Number of events'))
|
|
183
|
+
.option('-q, --query <query>', 'Search query');
|
|
184
|
+
cli
|
|
185
|
+
.command('hidden-cmd', 'Should not appear')
|
|
186
|
+
.hidden();
|
|
187
|
+
const pages = generateDocs({ cli });
|
|
188
|
+
expect(pages.map((p) => p.slug)).toMatchInlineSnapshot(`
|
|
189
|
+
[
|
|
190
|
+
"index",
|
|
191
|
+
"event-view",
|
|
192
|
+
"event-list",
|
|
193
|
+
]
|
|
194
|
+
`);
|
|
195
|
+
// Index page
|
|
196
|
+
expect(pages[0].content).toMatchInlineSnapshot(`
|
|
197
|
+
"# sentry
|
|
198
|
+
|
|
199
|
+
Version: 1.0.0
|
|
200
|
+
|
|
201
|
+
## Commands
|
|
202
|
+
|
|
203
|
+
| Command | Description |
|
|
204
|
+
|---------|-------------|
|
|
205
|
+
| [\`event view\`](./event-view.md) | View details of a specific event |
|
|
206
|
+
| [\`event list\`](./event-list.md) | List events for an issue |
|
|
207
|
+
|
|
208
|
+
## Global Options
|
|
209
|
+
|
|
210
|
+
| Option | Default | Description |
|
|
211
|
+
|--------|---------|-------------|
|
|
212
|
+
| \`-v, --version\` | - | Display version number |
|
|
213
|
+
| \`-h, --help\` | - | Display this message |
|
|
214
|
+
"
|
|
215
|
+
`);
|
|
216
|
+
// Command page with examples
|
|
217
|
+
expect(pages[1].content).toMatchInlineSnapshot(`
|
|
218
|
+
"# event view
|
|
219
|
+
|
|
220
|
+
View details of a specific event
|
|
221
|
+
|
|
222
|
+
## Usage
|
|
223
|
+
|
|
224
|
+
\`\`\`sh
|
|
225
|
+
sentry event view <id>
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
## Arguments
|
|
229
|
+
|
|
230
|
+
| Argument | Required | Description |
|
|
231
|
+
|----------|----------|-------------|
|
|
232
|
+
| \`<id>\` | Yes | id |
|
|
233
|
+
|
|
234
|
+
## Options
|
|
235
|
+
|
|
236
|
+
| Option | Default | Description |
|
|
237
|
+
|--------|---------|-------------|
|
|
238
|
+
| \`-w, --web\` | - | Open in browser |
|
|
239
|
+
| \`--spans <spans>\` | \`3\` | Span tree depth limit |
|
|
240
|
+
|
|
241
|
+
## Global Options
|
|
242
|
+
|
|
243
|
+
| Option | Default | Description |
|
|
244
|
+
|--------|---------|-------------|
|
|
245
|
+
| \`-v, --version\` | - | Display version number |
|
|
246
|
+
| \`-h, --help\` | - | Display this message |
|
|
247
|
+
|
|
248
|
+
## Examples
|
|
249
|
+
|
|
250
|
+
\`\`\`
|
|
251
|
+
sentry event view abc123
|
|
252
|
+
\`\`\`
|
|
253
|
+
"
|
|
254
|
+
`);
|
|
255
|
+
// Command page without examples
|
|
256
|
+
expect(pages[2].content).toMatchInlineSnapshot(`
|
|
257
|
+
"# event list
|
|
258
|
+
|
|
259
|
+
List events for an issue
|
|
260
|
+
|
|
261
|
+
## Usage
|
|
262
|
+
|
|
263
|
+
\`\`\`sh
|
|
264
|
+
sentry event list <issue>
|
|
265
|
+
\`\`\`
|
|
266
|
+
|
|
267
|
+
## Arguments
|
|
268
|
+
|
|
269
|
+
| Argument | Required | Description |
|
|
270
|
+
|----------|----------|-------------|
|
|
271
|
+
| \`<issue>\` | Yes | issue |
|
|
272
|
+
|
|
273
|
+
## Options
|
|
274
|
+
|
|
275
|
+
| Option | Default | Description |
|
|
276
|
+
|--------|---------|-------------|
|
|
277
|
+
| \`-n, --limit <limit>\` | \`25\` | Number of events |
|
|
278
|
+
| \`-q, --query <query>\` | - | Search query |
|
|
279
|
+
|
|
280
|
+
## Global Options
|
|
281
|
+
|
|
282
|
+
| Option | Default | Description |
|
|
283
|
+
|--------|---------|-------------|
|
|
284
|
+
| \`-v, --version\` | - | Display version number |
|
|
285
|
+
| \`-h, --help\` | - | Display this message |
|
|
286
|
+
"
|
|
287
|
+
`);
|
|
288
|
+
});
|
|
289
|
+
test('handles CLI with no commands', () => {
|
|
290
|
+
const cli = gokeTestable('empty');
|
|
291
|
+
const pages = generateDocs({ cli });
|
|
292
|
+
expect(pages).toEqual([]);
|
|
293
|
+
});
|
|
294
|
+
test('skips deprecated options', () => {
|
|
295
|
+
const cli = gokeTestable('mycli');
|
|
296
|
+
cli
|
|
297
|
+
.command('deploy', 'Deploy the app')
|
|
298
|
+
.option('--force', 'Skip confirmation')
|
|
299
|
+
.option('--old-flag', z.boolean().meta({ deprecated: true }).describe('Use --force instead'));
|
|
300
|
+
const pages = generateDocs({ cli });
|
|
301
|
+
const deployPage = pages.find((p) => p.slug === 'deploy');
|
|
302
|
+
// The deprecated option should not appear
|
|
303
|
+
expect(deployPage.content).not.toContain('old-flag');
|
|
168
304
|
});
|
|
169
305
|
});
|
|
@@ -397,6 +397,70 @@ describe('type-level: README TypeScript examples', () => {
|
|
|
397
397
|
expectTypeOf(process.cwd).toEqualTypeOf();
|
|
398
398
|
});
|
|
399
399
|
});
|
|
400
|
+
test('use() with sub-CLI preserves parent middleware typing', () => {
|
|
401
|
+
const sub = goke();
|
|
402
|
+
sub
|
|
403
|
+
.command('deploy', 'Deploy the app')
|
|
404
|
+
.option('--force', z.boolean())
|
|
405
|
+
.action((options) => {
|
|
406
|
+
// Sub-CLI command's action sees its own options
|
|
407
|
+
expectTypeOf(options.force).toEqualTypeOf();
|
|
408
|
+
});
|
|
409
|
+
goke('test')
|
|
410
|
+
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
411
|
+
.use(sub)
|
|
412
|
+
.use((options) => {
|
|
413
|
+
// Parent middleware still sees parent's accumulated options after .use(subCli)
|
|
414
|
+
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
415
|
+
})
|
|
416
|
+
.command('build', 'Build')
|
|
417
|
+
.option('--target <target>', z.string())
|
|
418
|
+
.action((options) => {
|
|
419
|
+
// Parent's inline command still sees global options
|
|
420
|
+
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
421
|
+
expectTypeOf(options.target).toEqualTypeOf();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
test('use() with sub-CLI does not leak sub-CLI types to parent', () => {
|
|
425
|
+
const sub = goke()
|
|
426
|
+
.option('--sub-only <val>', z.string());
|
|
427
|
+
sub.command('sub-cmd', 'Sub command').action(() => { });
|
|
428
|
+
goke('test')
|
|
429
|
+
.option('--parent-only <val>', z.number())
|
|
430
|
+
.use(sub)
|
|
431
|
+
.use((options) => {
|
|
432
|
+
expectTypeOf(options.parentOnly).toEqualTypeOf();
|
|
433
|
+
// @ts-expect-error subOnly is not declared on the parent
|
|
434
|
+
options.subOnly;
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
test('getAction() returns correctly typed function', () => {
|
|
438
|
+
const cmd = goke('test')
|
|
439
|
+
.command('convert <input> <output>', 'Convert file format')
|
|
440
|
+
.option('--quality <quality>', z.number())
|
|
441
|
+
.option('--format <format>', z.enum(['png', 'jpg']))
|
|
442
|
+
.action((input, output, options, ctx) => {
|
|
443
|
+
void input;
|
|
444
|
+
void output;
|
|
445
|
+
void options;
|
|
446
|
+
void ctx;
|
|
447
|
+
});
|
|
448
|
+
const action = cmd.getAction();
|
|
449
|
+
expectTypeOf(action).parameter(0).toEqualTypeOf(); // input
|
|
450
|
+
expectTypeOf(action).parameter(1).toEqualTypeOf(); // output
|
|
451
|
+
});
|
|
452
|
+
test('getAction() with no positional args has (options, ctx) signature', () => {
|
|
453
|
+
const cmd = goke('test')
|
|
454
|
+
.command('deploy', 'Deploy')
|
|
455
|
+
.option('--env <env>', z.enum(['staging', 'production']))
|
|
456
|
+
.action((options, ctx) => {
|
|
457
|
+
void options;
|
|
458
|
+
void ctx;
|
|
459
|
+
});
|
|
460
|
+
const action = cmd.getAction();
|
|
461
|
+
// First param is options with env
|
|
462
|
+
expectTypeOf(action).parameter(0).toMatchTypeOf();
|
|
463
|
+
});
|
|
400
464
|
test('README global options and middleware example stays typed end-to-end', () => {
|
|
401
465
|
// `z.boolean().default(false)` and `z.string().default(...)` are
|
|
402
466
|
// effectively required at runtime: the default applies when the flag is
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
* Known AI coding agent names.
|
|
13
|
+
*/
|
|
14
|
+
export type AgentName = (string & {}) | 'cursor' | 'claude' | 'devin' | 'replit' | 'gemini' | 'codex' | 'auggie' | 'opencode' | 'kiro' | 'goose' | 'pi';
|
|
15
|
+
/**
|
|
16
|
+
* Information about the detected AI coding agent.
|
|
17
|
+
*/
|
|
18
|
+
export type AgentInfo = {
|
|
19
|
+
/** The name of the detected agent, or undefined if no agent was detected. */
|
|
20
|
+
name?: AgentName;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Detect the current AI coding agent from environment variables.
|
|
24
|
+
*
|
|
25
|
+
* Checks `AI_AGENT` env var first (explicit override), then scans for
|
|
26
|
+
* known agent-specific env vars in priority order.
|
|
27
|
+
*
|
|
28
|
+
* Supported agents: `cursor`, `claude`, `devin`, `replit`, `gemini`,
|
|
29
|
+
* `codex`, `auggie`, `opencode`, `kiro`, `goose`, `pi`
|
|
30
|
+
*/
|
|
31
|
+
export declare function detectAgent(): AgentInfo;
|
|
32
|
+
/** Detected agent info, evaluated once at import time. */
|
|
33
|
+
export declare const agentInfo: AgentInfo;
|
|
34
|
+
/** Name of the detected agent, or undefined if not running inside one. */
|
|
35
|
+
export declare const agent: AgentName | undefined;
|
|
36
|
+
/** Whether the current process is running inside an AI coding agent. */
|
|
37
|
+
export declare const isAgent: boolean;
|
|
38
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH;;GAEG;AACH,MAAM,MAAM,SAAS,GACjB,CAAC,MAAM,GAAG,EAAE,CAAC,GACb,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,UAAU,GACV,MAAM,GACN,OAAO,GACP,IAAI,CAAA;AAgCR;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,IAAI,SAAS,CAavC;AAED,0DAA0D;AAC1D,eAAO,MAAM,SAAS,EAAE,SAAyC,CAAA;AAEjE,0EAA0E;AAC1E,eAAO,MAAM,KAAK,EAAE,SAAS,GAAG,SAA0B,CAAA;AAE1D,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,OAA0B,CAAA"}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
const env = globalThis.process?.env || Object.create(null);
|
|
12
|
+
function envMatcher(envKey, regex) {
|
|
13
|
+
return () => {
|
|
14
|
+
const value = env[envKey];
|
|
15
|
+
return value ? regex.test(value) : false;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Detection order matters: specific agents first, IDE-based agents last
|
|
19
|
+
// so that agents running inside those IDEs are detected by their own env vars first.
|
|
20
|
+
const agents = [
|
|
21
|
+
// CLI agents
|
|
22
|
+
['claude', ['CLAUDECODE', 'CLAUDE_CODE']],
|
|
23
|
+
['replit', ['REPL_ID']],
|
|
24
|
+
['gemini', ['GEMINI_CLI']],
|
|
25
|
+
['codex', ['CODEX_SANDBOX', 'CODEX_THREAD_ID']],
|
|
26
|
+
['opencode', ['OPENCODE']],
|
|
27
|
+
['pi', [envMatcher('PATH', /\.pi[\\/]agent/)]],
|
|
28
|
+
['auggie', ['AUGMENT_AGENT']],
|
|
29
|
+
['goose', ['GOOSE_PROVIDER']],
|
|
30
|
+
// IDE-based agents (checked last)
|
|
31
|
+
['devin', [envMatcher('EDITOR', /devin/)]],
|
|
32
|
+
['cursor', ['CURSOR_AGENT']],
|
|
33
|
+
['kiro', [envMatcher('TERM_PROGRAM', /kiro/)]],
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Detect the current AI coding agent from environment variables.
|
|
37
|
+
*
|
|
38
|
+
* Checks `AI_AGENT` env var first (explicit override), then scans for
|
|
39
|
+
* known agent-specific env vars in priority order.
|
|
40
|
+
*
|
|
41
|
+
* Supported agents: `cursor`, `claude`, `devin`, `replit`, `gemini`,
|
|
42
|
+
* `codex`, `auggie`, `opencode`, `kiro`, `goose`, `pi`
|
|
43
|
+
*/
|
|
44
|
+
export function detectAgent() {
|
|
45
|
+
const aiAgent = env.AI_AGENT;
|
|
46
|
+
if (aiAgent) {
|
|
47
|
+
return { name: aiAgent.toLowerCase() };
|
|
48
|
+
}
|
|
49
|
+
for (const [name, checks] of agents) {
|
|
50
|
+
for (const check of checks) {
|
|
51
|
+
if (typeof check === 'string' ? env[check] : check()) {
|
|
52
|
+
return { name };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
/** Detected agent info, evaluated once at import time. */
|
|
59
|
+
export const agentInfo = /* #__PURE__ */ detectAgent();
|
|
60
|
+
/** Name of the detected agent, or undefined if not running inside one. */
|
|
61
|
+
export const agent = agentInfo.name;
|
|
62
|
+
/** Whether the current process is running inside an AI coding agent. */
|
|
63
|
+
export const isAgent = !!agentInfo.name;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion support for goke CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Two pieces work together:
|
|
5
|
+
* 1. A hidden `--get-goke-completions` flag in the CLI binary. When present,
|
|
6
|
+
* the CLI skips normal execution and prints matching completions to stdout.
|
|
7
|
+
* 2. A shell-specific shim script installed into an fpath/completion directory.
|
|
8
|
+
* On each Tab press the shell calls the CLI binary with the flag + current words.
|
|
9
|
+
*
|
|
10
|
+
* The `installCompletions` function finds a writable completion directory and
|
|
11
|
+
* writes the shim. Since the shim calls the binary on every Tab, completions
|
|
12
|
+
* are always up-to-date with the installed CLI.
|
|
13
|
+
*/
|
|
14
|
+
/** The hidden flag the shell script passes to the CLI on each Tab press */
|
|
15
|
+
export declare const COMPLETION_FLAG = "get-goke-completions";
|
|
16
|
+
/**
|
|
17
|
+
* Zsh completion script template.
|
|
18
|
+
*
|
|
19
|
+
* The `#compdef` header lets zsh autoload this as an fpath function.
|
|
20
|
+
* On Tab press it calls the CLI binary with `--get-goke-completions` and
|
|
21
|
+
* all typed words. The binary returns `name:description` pairs (one per line)
|
|
22
|
+
* and zsh renders them with `_describe`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const zshTemplate = "#compdef {{app_name}}\n###-begin-{{app_name}}-completions-###\n_{{app_name_safe}}_completions() {\n local reply\n local si=$IFS\n IFS=$'\\n' reply=($(COMP_CWORD=\"$((CURRENT-1))\" COMP_LINE=\"$BUFFER\" COMP_POINT=\"$CURSOR\" GOKE_COMPLETION_SHELL=zsh {{app_path}} --get-goke-completions \"${words[@]}\"))\n IFS=$si\n if [[ ${#reply} -gt 0 ]]; then\n _describe 'values' reply\n else\n _default\n fi\n}\nif [[ \"'${zsh_eval_context[-1]}\" == \"loadautofunc\" ]]; then\n _{{app_name_safe}}_completions \"$@\"\nelse\n compdef _{{app_name_safe}}_completions {{app_name}}\nfi\n###-end-{{app_name}}-completions-###\n";
|
|
25
|
+
/**
|
|
26
|
+
* Bash completion script template.
|
|
27
|
+
*
|
|
28
|
+
* On Tab press bash calls the function which invokes the CLI binary with
|
|
29
|
+
* `--get-goke-completions` and all typed words. The binary returns plain
|
|
30
|
+
* completion strings (one per line).
|
|
31
|
+
*/
|
|
32
|
+
export declare const bashTemplate = "###-begin-{{app_name}}-completions-###\n_{{app_name_safe}}_completions()\n{\n local cur_word args type_list\n\n cur_word=\"${COMP_WORDS[COMP_CWORD]}\"\n args=(\"${COMP_WORDS[@]}\")\n\n # Bash 3 compatible (no mapfile). Works on macOS default bash.\n local IFS=$'\\n'\n type_list=($(GOKE_COMPLETION_SHELL=bash {{app_path}} --get-goke-completions \"${args[@]}\"))\n unset IFS\n COMPREPLY=($(compgen -W \"$( printf '%q ' \"${type_list[@]}\" )\" -- \"${cur_word}\" |\n awk '/ / { print \"\\\"\"$0\"\\\"\" } /^[^ ]+$/ { print $0 }'))\n\n if [ ${#COMPREPLY[@]} -eq 0 ]; then\n COMPREPLY=()\n fi\n\n return 0\n}\ncomplete -o bashdefault -o default -F _{{app_name_safe}}_completions {{app_name}}\n###-end-{{app_name}}-completions-###\n";
|
|
33
|
+
export type ShellType = 'zsh' | 'bash';
|
|
34
|
+
/**
|
|
35
|
+
* Detect the current shell from environment variables.
|
|
36
|
+
* Returns 'zsh', 'bash', or null if unrecognized.
|
|
37
|
+
*/
|
|
38
|
+
export declare function detectShell(): ShellType | null;
|
|
39
|
+
/**
|
|
40
|
+
* Validate and normalize a shell value from user input.
|
|
41
|
+
* Returns a valid ShellType or throws if the value is invalid.
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateShell(value: unknown): ShellType | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Detect which shell format to use for completion output.
|
|
46
|
+
* Prefers the explicit GOKE_COMPLETION_SHELL env var (set by the shell shim)
|
|
47
|
+
* over the login $SHELL. This prevents format mismatch when a bash shim runs
|
|
48
|
+
* on a system where $SHELL is zsh.
|
|
49
|
+
*/
|
|
50
|
+
export declare function detectCompletionShell(): ShellType | null;
|
|
51
|
+
/**
|
|
52
|
+
* Generate a completion script for the given shell.
|
|
53
|
+
*
|
|
54
|
+
* @param shell - Target shell ('zsh' or 'bash')
|
|
55
|
+
* @param cliName - The CLI binary name (e.g. 'my-cli')
|
|
56
|
+
* @param cliPath - Full path to the CLI binary. If not provided, uses cliName.
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateCompletionScript(shell: ShellType, cliName: string, cliPath?: string): string;
|
|
59
|
+
interface InstallResult {
|
|
60
|
+
/** The file path where the completion script was written */
|
|
61
|
+
path: string;
|
|
62
|
+
/** The shell the script was generated for */
|
|
63
|
+
shell: ShellType;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find a writable completion directory, generate the shell script, and write it.
|
|
67
|
+
*
|
|
68
|
+
* For zsh, scans `$fpath` directories plus well-known fallbacks.
|
|
69
|
+
* For bash, scans XDG and legacy user completion dirs.
|
|
70
|
+
*
|
|
71
|
+
* Throws if no writable directory is found.
|
|
72
|
+
*
|
|
73
|
+
* @param cliName - The CLI binary name
|
|
74
|
+
* @param cliPath - Full path to the CLI binary
|
|
75
|
+
* @param shell - Target shell. Auto-detected from $SHELL if omitted.
|
|
76
|
+
*/
|
|
77
|
+
export declare function installCompletions(cliName: string, cliPath: string, shell?: ShellType): Promise<InstallResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Remove an installed completion script.
|
|
80
|
+
*
|
|
81
|
+
* Searches the same candidate directories as `installCompletions` and removes
|
|
82
|
+
* any matching completion files found.
|
|
83
|
+
*
|
|
84
|
+
* @returns The paths that were removed, or empty array if none found.
|
|
85
|
+
*/
|
|
86
|
+
export declare function uninstallCompletions(cliName: string, shell?: ShellType): Promise<string[]>;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=completions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../src/completions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,2EAA2E;AAC3E,eAAO,MAAM,eAAe,yBAAyB,CAAA;AAIrD;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,qnBAmBvB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,ywBAuBxB,CAAA;AAID,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,CAAA;AAEtC;;;GAGG;AACH,wBAAgB,WAAW,IAAI,SAAS,GAAG,IAAI,CAK9C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAInE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,SAAS,GAAG,IAAI,CAIxD;AAUD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CASR;AA+BD,UAAU,aAAa;IACrB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,6CAA6C;IAC7C,KAAK,EAAE,SAAS,CAAA;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,SAAS,GAChB,OAAO,CAAC,aAAa,CAAC,CA4FxB;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,SAAS,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC,CAsDnB"}
|