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.
Files changed (49) hide show
  1. package/README.md +61 -0
  2. package/dist/Cli.d.ts +15 -0
  3. package/dist/Cli.d.ts.map +1 -1
  4. package/dist/Cli.js +207 -11
  5. package/dist/Cli.js.map +1 -1
  6. package/dist/Filter.js +0 -18
  7. package/dist/Filter.js.map +1 -1
  8. package/dist/Help.d.ts +4 -0
  9. package/dist/Help.d.ts.map +1 -1
  10. package/dist/Help.js +17 -14
  11. package/dist/Help.js.map +1 -1
  12. package/dist/Parser.d.ts +2 -0
  13. package/dist/Parser.d.ts.map +1 -1
  14. package/dist/Parser.js +69 -37
  15. package/dist/Parser.js.map +1 -1
  16. package/dist/bin.d.ts +1 -0
  17. package/dist/bin.d.ts.map +1 -1
  18. package/dist/bin.js +17 -2
  19. package/dist/bin.js.map +1 -1
  20. package/dist/internal/command.d.ts +2 -0
  21. package/dist/internal/command.d.ts.map +1 -1
  22. package/dist/internal/command.js +1 -0
  23. package/dist/internal/command.js.map +1 -1
  24. package/dist/internal/configSchema.d.ts +8 -0
  25. package/dist/internal/configSchema.d.ts.map +1 -0
  26. package/dist/internal/configSchema.js +57 -0
  27. package/dist/internal/configSchema.js.map +1 -0
  28. package/dist/internal/helpers.d.ts +5 -0
  29. package/dist/internal/helpers.d.ts.map +1 -0
  30. package/dist/internal/helpers.js +9 -0
  31. package/dist/internal/helpers.js.map +1 -0
  32. package/examples/npm/.npmrc.json +21 -0
  33. package/examples/npm/config.schema.json +137 -0
  34. package/package.json +1 -1
  35. package/src/Cli.test-d.ts +39 -0
  36. package/src/Cli.test.ts +554 -3
  37. package/src/Cli.ts +266 -11
  38. package/src/Filter.ts +0 -17
  39. package/src/Help.test.ts +66 -0
  40. package/src/Help.ts +20 -13
  41. package/src/Parser.test-d.ts +22 -0
  42. package/src/Parser.test.ts +89 -0
  43. package/src/Parser.ts +86 -35
  44. package/src/bin.ts +21 -2
  45. package/src/e2e.test.ts +1 -1
  46. package/src/internal/command.ts +3 -0
  47. package/src/internal/configSchema.test.ts +193 -0
  48. package/src/internal/configSchema.ts +66 -0
  49. package/src/internal/helpers.ts +9 -0
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "[!start-pkg]": "",
17
17
  "name": "incur",
18
18
  "type": "module",
19
- "version": "0.3.5",
19
+ "version": "0.3.6",
20
20
  "license": "MIT",
21
21
  "repository": {
22
22
  "type": "git",
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: ••••ret)
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: ••••com, default: https://api.example.com)',
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: ••••ret')
2324
+ expect(output2).toContain('set: ****cret')
1774
2325
  })
1775
2326
 
1776
2327
  test('--llms json includes schema.env', async () => {