incur 0.3.5 → 0.3.6
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 +61 -0
- package/dist/Cli.d.ts +15 -0
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +207 -11
- package/dist/Cli.js.map +1 -1
- package/dist/Filter.js +0 -18
- package/dist/Filter.js.map +1 -1
- package/dist/Help.d.ts +4 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +17 -14
- package/dist/Help.js.map +1 -1
- package/dist/Parser.d.ts +2 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +69 -37
- package/dist/Parser.js.map +1 -1
- package/dist/bin.d.ts +1 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +17 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/command.d.ts +2 -0
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +1 -0
- package/dist/internal/command.js.map +1 -1
- package/dist/internal/configSchema.d.ts +8 -0
- package/dist/internal/configSchema.d.ts.map +1 -0
- package/dist/internal/configSchema.js +57 -0
- package/dist/internal/configSchema.js.map +1 -0
- package/dist/internal/helpers.d.ts +5 -0
- package/dist/internal/helpers.d.ts.map +1 -0
- package/dist/internal/helpers.js +9 -0
- package/dist/internal/helpers.js.map +1 -0
- package/examples/npm/.npmrc.json +21 -0
- package/examples/npm/config.schema.json +137 -0
- package/package.json +1 -1
- package/src/Cli.test-d.ts +39 -0
- package/src/Cli.test.ts +554 -3
- package/src/Cli.ts +266 -11
- package/src/Filter.ts +0 -17
- package/src/Help.test.ts +66 -0
- package/src/Help.ts +20 -13
- package/src/Parser.test-d.ts +22 -0
- package/src/Parser.test.ts +89 -0
- package/src/Parser.ts +86 -35
- package/src/bin.ts +21 -2
- package/src/e2e.test.ts +1 -1
- package/src/internal/command.ts +3 -0
- package/src/internal/configSchema.test.ts +193 -0
- package/src/internal/configSchema.ts +66 -0
- package/src/internal/helpers.ts +9 -0
package/package.json
CHANGED
package/src/Cli.test-d.ts
CHANGED
|
@@ -289,3 +289,42 @@ test('run() context exposes format metadata', () => {
|
|
|
289
289
|
},
|
|
290
290
|
})
|
|
291
291
|
})
|
|
292
|
+
|
|
293
|
+
test('create() accepts config-file defaults options', () => {
|
|
294
|
+
Cli.create('test', {
|
|
295
|
+
config: {},
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
Cli.create('test', {
|
|
299
|
+
config: { flag: 'config' },
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
Cli.create('test', {
|
|
303
|
+
config: { files: ['.myrc.json', '~/.config/my/config.json'] },
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
Cli.create('test', {
|
|
307
|
+
config: {
|
|
308
|
+
flag: 'config',
|
|
309
|
+
files: ['config.toml'],
|
|
310
|
+
loader: async (path) => {
|
|
311
|
+
if (!path) return undefined
|
|
312
|
+
return { key: 'value' }
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
Cli.create('test', {
|
|
318
|
+
config: { loader: async () => ({ key: 'value' }) },
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
Cli.create('test', {
|
|
322
|
+
// @ts-expect-error — flag must be a string
|
|
323
|
+
config: { flag: true },
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
Cli.create('test', {
|
|
327
|
+
// @ts-expect-error — files must be string[]
|
|
328
|
+
config: { files: [42] },
|
|
329
|
+
})
|
|
330
|
+
})
|
package/src/Cli.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Cli, Errors, z } from 'incur'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir, tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
|
|
3
6
|
const originalIsTTY = process.stdout.isTTY
|
|
4
7
|
beforeAll(() => {
|
|
@@ -37,6 +40,42 @@ async function serve(
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function createConfigCli(flag?: string) {
|
|
44
|
+
const project = Cli.create('project').command('list', {
|
|
45
|
+
options: z.object({
|
|
46
|
+
label: z.array(z.string()).default([]),
|
|
47
|
+
limit: z.number().default(10),
|
|
48
|
+
}),
|
|
49
|
+
run(c) {
|
|
50
|
+
return c.options
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const cli = Cli.create('test', {
|
|
55
|
+
config: flag !== undefined ? { flag } : {},
|
|
56
|
+
options: z.object({
|
|
57
|
+
rootValue: z.string().default('root-default'),
|
|
58
|
+
}),
|
|
59
|
+
run(c) {
|
|
60
|
+
return c.options
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
cli.command('echo', {
|
|
65
|
+
options: z.object({
|
|
66
|
+
prefix: z.string().default(''),
|
|
67
|
+
upper: z.boolean().default(false),
|
|
68
|
+
}),
|
|
69
|
+
run(c) {
|
|
70
|
+
return c.options
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
cli.command(project)
|
|
75
|
+
|
|
76
|
+
return cli
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
describe('create', () => {
|
|
41
80
|
test('returns cli instance with name', () => {
|
|
42
81
|
const cli = Cli.create('test')
|
|
@@ -62,6 +101,518 @@ describe('command', () => {
|
|
|
62
101
|
})
|
|
63
102
|
})
|
|
64
103
|
|
|
104
|
+
describe('config defaults', () => {
|
|
105
|
+
let cwd: string
|
|
106
|
+
let dir: string
|
|
107
|
+
|
|
108
|
+
beforeEach(async () => {
|
|
109
|
+
cwd = process.cwd()
|
|
110
|
+
dir = await mkdtemp(join(tmpdir(), 'incur-config-'))
|
|
111
|
+
process.chdir(dir)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
afterEach(async () => {
|
|
115
|
+
process.chdir(cwd)
|
|
116
|
+
await rm(dir, { force: true, recursive: true })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('auto-loads <cli>.json for leaf commands', async () => {
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(dir, 'test.json'),
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
commands: {
|
|
124
|
+
echo: {
|
|
125
|
+
options: {
|
|
126
|
+
prefix: 'cfg',
|
|
127
|
+
upper: true,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const { output } = await serve(createConfigCli(), ['echo', '--json'])
|
|
135
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'cfg', upper: true })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('ignores a missing auto config file', async () => {
|
|
139
|
+
const { output } = await serve(createConfigCli(), ['echo', '--json'])
|
|
140
|
+
expect(JSON.parse(output)).toEqual({ prefix: '', upper: false })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('root options coexist with subcommand keys', async () => {
|
|
144
|
+
await writeFile(
|
|
145
|
+
join(dir, 'test.json'),
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
options: { rootValue: 'cfg-root' },
|
|
148
|
+
commands: {
|
|
149
|
+
echo: { options: { prefix: 'cfg' } },
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const rootResult = await serve(createConfigCli(), ['--json'])
|
|
155
|
+
expect(JSON.parse(rootResult.output)).toEqual({ rootValue: 'cfg-root' })
|
|
156
|
+
|
|
157
|
+
const echoResult = await serve(createConfigCli(), ['echo', '--json'])
|
|
158
|
+
expect(JSON.parse(echoResult.output)).toEqual({ prefix: 'cfg', upper: false })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('walks nested command sections in config tree', async () => {
|
|
162
|
+
await writeFile(
|
|
163
|
+
join(dir, 'test.json'),
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
commands: {
|
|
166
|
+
project: {
|
|
167
|
+
commands: {
|
|
168
|
+
list: {
|
|
169
|
+
options: {
|
|
170
|
+
label: ['cfg'],
|
|
171
|
+
limit: 25,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const { output } = await serve(createConfigCli(), ['project', 'list', '--json'])
|
|
181
|
+
expect(JSON.parse(output)).toEqual({ label: ['cfg'], limit: 25 })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('uses an explicit --config path instead of the auto file', async () => {
|
|
185
|
+
await writeFile(
|
|
186
|
+
join(dir, 'test.json'),
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
commands: { echo: { options: { prefix: 'auto' } } },
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
await writeFile(
|
|
192
|
+
join(dir, 'custom.json'),
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
commands: { echo: { options: { prefix: 'custom', upper: true } } },
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const { output } = await serve(createConfigCli('config'), [
|
|
199
|
+
'echo',
|
|
200
|
+
'--config',
|
|
201
|
+
'custom.json',
|
|
202
|
+
'--json',
|
|
203
|
+
])
|
|
204
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'custom', upper: true })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('--no-config disables earlier config flags, and a later --config wins again', async () => {
|
|
208
|
+
await writeFile(
|
|
209
|
+
join(dir, 'one.json'),
|
|
210
|
+
JSON.stringify({
|
|
211
|
+
commands: { echo: { options: { prefix: 'one' } } },
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
await writeFile(
|
|
215
|
+
join(dir, 'two.json'),
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
commands: { echo: { options: { prefix: 'two' } } },
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const first = await serve(createConfigCli('config'), [
|
|
222
|
+
'echo',
|
|
223
|
+
'--config',
|
|
224
|
+
'one.json',
|
|
225
|
+
'--no-config',
|
|
226
|
+
'--json',
|
|
227
|
+
])
|
|
228
|
+
expect(JSON.parse(first.output)).toEqual({ prefix: '', upper: false })
|
|
229
|
+
|
|
230
|
+
const second = await serve(createConfigCli('config'), [
|
|
231
|
+
'echo',
|
|
232
|
+
'--config',
|
|
233
|
+
'one.json',
|
|
234
|
+
'--no-config',
|
|
235
|
+
'--config=two.json',
|
|
236
|
+
'--json',
|
|
237
|
+
])
|
|
238
|
+
expect(JSON.parse(second.output)).toEqual({ prefix: 'two', upper: false })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('fails when an explicit config file is missing', async () => {
|
|
242
|
+
const { exitCode, output } = await serve(createConfigCli('config'), [
|
|
243
|
+
'echo',
|
|
244
|
+
'--config',
|
|
245
|
+
'missing.json',
|
|
246
|
+
])
|
|
247
|
+
expect(exitCode).toBe(1)
|
|
248
|
+
expect(output).toContain('Config file not found')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('fails on invalid JSON config files', async () => {
|
|
252
|
+
await writeFile(join(dir, 'test.json'), '{ invalid')
|
|
253
|
+
|
|
254
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
255
|
+
expect(exitCode).toBe(1)
|
|
256
|
+
expect(output).toContain('Invalid JSON config file')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('fails when the config file top level is not an object', async () => {
|
|
260
|
+
await writeFile(join(dir, 'test.json'), JSON.stringify(['bad']))
|
|
261
|
+
|
|
262
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
263
|
+
expect(exitCode).toBe(1)
|
|
264
|
+
expect(output).toContain('expected a top-level object')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('fails when the selected config section is not an object', async () => {
|
|
268
|
+
await writeFile(join(dir, 'test.json'), JSON.stringify({ commands: { echo: true } }))
|
|
269
|
+
|
|
270
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
271
|
+
expect(exitCode).toBe(1)
|
|
272
|
+
expect(output).toContain("Invalid config section for 'echo'")
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('fails validation when config option values are invalid', async () => {
|
|
276
|
+
await writeFile(
|
|
277
|
+
join(dir, 'test.json'),
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
commands: { echo: { options: { upper: 'nope' } } },
|
|
280
|
+
}),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
const { exitCode, output } = await serve(createConfigCli(), ['echo'])
|
|
284
|
+
expect(exitCode).toBe(1)
|
|
285
|
+
expect(output).toContain('VALIDATION_ERROR')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('argv overrides invalid config values at the CLI layer', async () => {
|
|
289
|
+
await writeFile(
|
|
290
|
+
join(dir, 'test.json'),
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
commands: { echo: { options: { prefix: 123 } } },
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const { output } = await serve(createConfigCli(), ['echo', '--prefix', 'cli', '--json'])
|
|
297
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'cli', upper: false })
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('built-in commands ignore config loading', async () => {
|
|
301
|
+
await writeFile(join(dir, 'test.json'), '{ invalid')
|
|
302
|
+
|
|
303
|
+
const { output, exitCode } = await serve(createConfigCli(), ['--help'])
|
|
304
|
+
expect(exitCode).toBeUndefined()
|
|
305
|
+
expect(output).toContain('Global Options:')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('config without flag does not reserve --config', async () => {
|
|
309
|
+
const cli = Cli.create('test', { config: {} })
|
|
310
|
+
cli.command('echo', {
|
|
311
|
+
options: z.object({ config: z.string().default('') }),
|
|
312
|
+
run(c) {
|
|
313
|
+
return c.options
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const { output } = await serve(cli, ['echo', '--config', 'my-value', '--json'])
|
|
318
|
+
expect(JSON.parse(output)).toEqual({ config: 'my-value' })
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('--help shows config flags only when flag name is set', async () => {
|
|
322
|
+
const { output } = await serve(createConfigCli('config'), ['--help'])
|
|
323
|
+
expect(output).toContain('--config <path>')
|
|
324
|
+
expect(output).toContain('--no-config')
|
|
325
|
+
|
|
326
|
+
const { output: noFlagOutput } = await serve(createConfigCli(), ['--help'])
|
|
327
|
+
expect(noFlagOutput).not.toContain('--config')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('custom flag name is used for config path override', async () => {
|
|
331
|
+
await writeFile(
|
|
332
|
+
join(dir, 'custom.json'),
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
commands: { echo: { options: { prefix: 'custom' } } },
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const { output } = await serve(createConfigCli('settings'), [
|
|
339
|
+
'echo',
|
|
340
|
+
'--settings',
|
|
341
|
+
'custom.json',
|
|
342
|
+
'--json',
|
|
343
|
+
])
|
|
344
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'custom', upper: false })
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('searches files list in order, first match wins', async () => {
|
|
348
|
+
await writeFile(
|
|
349
|
+
join(dir, '.testrc.json'),
|
|
350
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'rc' } } } }),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
const cli = Cli.create('test', {
|
|
354
|
+
config: { files: ['test.json', '.testrc.json'] },
|
|
355
|
+
})
|
|
356
|
+
cli.command('echo', {
|
|
357
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
358
|
+
run: (c) => c.options,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
362
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'rc' })
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('files: [] disables auto-discovery', async () => {
|
|
366
|
+
await writeFile(
|
|
367
|
+
join(dir, 'test.json'),
|
|
368
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'should-not-load' } } } }),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
const cli = Cli.create('test', {
|
|
372
|
+
config: { files: [] },
|
|
373
|
+
})
|
|
374
|
+
cli.command('echo', {
|
|
375
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
376
|
+
run: (c) => c.options,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
380
|
+
expect(JSON.parse(output)).toEqual({ prefix: '' })
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('files supports ~ for home directory', async () => {
|
|
384
|
+
const configDir = join(homedir(), '.config', 'test-incur-files-tilde')
|
|
385
|
+
await mkdir(configDir, { recursive: true })
|
|
386
|
+
try {
|
|
387
|
+
await writeFile(
|
|
388
|
+
join(configDir, 'config.json'),
|
|
389
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'home' } } } }),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
const cli = Cli.create('test', {
|
|
393
|
+
config: { files: ['test.json', '~/.config/test-incur-files-tilde/config.json'] },
|
|
394
|
+
})
|
|
395
|
+
cli.command('echo', {
|
|
396
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
397
|
+
run: (c) => c.options,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
401
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'home' })
|
|
402
|
+
} finally {
|
|
403
|
+
await rm(configDir, { recursive: true, force: true })
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('explicit --flag overrides files list', async () => {
|
|
408
|
+
await writeFile(
|
|
409
|
+
join(dir, '.testrc.json'),
|
|
410
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'rc' } } } }),
|
|
411
|
+
)
|
|
412
|
+
await writeFile(
|
|
413
|
+
join(dir, 'override.json'),
|
|
414
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'override' } } } }),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
const cli = Cli.create('test', {
|
|
418
|
+
config: { flag: 'config', files: ['.testrc.json'] },
|
|
419
|
+
})
|
|
420
|
+
cli.command('echo', {
|
|
421
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
422
|
+
run: (c) => c.options,
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const { output } = await serve(cli, ['echo', '--config', 'override.json', '--json'])
|
|
426
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'override' })
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test('custom loader replaces JSON parsing', async () => {
|
|
430
|
+
await writeFile(join(dir, 'test.ini'), 'prefix=ini-value')
|
|
431
|
+
|
|
432
|
+
const cli = Cli.create('test', {
|
|
433
|
+
config: {
|
|
434
|
+
files: ['test.ini'],
|
|
435
|
+
async loader(path) {
|
|
436
|
+
if (!path) return undefined
|
|
437
|
+
const raw = await readFile(path, 'utf8')
|
|
438
|
+
const obj: Record<string, unknown> = {}
|
|
439
|
+
for (const line of raw.split('\n')) {
|
|
440
|
+
const eq = line.indexOf('=')
|
|
441
|
+
if (eq !== -1) obj[line.slice(0, eq).trim()] = line.slice(eq + 1).trim()
|
|
442
|
+
}
|
|
443
|
+
return { commands: { echo: { options: obj } } }
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
cli.command('echo', {
|
|
448
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
449
|
+
run: (c) => c.options,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
453
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'ini-value' })
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('loader with files: [] receives undefined path', async () => {
|
|
457
|
+
const cli = Cli.create('test', {
|
|
458
|
+
config: {
|
|
459
|
+
files: [],
|
|
460
|
+
loader: async (path) => {
|
|
461
|
+
expect(path).toBeUndefined()
|
|
462
|
+
return { commands: { echo: { options: { prefix: 'from-loader' } } } }
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
cli.command('echo', {
|
|
467
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
468
|
+
run: (c) => c.options,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
472
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'from-loader' })
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('loader returning undefined applies no defaults', async () => {
|
|
476
|
+
const cli = Cli.create('test', {
|
|
477
|
+
config: { files: [], loader: async () => undefined },
|
|
478
|
+
})
|
|
479
|
+
cli.command('echo', {
|
|
480
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
481
|
+
run: (c) => c.options,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
485
|
+
expect(JSON.parse(output)).toEqual({ prefix: '' })
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('--no-flag skips loader entirely', async () => {
|
|
489
|
+
let loaderCalled = false
|
|
490
|
+
const cli = Cli.create('test', {
|
|
491
|
+
config: {
|
|
492
|
+
flag: 'config',
|
|
493
|
+
files: [],
|
|
494
|
+
loader: async () => {
|
|
495
|
+
loaderCalled = true
|
|
496
|
+
return { commands: { echo: { options: { prefix: 'should-not-load' } } } }
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
cli.command('echo', {
|
|
501
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
502
|
+
run: (c) => c.options,
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const { output } = await serve(cli, ['echo', '--no-config', '--json'])
|
|
506
|
+
expect(JSON.parse(output)).toEqual({ prefix: '' })
|
|
507
|
+
expect(loaderCalled).toBe(false)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('loader errors propagate', async () => {
|
|
511
|
+
const cli = Cli.create('test', {
|
|
512
|
+
config: {
|
|
513
|
+
files: [],
|
|
514
|
+
loader: async () => {
|
|
515
|
+
throw new Error('Remote config server unreachable')
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
})
|
|
519
|
+
cli.command('echo', {
|
|
520
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
521
|
+
run: (c) => c.options,
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const { exitCode, output } = await serve(cli, ['echo'])
|
|
525
|
+
expect(exitCode).toBe(1)
|
|
526
|
+
expect(output).toContain('Remote config server unreachable')
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
test('--no-flag disables auto-discovery without prior --flag', async () => {
|
|
530
|
+
await writeFile(
|
|
531
|
+
join(dir, 'test.json'),
|
|
532
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'auto-loaded' } } } }),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
const { output } = await serve(createConfigCli('config'), ['echo', '--no-config', '--json'])
|
|
536
|
+
expect(JSON.parse(output)).toEqual({ prefix: '', upper: false })
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test('--config without a value produces an error', async () => {
|
|
540
|
+
const { exitCode, output } = await serve(createConfigCli('config'), ['echo', '--config'])
|
|
541
|
+
expect(exitCode).toBe(1)
|
|
542
|
+
expect(output).toContain('Missing value for flag')
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test('--config= (empty value) produces an error', async () => {
|
|
546
|
+
const { exitCode, output } = await serve(createConfigCli('config'), ['echo', '--config='])
|
|
547
|
+
expect(exitCode).toBe(1)
|
|
548
|
+
expect(output).toContain('Missing value for flag')
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('--no-settings works with custom flag name', async () => {
|
|
552
|
+
await writeFile(
|
|
553
|
+
join(dir, 'test.json'),
|
|
554
|
+
JSON.stringify({ commands: { echo: { options: { prefix: 'auto' } } } }),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const { output } = await serve(createConfigCli('settings'), ['echo', '--no-settings', '--json'])
|
|
558
|
+
expect(JSON.parse(output)).toEqual({ prefix: '', upper: false })
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('camelCase config keys are accepted at cli level', async () => {
|
|
562
|
+
const cli = Cli.create('test', { config: {} })
|
|
563
|
+
cli.command('echo', {
|
|
564
|
+
options: z.object({ saveDev: z.boolean().default(false) }),
|
|
565
|
+
run: (c) => c.options,
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
await writeFile(
|
|
569
|
+
join(dir, 'test.json'),
|
|
570
|
+
JSON.stringify({ commands: { echo: { options: { 'save-dev': true } } } }),
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
const { output } = await serve(cli, ['echo', '--json'])
|
|
574
|
+
expect(JSON.parse(output)).toEqual({ saveDev: true })
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
test('config defaults with only subcommand namespaces yields no option defaults', async () => {
|
|
578
|
+
await writeFile(
|
|
579
|
+
join(dir, 'test.json'),
|
|
580
|
+
JSON.stringify({
|
|
581
|
+
commands: {
|
|
582
|
+
echo: { options: { prefix: 'child' } },
|
|
583
|
+
project: { commands: { list: { options: { limit: 50 } } } },
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
const rootResult = await serve(createConfigCli(), ['--json'])
|
|
589
|
+
expect(JSON.parse(rootResult.output)).toEqual({ rootValue: 'root-default' })
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
test('explicit --flag path is forwarded to custom loader', async () => {
|
|
593
|
+
await writeFile(join(dir, 'custom.dat'), 'prefix=custom-loader')
|
|
594
|
+
|
|
595
|
+
const cli = Cli.create('test', {
|
|
596
|
+
config: {
|
|
597
|
+
flag: 'config',
|
|
598
|
+
async loader(path) {
|
|
599
|
+
if (!path) return undefined
|
|
600
|
+
const raw = await readFile(path, 'utf8')
|
|
601
|
+
const [, value] = raw.split('=')
|
|
602
|
+
return { commands: { echo: { options: { prefix: value!.trim() } } } }
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
})
|
|
606
|
+
cli.command('echo', {
|
|
607
|
+
options: z.object({ prefix: z.string().default('') }),
|
|
608
|
+
run: (c) => c.options,
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
const { output } = await serve(cli, ['echo', '--config', 'custom.dat', '--json'])
|
|
612
|
+
expect(JSON.parse(output)).toEqual({ prefix: 'custom-loader' })
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
65
616
|
describe('serve', () => {
|
|
66
617
|
test('outputs data only by default', async () => {
|
|
67
618
|
const cli = Cli.create('test')
|
|
@@ -1733,7 +2284,7 @@ describe('env', () => {
|
|
|
1733
2284
|
--verbose Show full output envelope
|
|
1734
2285
|
|
|
1735
2286
|
Environment Variables:
|
|
1736
|
-
API_TOKEN Auth token (set:
|
|
2287
|
+
API_TOKEN Auth token (set: ****cret)
|
|
1737
2288
|
API_URL API URL (default: https://api.example.com)
|
|
1738
2289
|
"
|
|
1739
2290
|
`)
|
|
@@ -1742,7 +2293,7 @@ describe('env', () => {
|
|
|
1742
2293
|
process.env.API_URL = 'https://custom.example.com'
|
|
1743
2294
|
const { output: output2 } = await serve(cli, ['deploy', '--help'])
|
|
1744
2295
|
expect(output2).toContain(
|
|
1745
|
-
'API_URL API URL (set:
|
|
2296
|
+
'API_URL API URL (set: ****.com, default: https://api.example.com)',
|
|
1746
2297
|
)
|
|
1747
2298
|
} finally {
|
|
1748
2299
|
delete process.env.API_TOKEN
|
|
@@ -1770,7 +2321,7 @@ describe('env', () => {
|
|
|
1770
2321
|
const { output: output2 } = await serve(cli, ['deploy', '--help'], {
|
|
1771
2322
|
env: { API_TOKEN: 'secret' },
|
|
1772
2323
|
})
|
|
1773
|
-
expect(output2).toContain('set:
|
|
2324
|
+
expect(output2).toContain('set: ****cret')
|
|
1774
2325
|
})
|
|
1775
2326
|
|
|
1776
2327
|
test('--llms json includes schema.env', async () => {
|