@pikku/core 0.12.20 → 0.12.22
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/CHANGELOG.md +17 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/handle-error.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/handle-error.test.ts +1 -1
- package/src/handle-error.ts +4 -1
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,7 +2,13 @@ import { test, describe, beforeEach, afterEach } from 'node:test'
|
|
|
2
2
|
import * as assert from 'assert'
|
|
3
3
|
import { NotFoundError } from '../../errors/errors.js'
|
|
4
4
|
import type { CorePikkuMiddleware } from '../../types/core.types.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CLIError,
|
|
7
|
+
executeCLI,
|
|
8
|
+
wireCLI,
|
|
9
|
+
runCLICommand,
|
|
10
|
+
pikkuCLIRender,
|
|
11
|
+
} from './cli-runner.js'
|
|
6
12
|
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
7
13
|
import { addFunction } from '../../function/function-runner.js'
|
|
8
14
|
|
|
@@ -49,7 +55,14 @@ describe('CLI Runner', () => {
|
|
|
49
55
|
programs: {
|
|
50
56
|
'test-cli': {
|
|
51
57
|
program: 'test-cli',
|
|
52
|
-
commands: {
|
|
58
|
+
commands: {
|
|
59
|
+
greet: {
|
|
60
|
+
command: 'greet <name>',
|
|
61
|
+
pikkuFuncId: 'greetFunc',
|
|
62
|
+
positionals: [{ name: 'name', required: true }],
|
|
63
|
+
options: {},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
53
66
|
options: {},
|
|
54
67
|
},
|
|
55
68
|
},
|
|
@@ -401,4 +414,244 @@ describe('CLI Runner', () => {
|
|
|
401
414
|
assert.deepStrictEqual(receivedData, { test: 'value' })
|
|
402
415
|
})
|
|
403
416
|
})
|
|
417
|
+
|
|
418
|
+
describe('executeCLI', () => {
|
|
419
|
+
test('should require args to be provided', async () => {
|
|
420
|
+
await assert.rejects(
|
|
421
|
+
() =>
|
|
422
|
+
executeCLI({
|
|
423
|
+
programName: 'test-cli',
|
|
424
|
+
createSingletonServices: async () => singletonServices,
|
|
425
|
+
}),
|
|
426
|
+
/CLI arguments are required/
|
|
427
|
+
)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('should throw CLIError when the program is missing from cli metadata', async () => {
|
|
431
|
+
await assert.rejects(
|
|
432
|
+
() =>
|
|
433
|
+
executeCLI({
|
|
434
|
+
programName: 'test-cli',
|
|
435
|
+
args: [],
|
|
436
|
+
createSingletonServices: async () => singletonServices,
|
|
437
|
+
}),
|
|
438
|
+
/CLI program "test-cli" not found/
|
|
439
|
+
)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('should print help and return when --help is requested', async () => {
|
|
443
|
+
const logs: string[] = []
|
|
444
|
+
const originalLog = console.log
|
|
445
|
+
console.log = (message?: any) => {
|
|
446
|
+
logs.push(String(message))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
pikkuState(null, 'cli', 'meta', {
|
|
450
|
+
programs: {
|
|
451
|
+
'test-cli': {
|
|
452
|
+
program: 'test-cli',
|
|
453
|
+
description: 'Test CLI',
|
|
454
|
+
commands: {
|
|
455
|
+
greet: {
|
|
456
|
+
command: 'greet <name>',
|
|
457
|
+
description: 'Greets someone',
|
|
458
|
+
pikkuFuncId: 'greetFunc',
|
|
459
|
+
positionals: [{ name: 'name', required: true }],
|
|
460
|
+
options: {},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
options: {},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
renderers: {},
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
await executeCLI({
|
|
471
|
+
programName: 'test-cli',
|
|
472
|
+
args: ['--help'],
|
|
473
|
+
createSingletonServices: async () => singletonServices,
|
|
474
|
+
})
|
|
475
|
+
} finally {
|
|
476
|
+
console.log = originalLog
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
assert.ok(logs[0]?.includes('test-cli'))
|
|
480
|
+
assert.ok(logs[0]?.includes('greet'))
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
test('should show help and throw CLIError for unknown commands', async () => {
|
|
484
|
+
const logs: string[] = []
|
|
485
|
+
const originalLog = console.log
|
|
486
|
+
console.log = (message?: any) => {
|
|
487
|
+
logs.push(String(message))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
pikkuState(null, 'cli', 'meta', {
|
|
491
|
+
programs: {
|
|
492
|
+
'test-cli': {
|
|
493
|
+
program: 'test-cli',
|
|
494
|
+
commands: {
|
|
495
|
+
greet: {
|
|
496
|
+
command: 'greet <name>',
|
|
497
|
+
description: 'Greets someone',
|
|
498
|
+
pikkuFuncId: 'greetFunc',
|
|
499
|
+
positionals: [{ name: 'name', required: true }],
|
|
500
|
+
options: {},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
options: {},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
renderers: {},
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
await assert.rejects(
|
|
511
|
+
() =>
|
|
512
|
+
executeCLI({
|
|
513
|
+
programName: 'test-cli',
|
|
514
|
+
args: ['unknown'],
|
|
515
|
+
createSingletonServices: async () => singletonServices,
|
|
516
|
+
}),
|
|
517
|
+
(error: any) => {
|
|
518
|
+
assert.ok(error instanceof CLIError)
|
|
519
|
+
assert.strictEqual(error.message, 'Unknown command')
|
|
520
|
+
assert.strictEqual(error.exitCode, 1)
|
|
521
|
+
return true
|
|
522
|
+
}
|
|
523
|
+
)
|
|
524
|
+
} finally {
|
|
525
|
+
console.log = originalLog
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
assert.ok(logs[0]?.includes('test-cli'))
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
test('should print parse errors and throw CLIError', async () => {
|
|
532
|
+
const errors: string[] = []
|
|
533
|
+
const originalError = console.error
|
|
534
|
+
console.error = (message?: any) => {
|
|
535
|
+
errors.push(String(message))
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
pikkuState(null, 'cli', 'meta', {
|
|
539
|
+
programs: {
|
|
540
|
+
'test-cli': {
|
|
541
|
+
program: 'test-cli',
|
|
542
|
+
commands: {
|
|
543
|
+
greet: {
|
|
544
|
+
command: 'greet <name>',
|
|
545
|
+
pikkuFuncId: 'greetFunc',
|
|
546
|
+
positionals: [{ name: 'name', required: true }],
|
|
547
|
+
options: {},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
options: {},
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
renderers: {},
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
await assert.rejects(
|
|
558
|
+
() =>
|
|
559
|
+
executeCLI({
|
|
560
|
+
programName: 'test-cli',
|
|
561
|
+
args: ['greet'],
|
|
562
|
+
createSingletonServices: async () => singletonServices,
|
|
563
|
+
}),
|
|
564
|
+
(error: any) => {
|
|
565
|
+
assert.ok(error instanceof CLIError)
|
|
566
|
+
assert.ok(error.message.includes('Missing required argument: name'))
|
|
567
|
+
return true
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
} finally {
|
|
571
|
+
console.error = originalError
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
assert.strictEqual(errors[0], 'Errors:')
|
|
575
|
+
assert.ok(errors[1]?.includes('Missing required argument: name'))
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
test('should create config and singleton services with parsed data and execute the command', async () => {
|
|
579
|
+
let receivedConfigData: any
|
|
580
|
+
let receivedConfig: any
|
|
581
|
+
let executed = false
|
|
582
|
+
|
|
583
|
+
pikkuState(null, 'cli', 'meta', {
|
|
584
|
+
programs: {
|
|
585
|
+
'test-cli': {
|
|
586
|
+
program: 'test-cli',
|
|
587
|
+
commands: {
|
|
588
|
+
greet: {
|
|
589
|
+
command: 'greet <name>',
|
|
590
|
+
pikkuFuncId: 'greetFunc',
|
|
591
|
+
positionals: [{ name: 'name', required: true }],
|
|
592
|
+
options: {
|
|
593
|
+
loud: {
|
|
594
|
+
description: 'Use loud greeting',
|
|
595
|
+
short: 'l',
|
|
596
|
+
default: false,
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
options: {},
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
renderers: {},
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
pikkuState(null, 'cli', 'programs', {
|
|
608
|
+
'test-cli': {
|
|
609
|
+
defaultRenderer: undefined,
|
|
610
|
+
middleware: [],
|
|
611
|
+
renderers: {},
|
|
612
|
+
},
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
pikkuState(null, 'function', 'meta', {
|
|
616
|
+
greetFunc: {
|
|
617
|
+
pikkuFuncId: 'greetFunc',
|
|
618
|
+
inputSchemaName: null,
|
|
619
|
+
outputSchemaName: null,
|
|
620
|
+
sessionless: true,
|
|
621
|
+
},
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
addFunction('greetFunc', {
|
|
625
|
+
func: async () => {
|
|
626
|
+
executed = true
|
|
627
|
+
return undefined
|
|
628
|
+
},
|
|
629
|
+
auth: false,
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
await executeCLI({
|
|
633
|
+
programName: 'test-cli',
|
|
634
|
+
args: ['greet', 'Alice', '--loud'],
|
|
635
|
+
createConfig: async (_variables, data) => {
|
|
636
|
+
receivedConfigData = data
|
|
637
|
+
return { config: true }
|
|
638
|
+
},
|
|
639
|
+
createSingletonServices: async (config) => {
|
|
640
|
+
receivedConfig = config
|
|
641
|
+
return singletonServices
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
assert.deepStrictEqual(receivedConfigData, {
|
|
646
|
+
name: 'Alice',
|
|
647
|
+
loud: true,
|
|
648
|
+
})
|
|
649
|
+
assert.deepStrictEqual(receivedConfig, { config: true })
|
|
650
|
+
assert.strictEqual(executed, true)
|
|
651
|
+
assert.strictEqual(
|
|
652
|
+
pikkuState(null, 'package', 'singletonServices'),
|
|
653
|
+
singletonServices
|
|
654
|
+
)
|
|
655
|
+
})
|
|
656
|
+
})
|
|
404
657
|
})
|
|
@@ -324,6 +324,7 @@ export async function runCLICommand({
|
|
|
324
324
|
programData?.renderers[commandId] || programData?.defaultRenderer
|
|
325
325
|
|
|
326
326
|
// Create a CLI channel for progressive output
|
|
327
|
+
let cliState: unknown
|
|
327
328
|
const channel: PikkuChannel<unknown, unknown> = {
|
|
328
329
|
channelId: `cli:${program}:${commandId}`,
|
|
329
330
|
openingData: pluckedData,
|
|
@@ -341,6 +342,13 @@ export async function runCLICommand({
|
|
|
341
342
|
}
|
|
342
343
|
},
|
|
343
344
|
state: 'open',
|
|
345
|
+
setState: (s) => {
|
|
346
|
+
cliState = s
|
|
347
|
+
},
|
|
348
|
+
getState: () => cliState as any,
|
|
349
|
+
clearState: () => {
|
|
350
|
+
cliState = undefined
|
|
351
|
+
},
|
|
344
352
|
}
|
|
345
353
|
|
|
346
354
|
const userSession = new PikkuSessionService<CoreUserSession>(
|
|
@@ -374,16 +382,28 @@ export async function runCLICommand({
|
|
|
374
382
|
packageName: currentCommand.packageName,
|
|
375
383
|
})
|
|
376
384
|
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
+
// Render the final result. `--json` / `--output json` routes the result
|
|
386
|
+
// through the global JSON renderer, but only for commands that opted into
|
|
387
|
+
// result-rendering by declaring a `render` — inline-printing commands (no
|
|
388
|
+
// per-command renderer) keep emitting their own output untouched.
|
|
389
|
+
// Skip if result is undefined (void functions handle their own output).
|
|
390
|
+
if (result !== undefined) {
|
|
391
|
+
const commandRenderer = programData?.renderers[commandId]
|
|
392
|
+
const jsonMode =
|
|
393
|
+
(data as any).json === true || (data as any).output === 'json'
|
|
394
|
+
const finalRenderer: CorePikkuCLIRender<any> | undefined =
|
|
395
|
+
jsonMode && commandRenderer !== undefined
|
|
396
|
+
? defaultJSONRenderer
|
|
397
|
+
: (commandRenderer ?? programData?.defaultRenderer)
|
|
398
|
+
if (finalRenderer !== undefined) {
|
|
399
|
+
await Promise.resolve(
|
|
400
|
+
finalRenderer(
|
|
401
|
+
singletonServices,
|
|
402
|
+
result,
|
|
403
|
+
userSession.get() as CoreUserSession | undefined
|
|
404
|
+
)
|
|
385
405
|
)
|
|
386
|
-
|
|
406
|
+
}
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
return result
|
|
@@ -476,7 +496,8 @@ export async function executeCLI({
|
|
|
476
496
|
const hasUnknownCommand = parsed.errors.some(
|
|
477
497
|
(error) =>
|
|
478
498
|
error.startsWith('Unknown command:') ||
|
|
479
|
-
error.startsWith('Command not found:')
|
|
499
|
+
error.startsWith('Command not found:') ||
|
|
500
|
+
error.startsWith('Missing subcommand:')
|
|
480
501
|
)
|
|
481
502
|
|
|
482
503
|
if (hasUnknownCommand) {
|
|
@@ -256,10 +256,8 @@ export interface CoreCLICommand<
|
|
|
256
256
|
PikkuPermission extends CorePikkuPermission<any, any, any>,
|
|
257
257
|
PikkuMiddleware extends CorePikkuMiddleware,
|
|
258
258
|
Options = any,
|
|
259
|
-
Subcommands extends Record<
|
|
260
|
-
string,
|
|
261
|
-
CoreCLICommandConfig<any, any, any>
|
|
262
|
-
> = Record<string, CoreCLICommandConfig<any, any, any>>,
|
|
259
|
+
Subcommands extends Record<string, CoreCLICommandConfig<any, any, any>> =
|
|
260
|
+
Record<string, CoreCLICommandConfig<any, any, any>>,
|
|
263
261
|
> {
|
|
264
262
|
parameters?: string
|
|
265
263
|
func: PikkuFunctionConfig
|
|
@@ -301,10 +299,8 @@ export type CLICommandDefinition<
|
|
|
301
299
|
PikkuPermission extends CorePikkuPermission<any, any, any>,
|
|
302
300
|
PikkuMiddleware extends CorePikkuMiddleware,
|
|
303
301
|
Options = any,
|
|
304
|
-
Subcommands extends Record<
|
|
305
|
-
string,
|
|
306
|
-
CoreCLICommandConfig<any, any, any>
|
|
307
|
-
> = Record<string, CoreCLICommandConfig<any, any, any>>,
|
|
302
|
+
Subcommands extends Record<string, CoreCLICommandConfig<any, any, any>> =
|
|
303
|
+
Record<string, CoreCLICommandConfig<any, any, any>>,
|
|
308
304
|
> =
|
|
309
305
|
| CoreCLICommand<
|
|
310
306
|
In,
|
|
@@ -172,6 +172,23 @@ describe('Command Parser', () => {
|
|
|
172
172
|
})
|
|
173
173
|
})
|
|
174
174
|
|
|
175
|
+
test('should report missing subcommand for a group node with no func', () => {
|
|
176
|
+
const result = parseCLIArguments(['user'], 'test-cli', testMeta)
|
|
177
|
+
|
|
178
|
+
assert.deepStrictEqual(result.commandPath, ['user'])
|
|
179
|
+
assert.ok(
|
|
180
|
+
result.errors.some((error) => error.startsWith('Missing subcommand:')),
|
|
181
|
+
`expected a "Missing subcommand:" error, got: ${JSON.stringify(result.errors)}`
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('should not flag a runnable command as a missing subcommand', () => {
|
|
186
|
+
const result = parseCLIArguments(['greet', 'Alice'], 'test-cli', testMeta)
|
|
187
|
+
|
|
188
|
+
assert.deepStrictEqual(result.commandPath, ['greet'])
|
|
189
|
+
assert.strictEqual(result.errors.length, 0)
|
|
190
|
+
})
|
|
191
|
+
|
|
175
192
|
test('should parse variadic positionals', () => {
|
|
176
193
|
const result = parseCLIArguments(
|
|
177
194
|
['files', 'file1.txt', 'file2.txt', 'file3.txt'],
|
|
@@ -442,4 +459,70 @@ describe('Command Parser', () => {
|
|
|
442
459
|
assert.ok(help.includes('Unknown command'))
|
|
443
460
|
})
|
|
444
461
|
})
|
|
462
|
+
|
|
463
|
+
describe('camelCase ↔ kebab-case options', () => {
|
|
464
|
+
const kebabMeta: CLIMeta = {
|
|
465
|
+
programs: {
|
|
466
|
+
'test-cli': {
|
|
467
|
+
program: 'test-cli',
|
|
468
|
+
options: {},
|
|
469
|
+
commands: {
|
|
470
|
+
deploy: {
|
|
471
|
+
pikkuFuncId: 'deployFunc',
|
|
472
|
+
positionals: [],
|
|
473
|
+
options: {
|
|
474
|
+
autoApply: {
|
|
475
|
+
description: 'Deploy without the confirmation prompt',
|
|
476
|
+
default: false,
|
|
477
|
+
},
|
|
478
|
+
apiUrl: { description: 'Override the API URL' },
|
|
479
|
+
token: { description: 'Auth token', required: true },
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
renderers: {},
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
test('renders camelCase option keys as kebab-case in help', () => {
|
|
489
|
+
const help = generateCommandHelp('test-cli', kebabMeta, ['deploy'])
|
|
490
|
+
|
|
491
|
+
assert.ok(help.includes('--auto-apply'), 'expected --auto-apply')
|
|
492
|
+
assert.ok(help.includes('--api-url'), 'expected --api-url')
|
|
493
|
+
assert.ok(!help.includes('--autoApply'), 'should not show camelCase')
|
|
494
|
+
assert.ok(!help.includes('--apiUrl'), 'should not show camelCase')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('accepts the kebab-case form and maps it to the camelCase field', () => {
|
|
498
|
+
const result = parseCLIArguments(
|
|
499
|
+
['deploy', '--auto-apply', '--api-url', 'https://x'],
|
|
500
|
+
'test-cli',
|
|
501
|
+
kebabMeta
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
assert.strictEqual(result.options.autoApply, true)
|
|
505
|
+
assert.strictEqual(result.options.apiUrl, 'https://x')
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test('still accepts the camelCase form (back-compat)', () => {
|
|
509
|
+
const result = parseCLIArguments(
|
|
510
|
+
['deploy', '--autoApply', '--apiUrl', 'https://x'],
|
|
511
|
+
'test-cli',
|
|
512
|
+
kebabMeta
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
assert.strictEqual(result.options.autoApply, true)
|
|
516
|
+
assert.strictEqual(result.options.apiUrl, 'https://x')
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
test('renders missing-required-option errors in kebab-case', () => {
|
|
520
|
+
const result = parseCLIArguments(['deploy'], 'test-cli', kebabMeta)
|
|
521
|
+
|
|
522
|
+
assert.ok(
|
|
523
|
+
result.errors.some((e) => e.includes('--token')),
|
|
524
|
+
'expected a missing --token error'
|
|
525
|
+
)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
445
528
|
})
|
|
@@ -11,6 +11,13 @@ function toCamelCase(str: string): string {
|
|
|
11
11
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** Convert camelCase to kebab-case for display: "autoApply" → "auto-apply".
|
|
15
|
+
* Flags are stored camelCase (matching the function input field) but shown
|
|
16
|
+
* kebab; the parser accepts both forms via toCamelCase. */
|
|
17
|
+
function toKebabCase(str: string): string {
|
|
18
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* Result of parsing CLI arguments
|
|
16
23
|
*/
|
|
@@ -111,6 +118,18 @@ export function parseCLIArguments(
|
|
|
111
118
|
return result
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
// A group/parent command — has subcommands but no runnable function of its
|
|
122
|
+
// own — cannot be executed directly. Surface a routable error so the runner
|
|
123
|
+
// shows its subcommand help instead of attempting to run a missing function.
|
|
124
|
+
if (
|
|
125
|
+
!commandMeta.pikkuFuncId &&
|
|
126
|
+
commandMeta.subcommands &&
|
|
127
|
+
Object.keys(commandMeta.subcommands).length > 0
|
|
128
|
+
) {
|
|
129
|
+
result.errors.push(`Missing subcommand: ${result.commandPath.join(' ')}`)
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
// Collect all available options (global + inherited)
|
|
115
134
|
const availableOptions = collectAvailableOptions(meta, result.commandPath)
|
|
116
135
|
|
|
@@ -366,7 +385,7 @@ function applyOptionDefaults(
|
|
|
366
385
|
|
|
367
386
|
// Check required
|
|
368
387
|
if (def.required && !(name in options)) {
|
|
369
|
-
result.errors.push(`Missing required option: --${name}`)
|
|
388
|
+
result.errors.push(`Missing required option: --${toKebabCase(name)}`)
|
|
370
389
|
}
|
|
371
390
|
|
|
372
391
|
// Validate choices
|
|
@@ -376,13 +395,13 @@ function applyOptionDefaults(
|
|
|
376
395
|
for (const v of value) {
|
|
377
396
|
if (!def.choices.includes(v)) {
|
|
378
397
|
result.errors.push(
|
|
379
|
-
`Invalid value for --${name}: ${v}. Valid choices: ${def.choices.join(', ')}`
|
|
398
|
+
`Invalid value for --${toKebabCase(name)}: ${v}. Valid choices: ${def.choices.join(', ')}`
|
|
380
399
|
)
|
|
381
400
|
}
|
|
382
401
|
}
|
|
383
402
|
} else if (!def.choices.includes(value)) {
|
|
384
403
|
result.errors.push(
|
|
385
|
-
`Invalid value for --${name}: ${value}. Valid choices: ${def.choices.join(', ')}`
|
|
404
|
+
`Invalid value for --${toKebabCase(name)}: ${value}. Valid choices: ${def.choices.join(', ')}`
|
|
386
405
|
)
|
|
387
406
|
}
|
|
388
407
|
}
|
|
@@ -487,7 +506,7 @@ function formatOptions(options: Record<string, CLIOption>, lines: string[]) {
|
|
|
487
506
|
} else {
|
|
488
507
|
line += ' '
|
|
489
508
|
}
|
|
490
|
-
line += `--${name}`
|
|
509
|
+
line += `--${toKebabCase(name)}`
|
|
491
510
|
|
|
492
511
|
if (opt.default !== undefined && typeof opt.default !== 'boolean') {
|
|
493
512
|
line += ` <value>`
|