goke 6.5.0 → 6.5.2
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/README.md +176 -9
- package/dist/__test__/readme-examples.test.d.ts +5 -0
- package/dist/__test__/readme-examples.test.d.ts.map +1 -0
- package/dist/__test__/readme-examples.test.js +169 -0
- package/dist/__test__/types.test-d.js +238 -1
- package/dist/goke.d.ts +105 -16
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +22 -2
- package/dist/picocolors.d.ts +55 -0
- package/dist/picocolors.d.ts.map +1 -0
- package/dist/picocolors.js +78 -0
- package/dist/runtime-node.js +2 -2
- package/package.json +3 -5
- package/src/__test__/readme-examples.test.ts +225 -0
- package/src/__test__/types.test-d.ts +262 -1
- package/src/goke.ts +149 -16
- package/src/picocolors.ts +140 -0
- package/src/runtime-node.ts +2 -2
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests that keep README examples and documented APIs executable.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import goke, { openInBrowser } from '../index.js'
|
|
8
|
+
import type { GokeOptions, GokeOutputStream } from '../index.js'
|
|
9
|
+
|
|
10
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g
|
|
11
|
+
|
|
12
|
+
const stripAnsi = (text: string) => text.replace(ANSI_RE, '')
|
|
13
|
+
|
|
14
|
+
function createTestOutputStream(): GokeOutputStream & { lines: string[]; readonly text: string } {
|
|
15
|
+
const lines: string[] = []
|
|
16
|
+
return {
|
|
17
|
+
lines,
|
|
18
|
+
get text() { return stripAnsi(lines.join('')) },
|
|
19
|
+
write(data: string) { lines.push(data) },
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function gokeTestable(name = '', options?: Partial<GokeOptions>) {
|
|
24
|
+
return goke(name, {
|
|
25
|
+
...options,
|
|
26
|
+
exit: () => {},
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('README smoke tests', () => {
|
|
31
|
+
test('intro example runs middleware and both command forms', async () => {
|
|
32
|
+
const stdout = createTestOutputStream()
|
|
33
|
+
const cli = gokeTestable('deploy', { stdout })
|
|
34
|
+
|
|
35
|
+
cli
|
|
36
|
+
.option('--env <env>', z.enum(['staging', 'production']).default('staging').describe('Target environment'))
|
|
37
|
+
.use((options, { console }) => {
|
|
38
|
+
console.log(`Environment: ${options.env}`)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
cli
|
|
42
|
+
.command('up', 'Deploy the app')
|
|
43
|
+
.option('--dry-run', 'Preview without deploying')
|
|
44
|
+
.action((options, { console, process }) => {
|
|
45
|
+
console.log(`Deploying from ${process.cwd} dryRun=${String(options.dryRun)}`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
cli
|
|
49
|
+
.command('logs <deploymentId>', 'Stream logs')
|
|
50
|
+
.option('--lines <n>', z.number().default(100).describe('Lines to tail'))
|
|
51
|
+
.action((deploymentId, options, { console }) => {
|
|
52
|
+
console.log(`logs ${deploymentId} ${options.lines}`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
cli.parse(['node', 'bin', '--env', 'production', 'up', '--dry-run'], { run: false })
|
|
56
|
+
await cli.runMatchedCommand()
|
|
57
|
+
|
|
58
|
+
expect(stdout.text).toBe(
|
|
59
|
+
`Environment: production\nDeploying from ${process.cwd()} dryRun=true\n`,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
stdout.lines.length = 0
|
|
63
|
+
|
|
64
|
+
cli.parse(['node', 'bin', 'logs', 'dep_123'], { run: false })
|
|
65
|
+
await cli.runMatchedCommand()
|
|
66
|
+
|
|
67
|
+
expect(stdout.text).toBe('Environment: staging\nlogs dep_123 100\n')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('simple parsing example stays executable and keeps examples in help output', async () => {
|
|
71
|
+
const stdout = createTestOutputStream()
|
|
72
|
+
const cli = gokeTestable('mycli', { stdout })
|
|
73
|
+
|
|
74
|
+
cli.option(
|
|
75
|
+
'--type [type]',
|
|
76
|
+
z.string().default('node').describe('Choose a project type'),
|
|
77
|
+
)
|
|
78
|
+
cli.option('--name <name>', 'Provide your name')
|
|
79
|
+
|
|
80
|
+
cli.command('lint [...files]', 'Lint files').action((files, options, { console, process }) => {
|
|
81
|
+
console.log(JSON.stringify({ files, options, cwd: process.cwd }))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
cli
|
|
85
|
+
.command('build [entry]', 'Build your app')
|
|
86
|
+
.option('--minify', 'Minify output')
|
|
87
|
+
.example('build src/index.ts')
|
|
88
|
+
.example('build src/index.ts --minify')
|
|
89
|
+
.action(async (entry, options, { console, process }) => {
|
|
90
|
+
console.log(JSON.stringify({ entry, options, nodeEnv: process.env.NODE_ENV }))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
cli.example((bin) => `${bin} lint src/**/*.ts`)
|
|
94
|
+
cli.help()
|
|
95
|
+
cli.version('0.0.0')
|
|
96
|
+
|
|
97
|
+
expect(stripAnsi(cli.helpText())).toContain('mycli lint src/**/*.ts')
|
|
98
|
+
|
|
99
|
+
cli.parse(['node', 'bin', '--type', 'bun', '--name', 'Tommy', 'build', 'src/index.ts', '--minify'], { run: false })
|
|
100
|
+
await cli.runMatchedCommand()
|
|
101
|
+
|
|
102
|
+
expect(stdout.text).toBe(
|
|
103
|
+
`${JSON.stringify({
|
|
104
|
+
entry: 'src/index.ts',
|
|
105
|
+
options: {
|
|
106
|
+
'--': [],
|
|
107
|
+
type: 'bun',
|
|
108
|
+
name: 'Tommy',
|
|
109
|
+
minify: true,
|
|
110
|
+
},
|
|
111
|
+
nodeEnv: process.env.NODE_ENV,
|
|
112
|
+
})}\n`,
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('many-commands README example runs root and nested commands', async () => {
|
|
117
|
+
const stdout = createTestOutputStream()
|
|
118
|
+
const cli = gokeTestable('deploy', { stdout })
|
|
119
|
+
|
|
120
|
+
cli
|
|
121
|
+
.command('', 'Deploy the current project')
|
|
122
|
+
.option('--env <env>', z.string().default('production').describe('Target environment'))
|
|
123
|
+
.option('--dry-run', 'Preview without deploying')
|
|
124
|
+
.action((options, { console, process }) => {
|
|
125
|
+
console.log(`Deploying to ${options.env} from ${process.cwd} dryRun=${String(options.dryRun)}`)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
cli
|
|
129
|
+
.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
130
|
+
.option('--follow', 'Follow log output')
|
|
131
|
+
.option('--lines <n>', z.number().default(100).describe('Number of lines'))
|
|
132
|
+
.action((deploymentId, options, { console, process }) => {
|
|
133
|
+
console.log(
|
|
134
|
+
`Streaming logs for ${deploymentId} from ${process.cwd} follow=${String(options.follow)} lines=${options.lines}`,
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: false })
|
|
139
|
+
await cli.runMatchedCommand()
|
|
140
|
+
|
|
141
|
+
expect(stdout.text).toBe(
|
|
142
|
+
`Deploying to staging from ${process.cwd()} dryRun=true\n`,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
stdout.lines.length = 0
|
|
146
|
+
|
|
147
|
+
cli.parse(['node', 'bin', 'logs', 'abc123', '--follow'], { run: false })
|
|
148
|
+
await cli.runMatchedCommand()
|
|
149
|
+
|
|
150
|
+
expect(stdout.text).toBe(
|
|
151
|
+
`Streaming logs for abc123 from ${process.cwd()} follow=true lines=100\n`,
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('documented command APIs', () => {
|
|
157
|
+
test('alias runs the same command through a short name', () => {
|
|
158
|
+
const cli = gokeTestable('mycli')
|
|
159
|
+
let seen = ''
|
|
160
|
+
|
|
161
|
+
cli.command('install', 'Install packages').alias('i').action(() => {
|
|
162
|
+
seen = 'install'
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
cli.parse(['node', 'bin', 'i'], { run: true })
|
|
166
|
+
|
|
167
|
+
expect(seen).toBe('install')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('command helpText returns command-specific help without printing', () => {
|
|
171
|
+
const stdout = createTestOutputStream()
|
|
172
|
+
const cli = goke('mycli', { stdout })
|
|
173
|
+
|
|
174
|
+
const command = cli
|
|
175
|
+
.command('deploy <env>', 'Deploy to an environment')
|
|
176
|
+
.option('--dry-run', 'Preview without deploying')
|
|
177
|
+
.example('# Deploy safely first')
|
|
178
|
+
.example('mycli deploy staging --dry-run')
|
|
179
|
+
|
|
180
|
+
cli.help()
|
|
181
|
+
|
|
182
|
+
const help = stripAnsi(command.helpText())
|
|
183
|
+
|
|
184
|
+
expect(help).toContain('$ mycli deploy <env>')
|
|
185
|
+
expect(help).toContain('--dry-run')
|
|
186
|
+
expect(help).toContain('Deploy safely first')
|
|
187
|
+
expect(stdout.text).toBe('')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('openInBrowser prints the URL to stdout in non-tty environments', () => {
|
|
191
|
+
const url = 'https://example.com/dashboard'
|
|
192
|
+
const originalStdoutWrite = process.stdout.write
|
|
193
|
+
const originalStderrWrite = process.stderr.write
|
|
194
|
+
const originalIsTTY = process.stdout.isTTY
|
|
195
|
+
let stdout = ''
|
|
196
|
+
let stderr = ''
|
|
197
|
+
|
|
198
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
199
|
+
configurable: true,
|
|
200
|
+
value: false,
|
|
201
|
+
})
|
|
202
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
203
|
+
stdout += String(chunk)
|
|
204
|
+
return true
|
|
205
|
+
}) as typeof process.stdout.write
|
|
206
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
207
|
+
stderr += String(chunk)
|
|
208
|
+
return true
|
|
209
|
+
}) as typeof process.stderr.write
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
openInBrowser(url)
|
|
213
|
+
} finally {
|
|
214
|
+
process.stdout.write = originalStdoutWrite
|
|
215
|
+
process.stderr.write = originalStderrWrite
|
|
216
|
+
Object.defineProperty(process.stdout, 'isTTY', {
|
|
217
|
+
configurable: true,
|
|
218
|
+
value: originalIsTTY,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(stdout).toBe(`${url}\n`)
|
|
223
|
+
expect(stderr).toBe('')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type-level tests for schema-based option inference.
|
|
3
3
|
* These tests verify that TypeScript infers the correct types from
|
|
4
|
-
* option names (template literals) and StandardJSONSchemaV1 schemas
|
|
4
|
+
* option names (template literals) and StandardJSONSchemaV1 schemas,
|
|
5
|
+
* and that `.action()` callbacks receive fully-typed positional args
|
|
6
|
+
* and options objects.
|
|
5
7
|
*
|
|
6
8
|
* These use expectTypeOf from vitest for compile-time type assertions.
|
|
7
9
|
*/
|
|
8
10
|
import { describe, test, expectTypeOf } from 'vitest'
|
|
11
|
+
import { z } from 'zod'
|
|
9
12
|
import type { StandardTypedV1, StandardJSONSchemaV1 } from '../coerce.js'
|
|
13
|
+
import type { GokeExecutionContext } from '../goke.js'
|
|
10
14
|
import goke from '../index.js'
|
|
11
15
|
|
|
12
16
|
// ─── Import type helpers from Command.ts ───
|
|
@@ -167,3 +171,260 @@ describe('type-level: middleware use() callback inference', () => {
|
|
|
167
171
|
})
|
|
168
172
|
})
|
|
169
173
|
})
|
|
174
|
+
|
|
175
|
+
describe('type-level: command() .action() positional args inference', () => {
|
|
176
|
+
test('command with no args → action receives only (options, ctx)', () => {
|
|
177
|
+
goke('test')
|
|
178
|
+
.command('deploy', 'Deploy the app')
|
|
179
|
+
.action((options, ctx) => {
|
|
180
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
181
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('command with one required arg → action receives (arg, options, ctx)', () => {
|
|
186
|
+
goke('test')
|
|
187
|
+
.command('get <id>', 'Fetch a resource by id')
|
|
188
|
+
.action((id, options, ctx) => {
|
|
189
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
190
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
191
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('command with two required args → action receives both as strings', () => {
|
|
196
|
+
goke('test')
|
|
197
|
+
.command('convert <input> <output>', 'Convert file formats')
|
|
198
|
+
.action((input, output, options) => {
|
|
199
|
+
expectTypeOf(input).toEqualTypeOf<string>()
|
|
200
|
+
expectTypeOf(output).toEqualTypeOf<string>()
|
|
201
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('command with optional arg → arg type includes undefined', () => {
|
|
206
|
+
goke('test')
|
|
207
|
+
.command('run [script]', 'Run a script')
|
|
208
|
+
.action((script, options) => {
|
|
209
|
+
expectTypeOf(script).toEqualTypeOf<string | undefined>()
|
|
210
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('command with variadic required arg → arg is string[]', () => {
|
|
215
|
+
goke('test')
|
|
216
|
+
.command('exec <...args>', 'Run a binary with args')
|
|
217
|
+
.action((args, options) => {
|
|
218
|
+
expectTypeOf(args).toEqualTypeOf<string[]>()
|
|
219
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('command with variadic optional arg → arg is string[]', () => {
|
|
224
|
+
goke('test')
|
|
225
|
+
.command('run [...rest]', 'Variadic optional')
|
|
226
|
+
.action((rest, options) => {
|
|
227
|
+
expectTypeOf(rest).toEqualTypeOf<string[]>()
|
|
228
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('multi-word command with required arg', () => {
|
|
233
|
+
goke('test')
|
|
234
|
+
.command('mcp getNodeXml <id>', 'Get XML for a node')
|
|
235
|
+
.action((id, options) => {
|
|
236
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
237
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('default command with one positional arg', () => {
|
|
242
|
+
goke('test')
|
|
243
|
+
.command('<file>', 'Default command')
|
|
244
|
+
.action((file, options) => {
|
|
245
|
+
expectTypeOf(file).toEqualTypeOf<string>()
|
|
246
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('mixed required and optional positional args', () => {
|
|
251
|
+
goke('test')
|
|
252
|
+
.command('send <to> [cc]', 'Send a message')
|
|
253
|
+
.action((to, cc, options) => {
|
|
254
|
+
expectTypeOf(to).toEqualTypeOf<string>()
|
|
255
|
+
expectTypeOf(cc).toEqualTypeOf<string | undefined>()
|
|
256
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('type-level: command() .action() option inference', () => {
|
|
262
|
+
test('single schema-based option is visible on options param', () => {
|
|
263
|
+
goke('test')
|
|
264
|
+
.command('serve', 'Start server')
|
|
265
|
+
.option('--port <port>', z.number())
|
|
266
|
+
.action((options, ctx) => {
|
|
267
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
268
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('multiple schema-based options are accumulated', () => {
|
|
273
|
+
goke('test')
|
|
274
|
+
.command('serve', 'Start server')
|
|
275
|
+
.option('--port <port>', z.number())
|
|
276
|
+
.option('--host <host>', z.string())
|
|
277
|
+
.option('--verbose', z.boolean())
|
|
278
|
+
.action((options) => {
|
|
279
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
280
|
+
expectTypeOf(options.host).toEqualTypeOf<string>()
|
|
281
|
+
// Boolean flag is optional (no <...> brackets)
|
|
282
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('required vs optional option shape', () => {
|
|
287
|
+
goke('test')
|
|
288
|
+
.command('cmd', 'Command')
|
|
289
|
+
.option('--name <name>', z.string())
|
|
290
|
+
.option('--count [count]', z.number())
|
|
291
|
+
.action((options) => {
|
|
292
|
+
expectTypeOf(options.name).toEqualTypeOf<string>()
|
|
293
|
+
expectTypeOf(options.count).toEqualTypeOf<number | undefined>()
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('camelCase conversion for kebab-case option names', () => {
|
|
298
|
+
goke('test')
|
|
299
|
+
.command('build', 'Build')
|
|
300
|
+
.option('--out-dir <dir>', z.string())
|
|
301
|
+
.option('--my-long-flag <val>', z.string())
|
|
302
|
+
.action((options) => {
|
|
303
|
+
expectTypeOf(options.outDir).toEqualTypeOf<string>()
|
|
304
|
+
expectTypeOf(options.myLongFlag).toEqualTypeOf<string>()
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('options combined with positional args', () => {
|
|
309
|
+
goke('test')
|
|
310
|
+
.command('convert <input> <output>', 'Convert file format')
|
|
311
|
+
.option('--quality <quality>', z.number())
|
|
312
|
+
.option('--format <format>', z.enum(['png', 'jpg', 'webp']))
|
|
313
|
+
.action((input, output, options, ctx) => {
|
|
314
|
+
expectTypeOf(input).toEqualTypeOf<string>()
|
|
315
|
+
expectTypeOf(output).toEqualTypeOf<string>()
|
|
316
|
+
expectTypeOf(options.quality).toEqualTypeOf<number>()
|
|
317
|
+
expectTypeOf(options.format).toEqualTypeOf<'png' | 'jpg' | 'webp'>()
|
|
318
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('global options from Goke are visible inside command actions', () => {
|
|
323
|
+
goke('test')
|
|
324
|
+
.option('--verbose', z.boolean())
|
|
325
|
+
.command('serve', 'Start server')
|
|
326
|
+
.option('--port <port>', z.number())
|
|
327
|
+
.action((options) => {
|
|
328
|
+
// Global option from cli.option()
|
|
329
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
330
|
+
// Command-local option
|
|
331
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('untyped option (string description) produces loose value type', () => {
|
|
336
|
+
goke('test')
|
|
337
|
+
.command('serve', 'Start server')
|
|
338
|
+
.option('--port <port>', 'Port number')
|
|
339
|
+
.action((options) => {
|
|
340
|
+
// Without a schema the runtime still guarantees required value options are strings.
|
|
341
|
+
expectTypeOf(options.port).toEqualTypeOf<string>()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('untyped optional value options keep raw mri sentinel shapes', () => {
|
|
346
|
+
goke('test')
|
|
347
|
+
.command('serve', 'Start server')
|
|
348
|
+
.option('--host [host]', 'Optional host override')
|
|
349
|
+
.option('--verbose', 'Verbose output')
|
|
350
|
+
.action((options) => {
|
|
351
|
+
expectTypeOf(options.host).toEqualTypeOf<string | boolean | undefined>()
|
|
352
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('accessing a non-existent option in action is a type error', () => {
|
|
357
|
+
goke('test')
|
|
358
|
+
.command('serve', 'Start server')
|
|
359
|
+
.option('--port <port>', z.number())
|
|
360
|
+
.action((options) => {
|
|
361
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
362
|
+
// @ts-expect-error nonExistent was never declared
|
|
363
|
+
options.nonExistent
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('accessing a non-existent positional arg in action is a type error', () => {
|
|
368
|
+
goke('test')
|
|
369
|
+
.command('get <id>', 'Fetch resource')
|
|
370
|
+
.action((id, options, ctx, ...rest) => {
|
|
371
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
372
|
+
expectTypeOf(options).toEqualTypeOf<{}>()
|
|
373
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
374
|
+
// No more positional slots — rest should be empty
|
|
375
|
+
expectTypeOf(rest).toEqualTypeOf<[]>()
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('action callback can omit trailing params (fewer-args is valid)', () => {
|
|
380
|
+
// Dropping context is fine
|
|
381
|
+
goke('test')
|
|
382
|
+
.command('serve', 'Start server')
|
|
383
|
+
.option('--port <port>', z.number())
|
|
384
|
+
.action((options) => {
|
|
385
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Dropping everything is fine
|
|
389
|
+
goke('test')
|
|
390
|
+
.command('serve', 'Start server')
|
|
391
|
+
.option('--port <port>', z.number())
|
|
392
|
+
.action(() => {})
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
describe('type-level: README TypeScript examples', () => {
|
|
397
|
+
test('README TypeScript example infers positional args and typed options', () => {
|
|
398
|
+
goke('my-program')
|
|
399
|
+
.command('serve <entry>', 'Start the app')
|
|
400
|
+
.option('--port <port>', z.number().default(3000).describe('Port number'))
|
|
401
|
+
.option('--watch', 'Watch files')
|
|
402
|
+
.action((entry, options, { console, process }) => {
|
|
403
|
+
expectTypeOf(entry).toEqualTypeOf<string>()
|
|
404
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
405
|
+
expectTypeOf(options.watch).toEqualTypeOf<boolean | undefined>()
|
|
406
|
+
expectTypeOf(console.log).toBeFunction()
|
|
407
|
+
expectTypeOf(process.cwd).toEqualTypeOf<string>()
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('README global options and middleware example stays typed end-to-end', () => {
|
|
412
|
+
goke('mycli')
|
|
413
|
+
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
414
|
+
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
415
|
+
.use((options, { process }) => {
|
|
416
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
417
|
+
expectTypeOf(options.apiUrl).toEqualTypeOf<string | undefined>()
|
|
418
|
+
expectTypeOf(process.stdin).toEqualTypeOf<string>()
|
|
419
|
+
})
|
|
420
|
+
.command('deploy <env>', 'Deploy to an environment')
|
|
421
|
+
.option('--dry-run', 'Preview without deploying')
|
|
422
|
+
.action((env, options, ctx) => {
|
|
423
|
+
expectTypeOf(env).toEqualTypeOf<string>()
|
|
424
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
425
|
+
expectTypeOf(options.apiUrl).toEqualTypeOf<string | undefined>()
|
|
426
|
+
expectTypeOf(options.dryRun).toEqualTypeOf<boolean | undefined>()
|
|
427
|
+
expectTypeOf(ctx).toEqualTypeOf<GokeExecutionContext>()
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
})
|