@pikku/core 0.9.11 → 0.10.0

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 (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/errors/errors.d.ts +2 -0
  3. package/dist/errors/errors.js +6 -0
  4. package/dist/function/function-runner.d.ts +15 -9
  5. package/dist/function/function-runner.js +55 -37
  6. package/dist/function/functions.types.d.ts +67 -1
  7. package/dist/function/functions.types.js +43 -1
  8. package/dist/index.d.ts +2 -6
  9. package/dist/index.js +2 -6
  10. package/dist/middleware/auth-apikey.d.ts +19 -11
  11. package/dist/middleware/auth-apikey.js +33 -33
  12. package/dist/middleware/auth-bearer.d.ts +34 -7
  13. package/dist/middleware/auth-bearer.js +53 -36
  14. package/dist/middleware/auth-cookie.d.ts +30 -12
  15. package/dist/middleware/auth-cookie.js +51 -43
  16. package/dist/middleware/timeout.d.ts +6 -2
  17. package/dist/middleware/timeout.js +7 -9
  18. package/dist/middleware-runner.d.ts +46 -43
  19. package/dist/middleware-runner.js +100 -69
  20. package/dist/permissions.d.ts +82 -20
  21. package/dist/permissions.js +176 -62
  22. package/dist/pikku-state.d.ts +46 -1
  23. package/dist/pikku-state.js +16 -1
  24. package/dist/services/content-service.d.ts +2 -3
  25. package/dist/services/index.d.ts +0 -1
  26. package/dist/services/index.js +0 -1
  27. package/dist/services/local-content.d.ts +2 -4
  28. package/dist/types/core.types.d.ts +110 -4
  29. package/dist/types/core.types.js +44 -1
  30. package/dist/utils.d.ts +1 -1
  31. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  32. package/dist/wirings/channel/channel-handler.js +16 -12
  33. package/dist/wirings/channel/channel-runner.d.ts +2 -2
  34. package/dist/wirings/channel/channel-runner.js +6 -24
  35. package/dist/wirings/channel/channel.types.d.ts +15 -14
  36. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  37. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  38. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  39. package/dist/wirings/cli/channel/cli-channel-runner.js +83 -0
  40. package/dist/wirings/cli/channel/index.d.ts +1 -0
  41. package/dist/wirings/cli/channel/index.js +1 -0
  42. package/dist/wirings/cli/cli-runner.d.ts +42 -0
  43. package/dist/wirings/cli/cli-runner.js +352 -0
  44. package/dist/wirings/cli/cli.types.d.ts +204 -0
  45. package/dist/wirings/cli/cli.types.js +1 -0
  46. package/dist/wirings/cli/command-parser.d.ts +19 -0
  47. package/dist/wirings/cli/command-parser.js +403 -0
  48. package/dist/wirings/cli/index.d.ts +5 -0
  49. package/dist/wirings/cli/index.js +5 -0
  50. package/dist/wirings/http/http-runner.d.ts +59 -10
  51. package/dist/wirings/http/http-runner.js +138 -131
  52. package/dist/wirings/http/http.types.d.ts +11 -10
  53. package/dist/wirings/http/index.d.ts +1 -1
  54. package/dist/wirings/http/index.js +1 -1
  55. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  56. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  57. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  58. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  59. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  60. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  61. package/dist/wirings/mcp/mcp-runner.js +32 -34
  62. package/dist/wirings/mcp/mcp.types.d.ts +38 -14
  63. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  64. package/dist/wirings/queue/queue-runner.js +25 -27
  65. package/dist/wirings/queue/queue.types.d.ts +6 -5
  66. package/dist/wirings/rpc/index.d.ts +1 -1
  67. package/dist/wirings/rpc/index.js +1 -1
  68. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  69. package/dist/wirings/rpc/rpc-runner.js +13 -8
  70. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  71. package/dist/wirings/scheduler/scheduler-runner.js +38 -35
  72. package/dist/wirings/scheduler/scheduler.types.d.ts +5 -4
  73. package/package.json +9 -7
  74. package/src/errors/errors.ts +6 -0
  75. package/src/factory-functions.test.ts +60 -1
  76. package/src/function/function-runner.test.ts +81 -26
  77. package/src/function/function-runner.ts +89 -57
  78. package/src/function/functions.types.ts +85 -2
  79. package/src/index.ts +6 -19
  80. package/src/middleware/auth-apikey.ts +42 -57
  81. package/src/middleware/auth-bearer.ts +66 -49
  82. package/src/middleware/auth-cookie.ts +63 -82
  83. package/src/middleware/timeout.ts +10 -14
  84. package/src/middleware-runner.test.ts +24 -16
  85. package/src/middleware-runner.ts +115 -87
  86. package/src/permissions.test.ts +26 -27
  87. package/src/permissions.ts +216 -104
  88. package/src/pikku-state.ts +79 -2
  89. package/src/services/content-service.ts +5 -4
  90. package/src/services/index.ts +0 -1
  91. package/src/services/local-content.ts +9 -4
  92. package/src/types/core.types.ts +143 -5
  93. package/src/utils.ts +1 -1
  94. package/src/wirings/channel/channel-handler.ts +18 -21
  95. package/src/wirings/channel/channel-runner.ts +19 -32
  96. package/src/wirings/channel/channel.types.ts +29 -16
  97. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  98. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  99. package/src/wirings/cli/channel/cli-channel-runner.ts +123 -0
  100. package/src/wirings/cli/channel/index.ts +1 -0
  101. package/src/wirings/cli/cli-runner.test.ts +403 -0
  102. package/src/wirings/cli/cli-runner.ts +539 -0
  103. package/src/wirings/cli/cli.types.ts +350 -0
  104. package/src/wirings/cli/command-parser.test.ts +445 -0
  105. package/src/wirings/cli/command-parser.ts +504 -0
  106. package/src/wirings/cli/index.ts +13 -0
  107. package/src/wirings/http/http-runner.test.ts +8 -7
  108. package/src/wirings/http/http-runner.ts +165 -158
  109. package/src/wirings/http/http.types.ts +58 -11
  110. package/src/wirings/http/index.ts +7 -1
  111. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  112. package/src/wirings/http/routers/http-router.ts +0 -2
  113. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  114. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  115. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  116. package/src/wirings/mcp/mcp-runner.ts +58 -55
  117. package/src/wirings/mcp/mcp.types.ts +65 -14
  118. package/src/wirings/queue/queue-runner.ts +44 -47
  119. package/src/wirings/queue/queue.types.ts +10 -6
  120. package/src/wirings/rpc/index.ts +1 -1
  121. package/src/wirings/rpc/rpc-runner.ts +27 -9
  122. package/src/wirings/scheduler/scheduler-runner.ts +58 -56
  123. package/src/wirings/scheduler/scheduler.types.ts +10 -3
  124. package/tsconfig.tsbuildinfo +1 -1
@@ -85,16 +85,15 @@ export const runChannelConnect = async ({
85
85
 
86
86
  const userSession = new PikkuUserSessionService(channelStore, channelId)
87
87
 
88
- const { channelConfig, openingData, meta, httpMiddleware } =
89
- await openChannel({
90
- channelId,
91
- createSessionServices,
92
- request,
93
- route,
94
- singletonServices,
95
- coerceDataFromSchema,
96
- userSession,
97
- })
88
+ const { channelConfig, openingData, meta } = await openChannel({
89
+ channelId,
90
+ createSessionServices,
91
+ request,
92
+ route,
93
+ singletonServices,
94
+ coerceDataFromSchema,
95
+ userSession,
96
+ })
98
97
 
99
98
  const main = async () => {
100
99
  try {
@@ -148,9 +147,8 @@ export const runChannelConnect = async ({
148
147
  },
149
148
  { http },
150
149
  combineMiddleware(PikkuWiringTypes.channel, channelConfig.name, {
151
- wiringMiddleware: channelConfig.middleware,
152
- wiringTags: channelConfig.tags,
153
- httpMiddleware,
150
+ wireInheritedMiddleware: meta.middleware,
151
+ wireMiddleware: channelConfig.middleware,
154
152
  }),
155
153
  main
156
154
  )
@@ -171,7 +169,7 @@ export const runChannelDisconnect = async ({
171
169
  if (!sessionServices && params.createSessionServices) {
172
170
  sessionServices = await params.createSessionServices(
173
171
  singletonServices,
174
- {},
172
+ { channel },
175
173
  session
176
174
  )
177
175
  }
@@ -195,7 +193,8 @@ export const runChannelMessage = async (
195
193
  let sessionServices: SessionServices | undefined
196
194
  const { openingData, channelName, session } =
197
195
  await params.channelStore.getChannelAndSession(params.channelId)
198
- const { channelHandler, channelConfig } = getVariablesForChannel({
196
+
197
+ const { channel, channelHandler, channelConfig } = getVariablesForChannel({
199
198
  ...params,
200
199
  openingData,
201
200
  channelName,
@@ -203,7 +202,7 @@ export const runChannelMessage = async (
203
202
  if (params.createSessionServices) {
204
203
  sessionServices = await params.createSessionServices(
205
204
  singletonServices,
206
- {},
205
+ { channel },
207
206
  session
208
207
  )
209
208
  }
@@ -211,7 +210,6 @@ export const runChannelMessage = async (
211
210
  try {
212
211
  const onMessage = processMessageHandlers(
213
212
  { ...singletonServices, ...sessionServices },
214
- session,
215
213
  channelConfig,
216
214
  channelHandler
217
215
  )
@@ -0,0 +1,123 @@
1
+ import { pikkuState } from '../../../pikku-state.js'
2
+ import { CorePikkuCLIRender, CLIMeta } from '../cli.types.js'
3
+ import { generateCommandHelp, parseCLIArguments } from '../command-parser.js'
4
+
5
+ /**
6
+ * Default JSON renderer for CLI output
7
+ */
8
+ const defaultJSONRenderer: CorePikkuCLIRender<any> = (_services, data) => {
9
+ console.log(JSON.stringify(data, null, 2))
10
+ }
11
+
12
+ /**
13
+ * Execute a CLI program via WebSocket channel
14
+ * This is the main entry point for CLI-over-Channel clients
15
+ */
16
+ export async function executeCLIViaChannel({
17
+ programName,
18
+ pikkuWS,
19
+ args = process.argv.slice(2),
20
+ renderers = {},
21
+ defaultRenderer,
22
+ }: {
23
+ programName: string
24
+ pikkuWS: any // CorePikkuWebsocket instance
25
+ args?: string[]
26
+ renderers?: Record<string, CorePikkuCLIRender<any>>
27
+ defaultRenderer?: CorePikkuCLIRender<any>
28
+ }): Promise<void> {
29
+ // Get CLI metadata from state
30
+ const allCLIMeta = pikkuState('cli', 'meta') as unknown as CLIMeta | undefined
31
+ if (!allCLIMeta) {
32
+ throw new Error(
33
+ '[PKU342] CLI metadata not found. No CLI wirings were registered. See https://pikku.dev/errors/pku342 for more information.'
34
+ )
35
+ }
36
+ const programMeta = allCLIMeta.programs[programName]
37
+
38
+ if (!programMeta) {
39
+ console.error(`Error: CLI program "${programName}" not found`)
40
+ process.exit(1)
41
+ }
42
+
43
+ // Parse arguments for this specific program
44
+ const parsed = parseCLIArguments(args, programName, allCLIMeta)
45
+
46
+ // Handle help (check after parsing to support subcommand help)
47
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
48
+ const helpText = generateCommandHelp(
49
+ programName,
50
+ allCLIMeta,
51
+ parsed.commandPath
52
+ )
53
+ console.log(helpText)
54
+ return
55
+ }
56
+
57
+ if (parsed.errors.length > 0) {
58
+ // Check if any error is about an unknown command
59
+ const hasUnknownCommand = parsed.errors.some(
60
+ (error) =>
61
+ error.startsWith('Unknown command:') ||
62
+ error.startsWith('Command not found:')
63
+ )
64
+
65
+ if (hasUnknownCommand) {
66
+ // Show help instead of error for unknown commands
67
+ const helpText = generateCommandHelp(
68
+ programName,
69
+ allCLIMeta,
70
+ parsed.commandPath
71
+ )
72
+ console.log(helpText)
73
+ process.exit(1)
74
+ } else {
75
+ // Show errors for other types of errors
76
+ console.error('Errors:')
77
+ parsed.errors.forEach((error) => console.error(` ${error}`))
78
+ process.exit(1)
79
+ }
80
+ }
81
+
82
+ // Merge positionals and options into single data object
83
+ const data = { ...parsed.positionals, ...parsed.options }
84
+
85
+ // Get the renderer for this command
86
+ const commandId = parsed.commandPath.join('.')
87
+ const renderer =
88
+ renderers[commandId] || defaultRenderer || defaultJSONRenderer
89
+
90
+ return new Promise((resolve, reject) => {
91
+ // Subscribe to responses for this command
92
+ const commandRoute = pikkuWS.getRoute('command')
93
+
94
+ const responseHandler = (response: any) => {
95
+ // Call renderer for any output
96
+ renderer(null as any, response, undefined)
97
+ }
98
+
99
+ commandRoute.subscribe(commandId, responseHandler)
100
+
101
+ // Handle connection close
102
+ pikkuWS.ws.addEventListener('close', () => {
103
+ commandRoute.unsubscribe(commandId, responseHandler)
104
+ resolve(undefined)
105
+ })
106
+
107
+ pikkuWS.ws.addEventListener('error', (error: any) => {
108
+ commandRoute.unsubscribe(commandId, responseHandler)
109
+ reject(error)
110
+ })
111
+
112
+ // Send the command once connected
113
+ if (pikkuWS.ws.readyState === 1) {
114
+ // Already open
115
+ commandRoute.send(commandId, data)
116
+ } else {
117
+ // Wait for open
118
+ pikkuWS.ws.addEventListener('open', () => {
119
+ commandRoute.send(commandId, data)
120
+ })
121
+ }
122
+ })
123
+ }
@@ -0,0 +1 @@
1
+ export * from './cli-channel-runner.js'
@@ -0,0 +1,403 @@
1
+ import { test, describe, beforeEach, afterEach } from 'node:test'
2
+ import * as assert from 'assert'
3
+ import { NotFoundError } from '../../errors/errors.js'
4
+ import { CorePikkuMiddleware } from '../../types/core.types.js'
5
+ import { wireCLI, runCLICommand, pikkuCLIRender } from './cli-runner.js'
6
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
7
+ import { addFunction } from '../../function/function-runner.js'
8
+
9
+ describe('CLI Runner', () => {
10
+ let singletonServices: any
11
+ let createSessionServices: any
12
+
13
+ beforeEach(() => {
14
+ resetPikkuState()
15
+
16
+ singletonServices = {
17
+ logger: {
18
+ info: () => {},
19
+ warn: () => {},
20
+ error: () => {},
21
+ },
22
+ }
23
+
24
+ createSessionServices = async () => ({})
25
+ })
26
+
27
+ afterEach(() => {
28
+ resetPikkuState()
29
+ })
30
+
31
+ describe('runCLICommand', () => {
32
+ test('should throw NotFoundError when program not found', async () => {
33
+ pikkuState('cli', 'meta', { programs: {}, renderers: {} })
34
+
35
+ await assert.rejects(
36
+ async () =>
37
+ runCLICommand({
38
+ program: 'nonexistent',
39
+ commandPath: ['test'],
40
+ data: {},
41
+ singletonServices,
42
+ }),
43
+ NotFoundError
44
+ )
45
+ })
46
+
47
+ test('should throw NotFoundError when command not found', async () => {
48
+ pikkuState('cli', 'meta', {
49
+ programs: {
50
+ 'test-cli': {
51
+ program: 'test-cli',
52
+ commands: {},
53
+ options: {},
54
+ },
55
+ },
56
+ renderers: {},
57
+ })
58
+
59
+ pikkuState('cli', 'programs', {
60
+ 'test-cli': {
61
+ defaultRenderer: undefined,
62
+ middleware: [],
63
+ renderers: {},
64
+ },
65
+ })
66
+
67
+ await assert.rejects(
68
+ async () =>
69
+ runCLICommand({
70
+ program: 'test-cli',
71
+ commandPath: ['nonexistent'],
72
+ data: {},
73
+ singletonServices,
74
+ }),
75
+ NotFoundError
76
+ )
77
+ })
78
+
79
+ test('should execute command function with merged data', async () => {
80
+ let receivedData: any
81
+ const testFunc = async (_services: any, data: any) => {
82
+ receivedData = data
83
+ return { success: true }
84
+ }
85
+
86
+ // Setup metadata
87
+ pikkuState('cli', 'meta', {
88
+ programs: {
89
+ 'test-cli': {
90
+ program: 'test-cli',
91
+ commands: {
92
+ greet: {
93
+ command: 'greet <name>',
94
+ pikkuFuncName: 'greetFunc',
95
+ positionals: [{ name: 'name', required: true }],
96
+ options: {},
97
+ },
98
+ },
99
+ options: {},
100
+ },
101
+ },
102
+ renderers: {},
103
+ })
104
+
105
+ pikkuState('cli', 'programs', {
106
+ 'test-cli': {
107
+ defaultRenderer: undefined,
108
+ middleware: [],
109
+ renderers: {},
110
+ },
111
+ })
112
+
113
+ pikkuState('function', 'meta', {
114
+ greetFunc: {
115
+ pikkuFuncName: 'greetFunc',
116
+ inputSchemaName: null,
117
+ outputSchemaName: null,
118
+ },
119
+ })
120
+
121
+ addFunction('greetFunc', { func: testFunc, auth: false })
122
+
123
+ const result = await runCLICommand({
124
+ program: 'test-cli',
125
+ commandPath: ['greet'],
126
+ data: { name: 'Alice', loud: true },
127
+ singletonServices,
128
+ })
129
+
130
+ assert.strictEqual(result.success, true)
131
+ assert.deepStrictEqual(receivedData, { name: 'Alice', loud: true })
132
+ })
133
+
134
+ test('should run middleware before function execution', async () => {
135
+ const executionOrder: string[] = []
136
+
137
+ const testMiddleware: CorePikkuMiddleware = async (
138
+ _services,
139
+ _interaction,
140
+ next
141
+ ) => {
142
+ executionOrder.push('middleware')
143
+ await next()
144
+ }
145
+
146
+ const testFunc = async () => {
147
+ executionOrder.push('function')
148
+ return { success: true }
149
+ }
150
+
151
+ pikkuState('cli', 'meta', {
152
+ programs: {
153
+ 'test-cli': {
154
+ program: 'test-cli',
155
+ commands: {
156
+ test: {
157
+ command: 'test',
158
+ pikkuFuncName: 'testFunc',
159
+ positionals: [],
160
+ options: {},
161
+ },
162
+ },
163
+ options: {},
164
+ },
165
+ },
166
+ renderers: {},
167
+ })
168
+
169
+ pikkuState('cli', 'programs', {
170
+ 'test-cli': {
171
+ defaultRenderer: undefined,
172
+ middleware: [testMiddleware],
173
+ renderers: {},
174
+ },
175
+ })
176
+
177
+ pikkuState('function', 'meta', {
178
+ testFunc: {
179
+ pikkuFuncName: 'testFunc',
180
+ inputSchemaName: null,
181
+ outputSchemaName: null,
182
+ },
183
+ })
184
+
185
+ addFunction('testFunc', { func: testFunc, auth: false })
186
+
187
+ await runCLICommand({
188
+ program: 'test-cli',
189
+ commandPath: ['test'],
190
+ data: {},
191
+ singletonServices,
192
+ })
193
+
194
+ assert.deepStrictEqual(executionOrder, ['middleware', 'function'])
195
+ })
196
+
197
+ test.skip('should provide CLI context to middleware', async () => {
198
+ let cliContext: any = null
199
+
200
+ const testMiddleware: CorePikkuMiddleware = async (
201
+ _services,
202
+ interaction,
203
+ next
204
+ ) => {
205
+ cliContext = interaction.cli
206
+ await next()
207
+ }
208
+
209
+ const testFunc = async () => ({ success: true })
210
+
211
+ pikkuState('cli', 'meta', {
212
+ programs: {
213
+ 'test-cli': {
214
+ program: 'test-cli',
215
+ commands: {
216
+ greet: {
217
+ command: 'greet <name>',
218
+ pikkuFuncName: 'greetFunc',
219
+ positionals: [{ name: 'name', required: true }],
220
+ options: {},
221
+ },
222
+ },
223
+ options: {},
224
+ },
225
+ },
226
+ renderers: {},
227
+ })
228
+
229
+ pikkuState('cli', 'programs', {
230
+ 'test-cli': {
231
+ defaultRenderer: undefined,
232
+ middleware: [testMiddleware],
233
+ renderers: {},
234
+ },
235
+ })
236
+
237
+ pikkuState('function', 'meta', {
238
+ greetFunc: {
239
+ pikkuFuncName: 'greetFunc',
240
+ inputSchemaName: null,
241
+ outputSchemaName: null,
242
+ },
243
+ })
244
+
245
+ addFunction('greetFunc', { func: testFunc, auth: false })
246
+
247
+ await runCLICommand({
248
+ program: 'test-cli',
249
+ commandPath: ['greet'],
250
+ data: { name: 'Alice' },
251
+ singletonServices,
252
+ })
253
+
254
+ assert.ok(cliContext, 'CLI context should be set by middleware')
255
+ assert.strictEqual(cliContext.program, 'test-cli')
256
+ assert.deepStrictEqual(cliContext.command, ['greet'])
257
+ assert.deepStrictEqual(cliContext.data, { name: 'Alice' })
258
+ })
259
+
260
+ test('should throw error when auth required but no session', async () => {
261
+ const testFunc = async () => ({ success: true })
262
+
263
+ pikkuState('cli', 'meta', {
264
+ programs: {
265
+ 'test-cli': {
266
+ program: 'test-cli',
267
+ commands: {
268
+ secure: {
269
+ command: 'secure',
270
+ pikkuFuncName: 'secureFunc',
271
+ positionals: [],
272
+ options: {},
273
+ },
274
+ },
275
+ options: {},
276
+ },
277
+ },
278
+ renderers: {},
279
+ })
280
+
281
+ pikkuState('function', 'meta', {
282
+ secureFunc: {
283
+ pikkuFuncName: 'secureFunc',
284
+ inputSchemaName: null,
285
+ outputSchemaName: null,
286
+ },
287
+ })
288
+
289
+ addFunction('secureFunc', { func: testFunc, auth: true })
290
+
291
+ await assert.rejects(
292
+ async () =>
293
+ runCLICommand({
294
+ program: 'test-cli',
295
+ commandPath: ['secure'],
296
+ data: {},
297
+ singletonServices,
298
+ createSessionServices,
299
+ }),
300
+ /Authentication required/
301
+ )
302
+ })
303
+ })
304
+
305
+ describe('wireCLI', () => {
306
+ test('should register CLI program and commands', () => {
307
+ const greetFunc = async () => 'Hello'
308
+
309
+ pikkuState('cli', 'meta', {
310
+ programs: {
311
+ 'my-cli': {
312
+ program: 'my-cli',
313
+ commands: {
314
+ greet: {
315
+ command: 'greet <name>',
316
+ pikkuFuncName: 'greetFunc',
317
+ positionals: [{ name: 'name', required: true }],
318
+ options: {
319
+ loud: {
320
+ description: 'Use loud greeting',
321
+ short: 'l',
322
+ default: false,
323
+ },
324
+ },
325
+ },
326
+ },
327
+ options: {},
328
+ },
329
+ },
330
+ renderers: {},
331
+ })
332
+
333
+ wireCLI({
334
+ program: 'my-cli',
335
+ commands: {
336
+ greet: greetFunc,
337
+ },
338
+ })
339
+
340
+ const programs = pikkuState('cli', 'programs')
341
+ assert.ok(programs['my-cli'])
342
+ assert.strictEqual(programs['my-cli'].middleware.length, 0)
343
+ })
344
+
345
+ test('should register CLI with global middleware', () => {
346
+ const middleware: CorePikkuMiddleware = async (_s, _i, next) => {
347
+ await next()
348
+ }
349
+
350
+ pikkuState('cli', 'meta', {
351
+ programs: {
352
+ 'my-cli': {
353
+ program: 'my-cli',
354
+ commands: {},
355
+ options: {},
356
+ },
357
+ },
358
+ renderers: {},
359
+ })
360
+
361
+ wireCLI({
362
+ program: 'my-cli',
363
+ commands: {},
364
+ middleware: [middleware],
365
+ })
366
+
367
+ const programs = pikkuState('cli', 'programs')
368
+ assert.strictEqual(programs['my-cli'].middleware.length, 1)
369
+ assert.strictEqual(programs['my-cli'].middleware[0], middleware)
370
+ })
371
+
372
+ test('should throw error when CLI metadata not found', () => {
373
+ pikkuState('cli', 'meta', { programs: {}, renderers: {} })
374
+
375
+ assert.throws(() => {
376
+ wireCLI({
377
+ program: 'nonexistent',
378
+ commands: {},
379
+ })
380
+ }, /CLI metadata not found for program 'nonexistent'/)
381
+ })
382
+ })
383
+
384
+ describe('pikkuCLIRender', () => {
385
+ test('should create CLI renderer function', () => {
386
+ const renderer = pikkuCLIRender((services, data) => {
387
+ console.log(data)
388
+ })
389
+
390
+ assert.strictEqual(typeof renderer, 'function')
391
+ })
392
+
393
+ test('should preserve renderer function behavior', async () => {
394
+ let receivedData: any
395
+ const renderer = pikkuCLIRender((services, data) => {
396
+ receivedData = data
397
+ })
398
+
399
+ await renderer({} as any, { test: 'value' })
400
+ assert.deepStrictEqual(receivedData, { test: 'value' })
401
+ })
402
+ })
403
+ })