incur 0.1.4 → 0.1.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 +81 -31
- package/SKILL.md +45 -11
- package/dist/Cli.d.ts +27 -0
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +80 -61
- package/dist/Cli.js.map +1 -1
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +22 -9
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.js +4 -4
- package/dist/bin.js.map +1 -1
- package/dist/internal/agents.d.ts +7 -0
- package/dist/internal/agents.d.ts.map +1 -1
- package/dist/internal/agents.js +30 -2
- package/dist/internal/agents.js.map +1 -1
- package/examples/npm/cli.ts +8 -8
- package/package.json +1 -1
- package/src/Cli.test-d.ts +12 -12
- package/src/Cli.test.ts +446 -29
- package/src/Cli.ts +146 -82
- package/src/Mcp.test.ts +6 -6
- package/src/SyncSkills.ts +26 -9
- package/src/bin.ts +4 -4
- package/src/e2e.test.ts +93 -39
- package/src/internal/agents.ts +32 -2
package/src/Cli.test.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Cli, Errors, z } from 'incur'
|
|
2
2
|
|
|
3
|
+
const originalIsTTY = process.stdout.isTTY
|
|
4
|
+
beforeAll(() => {
|
|
5
|
+
;(process.stdout as any).isTTY = false
|
|
6
|
+
})
|
|
7
|
+
afterAll(() => {
|
|
8
|
+
;(process.stdout as any).isTTY = originalIsTTY
|
|
9
|
+
})
|
|
10
|
+
|
|
3
11
|
let __mockSkillsHash: string | undefined
|
|
4
12
|
|
|
5
13
|
vi.mock('./SyncSkills.js', async (importOriginal) => {
|
|
@@ -46,8 +54,8 @@ describe('command', () => {
|
|
|
46
54
|
const cli = Cli.create('test')
|
|
47
55
|
const result = cli.command('greet', {
|
|
48
56
|
args: z.object({ name: z.string() }),
|
|
49
|
-
run(
|
|
50
|
-
return { message: `hello ${args.name}` }
|
|
57
|
+
run(c) {
|
|
58
|
+
return { message: `hello ${c.args.name}` }
|
|
51
59
|
},
|
|
52
60
|
})
|
|
53
61
|
expect(result).toBe(cli)
|
|
@@ -59,8 +67,8 @@ describe('serve', () => {
|
|
|
59
67
|
const cli = Cli.create('test')
|
|
60
68
|
cli.command('greet', {
|
|
61
69
|
args: z.object({ name: z.string() }),
|
|
62
|
-
run(
|
|
63
|
-
return { message: `hello ${args.name}` }
|
|
70
|
+
run(c) {
|
|
71
|
+
return { message: `hello ${c.args.name}` }
|
|
64
72
|
},
|
|
65
73
|
})
|
|
66
74
|
|
|
@@ -75,8 +83,8 @@ describe('serve', () => {
|
|
|
75
83
|
const cli = Cli.create('test')
|
|
76
84
|
cli.command('greet', {
|
|
77
85
|
args: z.object({ name: z.string() }),
|
|
78
|
-
run(
|
|
79
|
-
return { message: `hello ${args.name}` }
|
|
86
|
+
run(c) {
|
|
87
|
+
return { message: `hello ${c.args.name}` }
|
|
80
88
|
},
|
|
81
89
|
})
|
|
82
90
|
|
|
@@ -97,8 +105,8 @@ describe('serve', () => {
|
|
|
97
105
|
let receivedArgs: any
|
|
98
106
|
cli.command('add', {
|
|
99
107
|
args: z.object({ a: z.string(), b: z.string() }),
|
|
100
|
-
run(
|
|
101
|
-
receivedArgs = args
|
|
108
|
+
run(c) {
|
|
109
|
+
receivedArgs = c.args
|
|
102
110
|
return {}
|
|
103
111
|
},
|
|
104
112
|
})
|
|
@@ -128,6 +136,20 @@ describe('serve', () => {
|
|
|
128
136
|
|
|
129
137
|
const { output, exitCode } = await serve(cli, ['nonexistent'])
|
|
130
138
|
expect(exitCode).toBe(1)
|
|
139
|
+
expect(output).toMatchInlineSnapshot(`
|
|
140
|
+
"code: COMMAND_NOT_FOUND
|
|
141
|
+
message: 'nonexistent' is not a command. See 'test --help' for a list of available commands.
|
|
142
|
+
"
|
|
143
|
+
`)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('outputs human error for unknown command in TTY', async () => {
|
|
147
|
+
;(process.stdout as any).isTTY = true
|
|
148
|
+
const cli = Cli.create('test')
|
|
149
|
+
|
|
150
|
+
const { output, exitCode } = await serve(cli, ['nonexistent'])
|
|
151
|
+
;(process.stdout as any).isTTY = false
|
|
152
|
+
expect(exitCode).toBe(1)
|
|
131
153
|
expect(output).toMatchInlineSnapshot(`
|
|
132
154
|
"Error: 'nonexistent' is not a command. See 'test --help' for a list of available commands.
|
|
133
155
|
"
|
|
@@ -161,6 +183,25 @@ describe('serve', () => {
|
|
|
161
183
|
|
|
162
184
|
const { output, exitCode } = await serve(cli, ['fail'])
|
|
163
185
|
expect(exitCode).toBe(1)
|
|
186
|
+
expect(output).toMatchInlineSnapshot(`
|
|
187
|
+
"code: UNKNOWN
|
|
188
|
+
message: boom
|
|
189
|
+
"
|
|
190
|
+
`)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('wraps handler errors in human format in TTY', async () => {
|
|
194
|
+
;(process.stdout as any).isTTY = true
|
|
195
|
+
const cli = Cli.create('test')
|
|
196
|
+
cli.command('fail', {
|
|
197
|
+
run() {
|
|
198
|
+
throw new Error('boom')
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
203
|
+
;(process.stdout as any).isTTY = false
|
|
204
|
+
expect(exitCode).toBe(1)
|
|
164
205
|
expect(output).toMatchInlineSnapshot(`
|
|
165
206
|
"Error: boom
|
|
166
207
|
"
|
|
@@ -181,6 +222,30 @@ describe('serve', () => {
|
|
|
181
222
|
|
|
182
223
|
const { output, exitCode } = await serve(cli, ['fail'])
|
|
183
224
|
expect(exitCode).toBe(1)
|
|
225
|
+
expect(output).toMatchInlineSnapshot(`
|
|
226
|
+
"code: NOT_AUTHENTICATED
|
|
227
|
+
message: Token not found
|
|
228
|
+
retryable: false
|
|
229
|
+
"
|
|
230
|
+
`)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('IncurError shows human format in TTY', async () => {
|
|
234
|
+
;(process.stdout as any).isTTY = true
|
|
235
|
+
const cli = Cli.create('test')
|
|
236
|
+
cli.command('fail', {
|
|
237
|
+
run() {
|
|
238
|
+
throw new Errors.IncurError({
|
|
239
|
+
code: 'NOT_AUTHENTICATED',
|
|
240
|
+
message: 'Token not found',
|
|
241
|
+
retryable: false,
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const { output, exitCode } = await serve(cli, ['fail'])
|
|
247
|
+
;(process.stdout as any).isTTY = false
|
|
248
|
+
expect(exitCode).toBe(1)
|
|
184
249
|
expect(output).toMatchInlineSnapshot(`
|
|
185
250
|
"Error (NOT_AUTHENTICATED): Token not found
|
|
186
251
|
"
|
|
@@ -191,16 +256,62 @@ describe('serve', () => {
|
|
|
191
256
|
const cli = Cli.create('test')
|
|
192
257
|
cli.command('greet', {
|
|
193
258
|
args: z.object({ name: z.string() }),
|
|
194
|
-
run(
|
|
195
|
-
return { message: `hello ${args.name}` }
|
|
259
|
+
run(c) {
|
|
260
|
+
return { message: `hello ${c.args.name}` }
|
|
196
261
|
},
|
|
197
262
|
})
|
|
198
263
|
|
|
199
264
|
const { output, exitCode } = await serve(cli, ['greet'])
|
|
200
265
|
expect(exitCode).toBe(1)
|
|
266
|
+
expect(output).toContain('VALIDATION_ERROR')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('ValidationError shows human format in TTY', async () => {
|
|
270
|
+
;(process.stdout as any).isTTY = true
|
|
271
|
+
const cli = Cli.create('test')
|
|
272
|
+
cli.command('greet', {
|
|
273
|
+
args: z.object({ name: z.string() }),
|
|
274
|
+
run(c) {
|
|
275
|
+
return { message: `hello ${c.args.name}` }
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const { output, exitCode } = await serve(cli, ['greet'])
|
|
280
|
+
;(process.stdout as any).isTTY = false
|
|
281
|
+
expect(exitCode).toBe(1)
|
|
201
282
|
expect(output).toContain('Error: missing required argument <name>')
|
|
202
283
|
})
|
|
203
284
|
|
|
285
|
+
test('agent is true when not TTY', async () => {
|
|
286
|
+
let agent: boolean | undefined
|
|
287
|
+
const cli = Cli.create('test')
|
|
288
|
+
cli.command('ping', {
|
|
289
|
+
run(c) {
|
|
290
|
+
agent = c.agent
|
|
291
|
+
return {}
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
await serve(cli, ['ping'])
|
|
296
|
+
expect(agent).toBe(true)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('agent is false when TTY', async () => {
|
|
300
|
+
;(process.stdout as any).isTTY = true
|
|
301
|
+
let agent: boolean | undefined
|
|
302
|
+
const cli = Cli.create('test')
|
|
303
|
+
cli.command('ping', {
|
|
304
|
+
run(c) {
|
|
305
|
+
agent = c.agent
|
|
306
|
+
return {}
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
await serve(cli, ['ping'])
|
|
311
|
+
;(process.stdout as any).isTTY = false
|
|
312
|
+
expect(agent).toBe(false)
|
|
313
|
+
})
|
|
314
|
+
|
|
204
315
|
test('supports async handlers', async () => {
|
|
205
316
|
const cli = Cli.create('test')
|
|
206
317
|
cli.command('async', {
|
|
@@ -567,6 +678,24 @@ describe('subcommands', () => {
|
|
|
567
678
|
|
|
568
679
|
const { output, exitCode } = await serve(cli, ['pr', 'unknown'])
|
|
569
680
|
expect(exitCode).toBe(1)
|
|
681
|
+
expect(output).toMatchInlineSnapshot(`
|
|
682
|
+
"code: COMMAND_NOT_FOUND
|
|
683
|
+
message: 'unknown' is not a command. See 'test pr --help' for a list of available commands.
|
|
684
|
+
"
|
|
685
|
+
`)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
test('unknown subcommand shows human error in TTY', async () => {
|
|
689
|
+
;(process.stdout as any).isTTY = true
|
|
690
|
+
const cli = Cli.create('test')
|
|
691
|
+
const pr = Cli.create('pr', { description: 'PR management' })
|
|
692
|
+
.command('list', { run: () => ({}) })
|
|
693
|
+
.command('create', { run: () => ({}) })
|
|
694
|
+
cli.command(pr)
|
|
695
|
+
|
|
696
|
+
const { output, exitCode } = await serve(cli, ['pr', 'unknown'])
|
|
697
|
+
;(process.stdout as any).isTTY = false
|
|
698
|
+
expect(exitCode).toBe(1)
|
|
570
699
|
expect(output).toMatchInlineSnapshot(`
|
|
571
700
|
"Error: 'unknown' is not a command. See 'test pr --help' for a list of available commands.
|
|
572
701
|
"
|
|
@@ -628,6 +757,26 @@ describe('subcommands', () => {
|
|
|
628
757
|
|
|
629
758
|
const { output, exitCode } = await serve(cli, ['pr', 'fail'])
|
|
630
759
|
expect(exitCode).toBe(1)
|
|
760
|
+
expect(output).toMatchInlineSnapshot(`
|
|
761
|
+
"code: UNKNOWN
|
|
762
|
+
message: sub-boom
|
|
763
|
+
"
|
|
764
|
+
`)
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
test('error in sub-command shows human format in TTY', async () => {
|
|
768
|
+
;(process.stdout as any).isTTY = true
|
|
769
|
+
const cli = Cli.create('test')
|
|
770
|
+
const pr = Cli.create('pr', { description: 'PR management' }).command('fail', {
|
|
771
|
+
run() {
|
|
772
|
+
throw new Error('sub-boom')
|
|
773
|
+
},
|
|
774
|
+
})
|
|
775
|
+
cli.command(pr)
|
|
776
|
+
|
|
777
|
+
const { output, exitCode } = await serve(cli, ['pr', 'fail'])
|
|
778
|
+
;(process.stdout as any).isTTY = false
|
|
779
|
+
expect(exitCode).toBe(1)
|
|
631
780
|
expect(output).toMatchInlineSnapshot(`
|
|
632
781
|
"Error: sub-boom
|
|
633
782
|
"
|
|
@@ -653,8 +802,8 @@ describe('cta', () => {
|
|
|
653
802
|
test('string shorthand for cta commands', async () => {
|
|
654
803
|
const cli = Cli.create('test')
|
|
655
804
|
cli.command('list', {
|
|
656
|
-
run(
|
|
657
|
-
return ok({ items: [] }, { cta: { commands: ['get 1', 'get 2'] } })
|
|
805
|
+
run(c) {
|
|
806
|
+
return c.ok({ items: [] }, { cta: { commands: ['get 1', 'get 2'] } })
|
|
658
807
|
},
|
|
659
808
|
})
|
|
660
809
|
|
|
@@ -669,8 +818,8 @@ describe('cta', () => {
|
|
|
669
818
|
test('tuple shorthand with description', async () => {
|
|
670
819
|
const cli = Cli.create('test')
|
|
671
820
|
cli.command('list', {
|
|
672
|
-
run(
|
|
673
|
-
return ok(
|
|
821
|
+
run(c) {
|
|
822
|
+
return c.ok(
|
|
674
823
|
{ items: [] },
|
|
675
824
|
{
|
|
676
825
|
cta: { commands: [{ command: 'get 1', description: 'View item 1' }] },
|
|
@@ -689,8 +838,8 @@ describe('cta', () => {
|
|
|
689
838
|
test('tuple form with args/options', async () => {
|
|
690
839
|
const cli = Cli.create('test')
|
|
691
840
|
cli.command('create', {
|
|
692
|
-
run(
|
|
693
|
-
return ok(
|
|
841
|
+
run(c) {
|
|
842
|
+
return c.ok(
|
|
694
843
|
{ id: 1 },
|
|
695
844
|
{
|
|
696
845
|
cta: {
|
|
@@ -718,8 +867,8 @@ describe('cta', () => {
|
|
|
718
867
|
test('tuple form boolean args format as placeholders', async () => {
|
|
719
868
|
const cli = Cli.create('test')
|
|
720
869
|
cli.command('list', {
|
|
721
|
-
run(
|
|
722
|
-
return ok(
|
|
870
|
+
run(c) {
|
|
871
|
+
return c.ok(
|
|
723
872
|
{ items: [] },
|
|
724
873
|
{
|
|
725
874
|
cta: { commands: [{ command: 'get', args: { id: true }, options: { format: true } }] },
|
|
@@ -736,8 +885,8 @@ describe('cta', () => {
|
|
|
736
885
|
test('custom cta description', async () => {
|
|
737
886
|
const cli = Cli.create('test')
|
|
738
887
|
cli.command('create', {
|
|
739
|
-
run(
|
|
740
|
-
return ok(
|
|
888
|
+
run(c) {
|
|
889
|
+
return c.ok(
|
|
741
890
|
{ id: 1 },
|
|
742
891
|
{
|
|
743
892
|
cta: { description: 'View the created item:', commands: ['get 1'] },
|
|
@@ -910,6 +1059,23 @@ describe('leaf cli', () => {
|
|
|
910
1059
|
})
|
|
911
1060
|
const { output, exitCode } = await serve(cli, [])
|
|
912
1061
|
expect(exitCode).toBe(1)
|
|
1062
|
+
expect(output).toMatchInlineSnapshot(`
|
|
1063
|
+
"code: UNKNOWN
|
|
1064
|
+
message: boom
|
|
1065
|
+
"
|
|
1066
|
+
`)
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
test('errors show human format in TTY', async () => {
|
|
1070
|
+
;(process.stdout as any).isTTY = true
|
|
1071
|
+
const cli = Cli.create('fail', {
|
|
1072
|
+
run() {
|
|
1073
|
+
throw new Error('boom')
|
|
1074
|
+
},
|
|
1075
|
+
})
|
|
1076
|
+
const { output, exitCode } = await serve(cli, [])
|
|
1077
|
+
;(process.stdout as any).isTTY = false
|
|
1078
|
+
expect(exitCode).toBe(1)
|
|
913
1079
|
expect(output).toMatchInlineSnapshot(`
|
|
914
1080
|
"Error: boom
|
|
915
1081
|
"
|
|
@@ -1172,8 +1338,8 @@ describe('env', () => {
|
|
|
1172
1338
|
env: z.object({
|
|
1173
1339
|
API_TOKEN: z.string().describe('Auth token'),
|
|
1174
1340
|
}),
|
|
1175
|
-
run(
|
|
1176
|
-
receivedEnv = env
|
|
1341
|
+
run(c) {
|
|
1342
|
+
receivedEnv = c.env
|
|
1177
1343
|
return { ok: true }
|
|
1178
1344
|
},
|
|
1179
1345
|
})
|
|
@@ -1205,8 +1371,8 @@ describe('env', () => {
|
|
|
1205
1371
|
env: z.object({
|
|
1206
1372
|
API_URL: z.string().default('https://api.example.com').describe('API URL'),
|
|
1207
1373
|
}),
|
|
1208
|
-
run(
|
|
1209
|
-
receivedEnv = env
|
|
1374
|
+
run(c) {
|
|
1375
|
+
receivedEnv = c.env
|
|
1210
1376
|
return { ok: true }
|
|
1211
1377
|
},
|
|
1212
1378
|
})
|
|
@@ -1300,8 +1466,8 @@ describe('env', () => {
|
|
|
1300
1466
|
DEBUG: z.boolean().default(false).describe('Debug mode'),
|
|
1301
1467
|
PORT: z.number().default(3000).describe('Port'),
|
|
1302
1468
|
}),
|
|
1303
|
-
run(
|
|
1304
|
-
receivedEnv = env
|
|
1469
|
+
run(c) {
|
|
1470
|
+
receivedEnv = c.env
|
|
1305
1471
|
return { ok: true }
|
|
1306
1472
|
},
|
|
1307
1473
|
})
|
|
@@ -1329,9 +1495,7 @@ describe('skills staleness', () => {
|
|
|
1329
1495
|
cli.command('ping', { description: 'Health check', run: () => ({ pong: true }) })
|
|
1330
1496
|
|
|
1331
1497
|
await serve(cli, ['ping'])
|
|
1332
|
-
expect(stderrSpy).toHaveBeenCalledWith(
|
|
1333
|
-
expect.stringContaining("Skills are out of date. Run 'pnpx test skills add' to update."),
|
|
1334
|
-
)
|
|
1498
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("Skills are out of date. Run '"))
|
|
1335
1499
|
})
|
|
1336
1500
|
|
|
1337
1501
|
test('does not warn when hash matches', async () => {
|
|
@@ -1371,3 +1535,256 @@ describe('skills staleness', () => {
|
|
|
1371
1535
|
expect(stderrSpy).not.toHaveBeenCalled()
|
|
1372
1536
|
})
|
|
1373
1537
|
})
|
|
1538
|
+
|
|
1539
|
+
describe('outputPolicy', () => {
|
|
1540
|
+
beforeEach(() => {
|
|
1541
|
+
;(process.stdout as any).isTTY = true
|
|
1542
|
+
})
|
|
1543
|
+
afterEach(() => {
|
|
1544
|
+
;(process.stdout as any).isTTY = false
|
|
1545
|
+
})
|
|
1546
|
+
|
|
1547
|
+
test('default (all): displays data in human mode', async () => {
|
|
1548
|
+
const cli = Cli.create('test')
|
|
1549
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1550
|
+
|
|
1551
|
+
const { output } = await serve(cli, ['ping'])
|
|
1552
|
+
expect(output).toContain('pong: true')
|
|
1553
|
+
})
|
|
1554
|
+
|
|
1555
|
+
test('agent-only on command: suppresses data in human mode', async () => {
|
|
1556
|
+
const cli = Cli.create('test')
|
|
1557
|
+
cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
|
|
1558
|
+
|
|
1559
|
+
const { output } = await serve(cli, ['ping'])
|
|
1560
|
+
expect(output).toBe('')
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1563
|
+
test('agent-only on command: still outputs in agent mode (--json)', async () => {
|
|
1564
|
+
const cli = Cli.create('test')
|
|
1565
|
+
cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
|
|
1566
|
+
|
|
1567
|
+
const { output } = await serve(cli, ['ping', '--json'])
|
|
1568
|
+
expect(output).toContain('"pong"')
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
test('agent-only on root CLI: inherited by commands', async () => {
|
|
1572
|
+
const cli = Cli.create('test', { outputPolicy: 'agent-only' })
|
|
1573
|
+
cli.command('ping', { run: () => ({ pong: true }) })
|
|
1574
|
+
|
|
1575
|
+
const { output } = await serve(cli, ['ping'])
|
|
1576
|
+
expect(output).toBe('')
|
|
1577
|
+
})
|
|
1578
|
+
|
|
1579
|
+
test('agent-only on group: inherited by child commands', async () => {
|
|
1580
|
+
const cli = Cli.create('test')
|
|
1581
|
+
const sub = Cli.create('sub', { outputPolicy: 'agent-only' })
|
|
1582
|
+
sub.command('ping', { run: () => ({ pong: true }) })
|
|
1583
|
+
cli.command(sub)
|
|
1584
|
+
|
|
1585
|
+
const { output } = await serve(cli, ['sub', 'ping'])
|
|
1586
|
+
expect(output).toBe('')
|
|
1587
|
+
})
|
|
1588
|
+
|
|
1589
|
+
test('command overrides group outputPolicy', async () => {
|
|
1590
|
+
const cli = Cli.create('test')
|
|
1591
|
+
const sub = Cli.create('sub', { outputPolicy: 'agent-only' })
|
|
1592
|
+
sub.command('ping', { outputPolicy: 'all', run: () => ({ pong: true }) })
|
|
1593
|
+
cli.command(sub)
|
|
1594
|
+
|
|
1595
|
+
const { output } = await serve(cli, ['sub', 'ping'])
|
|
1596
|
+
expect(output).toContain('pong: true')
|
|
1597
|
+
})
|
|
1598
|
+
|
|
1599
|
+
test('agent-only suppresses streaming chunks in human mode', async () => {
|
|
1600
|
+
const cli = Cli.create('test')
|
|
1601
|
+
cli.command('stream', {
|
|
1602
|
+
outputPolicy: 'agent-only',
|
|
1603
|
+
async *run() {
|
|
1604
|
+
yield { step: 1 }
|
|
1605
|
+
yield { step: 2 }
|
|
1606
|
+
},
|
|
1607
|
+
})
|
|
1608
|
+
|
|
1609
|
+
const { output } = await serve(cli, ['stream'])
|
|
1610
|
+
expect(output).toBe('')
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
test('agent-only still shows errors in human mode', async () => {
|
|
1614
|
+
const cli = Cli.create('test')
|
|
1615
|
+
cli.command('fail', {
|
|
1616
|
+
outputPolicy: 'agent-only',
|
|
1617
|
+
run(c) {
|
|
1618
|
+
return c.error({ code: 'FAILED', message: 'something broke' })
|
|
1619
|
+
},
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
const { output } = await serve(cli, ['fail'])
|
|
1623
|
+
expect(output).toContain('Error (FAILED): something broke')
|
|
1624
|
+
})
|
|
1625
|
+
|
|
1626
|
+
test('agent-only still shows CTAs in human mode', async () => {
|
|
1627
|
+
const cli = Cli.create('test')
|
|
1628
|
+
cli.command('ping', {
|
|
1629
|
+
outputPolicy: 'agent-only',
|
|
1630
|
+
run(c) {
|
|
1631
|
+
return c.ok({ pong: true }, { cta: { commands: ['ping'] } })
|
|
1632
|
+
},
|
|
1633
|
+
})
|
|
1634
|
+
|
|
1635
|
+
const { output } = await serve(cli, ['ping'])
|
|
1636
|
+
expect(output).not.toContain('pong')
|
|
1637
|
+
expect(output).toContain('ping')
|
|
1638
|
+
})
|
|
1639
|
+
|
|
1640
|
+
test('agent-only suppresses data when TTY', async () => {
|
|
1641
|
+
;(process.stdout as any).isTTY = true
|
|
1642
|
+
const cli = Cli.create('test')
|
|
1643
|
+
cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
|
|
1644
|
+
|
|
1645
|
+
const { output } = await serve(cli, ['ping'])
|
|
1646
|
+
expect(output).toBe('')
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
test('agent-only still displays data when not TTY (piped)', async () => {
|
|
1650
|
+
;(process.stdout as any).isTTY = false
|
|
1651
|
+
const cli = Cli.create('test')
|
|
1652
|
+
cli.command('ping', { outputPolicy: 'agent-only', run: () => ({ pong: true }) })
|
|
1653
|
+
|
|
1654
|
+
const { output } = await serve(cli, ['ping'])
|
|
1655
|
+
expect(output).toContain('pong')
|
|
1656
|
+
})
|
|
1657
|
+
|
|
1658
|
+
test('all displays data regardless of TTY', async () => {
|
|
1659
|
+
const cli = Cli.create('test')
|
|
1660
|
+
cli.command('ping', { outputPolicy: 'all', run: () => ({ pong: true }) })
|
|
1661
|
+
|
|
1662
|
+
;(process.stdout as any).isTTY = true
|
|
1663
|
+
const tty = await serve(cli, ['ping'])
|
|
1664
|
+
expect(tty.output).toContain('pong: true')
|
|
1665
|
+
|
|
1666
|
+
;(process.stdout as any).isTTY = false
|
|
1667
|
+
const piped = await serve(cli, ['ping'])
|
|
1668
|
+
expect(piped.output).toContain('pong')
|
|
1669
|
+
})
|
|
1670
|
+
|
|
1671
|
+
test('agent-only streaming suppresses when TTY, outputs when piped', async () => {
|
|
1672
|
+
const cli = Cli.create('test')
|
|
1673
|
+
cli.command('stream', {
|
|
1674
|
+
outputPolicy: 'agent-only',
|
|
1675
|
+
async *run() {
|
|
1676
|
+
yield { step: 1 }
|
|
1677
|
+
},
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
;(process.stdout as any).isTTY = true
|
|
1681
|
+
const tty = await serve(cli, ['stream'])
|
|
1682
|
+
expect(tty.output).toBe('')
|
|
1683
|
+
|
|
1684
|
+
;(process.stdout as any).isTTY = false
|
|
1685
|
+
const piped = await serve(cli, ['stream'])
|
|
1686
|
+
expect(piped.output).toContain('step: 1')
|
|
1687
|
+
})
|
|
1688
|
+
|
|
1689
|
+
test('e2e: realistic multi-level CLI with mixed policies', async () => {
|
|
1690
|
+
const cli = Cli.create('tool', { description: 'A deployment tool' })
|
|
1691
|
+
|
|
1692
|
+
// Top-level command with agent-only
|
|
1693
|
+
cli.command('deploy', {
|
|
1694
|
+
outputPolicy: 'agent-only',
|
|
1695
|
+
args: z.object({ env: z.enum(['staging', 'production']) }),
|
|
1696
|
+
run(c) {
|
|
1697
|
+
return c.ok(
|
|
1698
|
+
{ id: 'deploy-123', url: `https://${c.args.env}.example.com` },
|
|
1699
|
+
{ cta: { commands: [{ command: 'status', description: 'Check status' }] } },
|
|
1700
|
+
)
|
|
1701
|
+
},
|
|
1702
|
+
})
|
|
1703
|
+
|
|
1704
|
+
// Group with inherited agent-only
|
|
1705
|
+
const internal = Cli.create('internal', {
|
|
1706
|
+
description: 'Internal commands',
|
|
1707
|
+
outputPolicy: 'agent-only',
|
|
1708
|
+
})
|
|
1709
|
+
internal.command('sync', { run: () => ({ synced: 42, duration: '1.2s' }) })
|
|
1710
|
+
internal.command('healthcheck', {
|
|
1711
|
+
outputPolicy: 'all',
|
|
1712
|
+
run: () => ({ healthy: true }),
|
|
1713
|
+
})
|
|
1714
|
+
|
|
1715
|
+
// Group without policy — children default to 'all'
|
|
1716
|
+
const db = Cli.create('db', { description: 'Database commands' })
|
|
1717
|
+
db.command('migrate', { run: () => ({ migrated: 3 }) })
|
|
1718
|
+
|
|
1719
|
+
cli.command(internal)
|
|
1720
|
+
cli.command(db)
|
|
1721
|
+
|
|
1722
|
+
// deploy: agent-only suppresses data, shows CTA
|
|
1723
|
+
const deploy = await serve(cli, ['deploy', 'staging'])
|
|
1724
|
+
expect(deploy.output).not.toContain('deploy-123')
|
|
1725
|
+
expect(deploy.output).toContain('Check status')
|
|
1726
|
+
|
|
1727
|
+
// deploy --verbose: agent mode shows everything
|
|
1728
|
+
const deployVerbose = await serve(cli, ['deploy', 'staging', '--verbose'])
|
|
1729
|
+
expect(deployVerbose.output).toContain('deploy-123')
|
|
1730
|
+
expect(deployVerbose.output).toContain('staging.example.com')
|
|
1731
|
+
|
|
1732
|
+
// deploy --json: agent mode shows data
|
|
1733
|
+
const deployJson = await serve(cli, ['deploy', 'staging', '--json'])
|
|
1734
|
+
expect(deployJson.output).toContain('deploy-123')
|
|
1735
|
+
|
|
1736
|
+
// internal sync: inherits agent-only from group
|
|
1737
|
+
const sync = await serve(cli, ['internal', 'sync'])
|
|
1738
|
+
expect(sync.output).toBe('')
|
|
1739
|
+
|
|
1740
|
+
// internal sync --json: agent mode works
|
|
1741
|
+
const syncJson = await serve(cli, ['internal', 'sync', '--json'])
|
|
1742
|
+
expect(syncJson.output).toContain('42')
|
|
1743
|
+
|
|
1744
|
+
// internal healthcheck: overrides to 'all'
|
|
1745
|
+
const health = await serve(cli, ['internal', 'healthcheck'])
|
|
1746
|
+
expect(health.output).toContain('healthy: true')
|
|
1747
|
+
|
|
1748
|
+
// db migrate: no policy, defaults to 'all'
|
|
1749
|
+
const migrate = await serve(cli, ['db', 'migrate'])
|
|
1750
|
+
expect(migrate.output).toContain('migrated: 3')
|
|
1751
|
+
})
|
|
1752
|
+
|
|
1753
|
+
test('e2e: agent-only with streaming and error in nested group', async () => {
|
|
1754
|
+
const cli = Cli.create('tool')
|
|
1755
|
+
const ops = Cli.create('ops', {
|
|
1756
|
+
description: 'Operations',
|
|
1757
|
+
outputPolicy: 'agent-only',
|
|
1758
|
+
})
|
|
1759
|
+
|
|
1760
|
+
ops.command('logs', {
|
|
1761
|
+
async *run() {
|
|
1762
|
+
yield { line: 'Starting...' }
|
|
1763
|
+
yield { line: 'Processing...' }
|
|
1764
|
+
yield { line: 'Done.' }
|
|
1765
|
+
},
|
|
1766
|
+
})
|
|
1767
|
+
|
|
1768
|
+
ops.command('restart', {
|
|
1769
|
+
run(c) {
|
|
1770
|
+
return c.error({ code: 'PERMISSION_DENIED', message: 'Requires admin role' })
|
|
1771
|
+
},
|
|
1772
|
+
})
|
|
1773
|
+
|
|
1774
|
+
cli.command(ops)
|
|
1775
|
+
|
|
1776
|
+
// Streaming: agent-only suppresses chunks in human mode
|
|
1777
|
+
const logs = await serve(cli, ['ops', 'logs'])
|
|
1778
|
+
expect(logs.output).toBe('')
|
|
1779
|
+
|
|
1780
|
+
// Streaming: --format jsonl still works
|
|
1781
|
+
const logsJsonl = await serve(cli, ['ops', 'logs', '--format', 'jsonl'])
|
|
1782
|
+
expect(logsJsonl.output).toContain('"type":"chunk"')
|
|
1783
|
+
expect(logsJsonl.output).toContain('Starting...')
|
|
1784
|
+
|
|
1785
|
+
// Errors still display in human mode despite agent-only
|
|
1786
|
+
const restart = await serve(cli, ['ops', 'restart'])
|
|
1787
|
+
expect(restart.output).toContain('Error (PERMISSION_DENIED): Requires admin role')
|
|
1788
|
+
expect(restart.exitCode).toBe(1)
|
|
1789
|
+
})
|
|
1790
|
+
})
|