@pikku/core 0.9.11 → 0.9.12-next.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 (99) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/function/function-runner.d.ts +13 -8
  3. package/dist/function/function-runner.js +49 -37
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +3 -1
  6. package/dist/middleware-runner.d.ts +61 -40
  7. package/dist/middleware-runner.js +118 -66
  8. package/dist/permissions.js +1 -0
  9. package/dist/pikku-state.d.ts +26 -1
  10. package/dist/pikku-state.js +10 -1
  11. package/dist/services/content-service.d.ts +2 -3
  12. package/dist/services/index.d.ts +0 -1
  13. package/dist/services/index.js +0 -1
  14. package/dist/services/local-content.d.ts +2 -4
  15. package/dist/types/core.types.d.ts +51 -2
  16. package/dist/types/core.types.js +21 -0
  17. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  18. package/dist/wirings/channel/channel-handler.js +12 -9
  19. package/dist/wirings/channel/channel-runner.d.ts +0 -2
  20. package/dist/wirings/channel/channel-runner.js +2 -3
  21. package/dist/wirings/channel/channel.types.d.ts +5 -3
  22. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  23. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  24. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  25. package/dist/wirings/cli/channel/cli-channel-runner.js +80 -0
  26. package/dist/wirings/cli/channel/index.d.ts +1 -0
  27. package/dist/wirings/cli/channel/index.js +1 -0
  28. package/dist/wirings/cli/cli-runner.d.ts +32 -0
  29. package/dist/wirings/cli/cli-runner.js +328 -0
  30. package/dist/wirings/cli/cli.types.d.ts +177 -0
  31. package/dist/wirings/cli/cli.types.js +1 -0
  32. package/dist/wirings/cli/command-parser.d.ts +19 -0
  33. package/dist/wirings/cli/command-parser.js +373 -0
  34. package/dist/wirings/cli/index.d.ts +5 -0
  35. package/dist/wirings/cli/index.js +5 -0
  36. package/dist/wirings/http/http-runner.d.ts +29 -10
  37. package/dist/wirings/http/http-runner.js +103 -131
  38. package/dist/wirings/http/http.types.d.ts +10 -10
  39. package/dist/wirings/http/index.d.ts +1 -1
  40. package/dist/wirings/http/index.js +1 -1
  41. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  42. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  43. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  44. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  45. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  46. package/dist/wirings/mcp/mcp-runner.js +30 -34
  47. package/dist/wirings/mcp/mcp.types.d.ts +11 -8
  48. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  49. package/dist/wirings/queue/queue-runner.js +25 -27
  50. package/dist/wirings/queue/queue.types.d.ts +6 -5
  51. package/dist/wirings/rpc/index.d.ts +1 -1
  52. package/dist/wirings/rpc/index.js +1 -1
  53. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  54. package/dist/wirings/rpc/rpc-runner.js +13 -8
  55. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  56. package/dist/wirings/scheduler/scheduler-runner.js +37 -35
  57. package/dist/wirings/scheduler/scheduler.types.d.ts +4 -3
  58. package/package.json +4 -3
  59. package/src/function/function-runner.test.ts +73 -23
  60. package/src/function/function-runner.ts +74 -55
  61. package/src/index.ts +8 -1
  62. package/src/middleware-runner.test.ts +24 -16
  63. package/src/middleware-runner.ts +136 -83
  64. package/src/permissions.ts +1 -0
  65. package/src/pikku-state.ts +47 -2
  66. package/src/services/content-service.ts +5 -4
  67. package/src/services/index.ts +0 -1
  68. package/src/services/local-content.ts +9 -4
  69. package/src/types/core.types.ts +74 -3
  70. package/src/wirings/channel/channel-handler.ts +9 -13
  71. package/src/wirings/channel/channel-runner.ts +2 -6
  72. package/src/wirings/channel/channel.types.ts +5 -1
  73. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  74. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  75. package/src/wirings/cli/channel/cli-channel-runner.ts +118 -0
  76. package/src/wirings/cli/channel/index.ts +1 -0
  77. package/src/wirings/cli/cli-runner.test.ts +382 -0
  78. package/src/wirings/cli/cli-runner.ts +503 -0
  79. package/src/wirings/cli/cli.types.ts +320 -0
  80. package/src/wirings/cli/command-parser.test.ts +440 -0
  81. package/src/wirings/cli/command-parser.ts +470 -0
  82. package/src/wirings/cli/index.ts +12 -0
  83. package/src/wirings/http/http-runner.test.ts +8 -7
  84. package/src/wirings/http/http-runner.ts +126 -159
  85. package/src/wirings/http/http.types.ts +56 -11
  86. package/src/wirings/http/index.ts +1 -1
  87. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  88. package/src/wirings/http/routers/http-router.ts +0 -2
  89. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  90. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  91. package/src/wirings/mcp/mcp-runner.ts +56 -55
  92. package/src/wirings/mcp/mcp.types.ts +23 -8
  93. package/src/wirings/queue/queue-runner.ts +44 -47
  94. package/src/wirings/queue/queue.types.ts +10 -6
  95. package/src/wirings/rpc/index.ts +1 -1
  96. package/src/wirings/rpc/rpc-runner.ts +27 -9
  97. package/src/wirings/scheduler/scheduler-runner.ts +57 -56
  98. package/src/wirings/scheduler/scheduler.types.ts +9 -2
  99. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,503 @@
1
+ import { NotFoundError } from '../../errors/errors.js'
2
+ import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
3
+ import { pikkuState } from '../../pikku-state.js'
4
+ import {
5
+ CorePikkuMiddleware,
6
+ CoreUserSession,
7
+ SessionServices,
8
+ PikkuWiringTypes,
9
+ } from '../../types/core.types.js'
10
+ import { closeSessionServices } from '../../utils.js'
11
+ import {
12
+ CoreCLI,
13
+ CLICommandMeta,
14
+ CLIOption,
15
+ CLIProgramState,
16
+ CorePikkuCLIRender,
17
+ CoreCLICommandConfig,
18
+ } from './cli.types.js'
19
+ import type {
20
+ CoreSingletonServices,
21
+ CoreServices,
22
+ CreateSessionServices,
23
+ PikkuInteraction,
24
+ CreateConfig,
25
+ CreateSingletonServices,
26
+ } from '../../types/core.types.js'
27
+ import { PikkuChannel } from '../channel/channel.types.js'
28
+ import { rpcService } from '../rpc/rpc-runner.js'
29
+ import { PikkuUserSessionService } from '../../services/user-session-service.js'
30
+ import { LocalVariablesService } from '../../services/local-variables.js'
31
+ import { generateCommandHelp, parseCLIArguments } from './command-parser.js'
32
+
33
+ /**
34
+ * Default JSON renderer for CLI output
35
+ */
36
+ const defaultJSONRenderer: CorePikkuCLIRender<any> = (_services, data) => {
37
+ console.log(JSON.stringify(data, null, 2))
38
+ }
39
+
40
+ /**
41
+ * Registers a CLI command tree and all its functions
42
+ */
43
+ export const wireCLI = <
44
+ Commands extends Record<string, CoreCLICommandConfig<any, any, any>>,
45
+ GlobalOptions,
46
+ PikkuMiddleware extends CorePikkuMiddleware,
47
+ GlobalOutput,
48
+ >(
49
+ cli: CoreCLI<Commands, GlobalOptions, PikkuMiddleware, GlobalOutput>
50
+ ) => {
51
+ // Get the existing metadata that was generated during inspection
52
+ const cliMeta = pikkuState('cli', 'meta') || {}
53
+
54
+ if (!cliMeta[cli.program]) {
55
+ throw new Error(
56
+ `CLI metadata not found for program '${cli.program}'. Did you run 'pikku all'?`
57
+ )
58
+ }
59
+
60
+ // Get existing programs state and add this program
61
+ const programs: Record<string, CLIProgramState> =
62
+ pikkuState('cli', 'programs') || {}
63
+ programs[cli.program] = {
64
+ defaultRenderer: (cli.render ||
65
+ defaultJSONRenderer) as CorePikkuCLIRender<any>,
66
+ middleware: cli.middleware || [],
67
+ renderers: {},
68
+ tags: cli.tags,
69
+ }
70
+ pikkuState('cli', 'programs', programs)
71
+
72
+ // Register all command functions recursively
73
+ registerCLICommands(
74
+ cli.commands as Record<string, any>,
75
+ [],
76
+ cli.options || {},
77
+ cli.program
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Unwraps a function from pikku wrappers (pikkuFunc, pikkuSessionlessFunc, etc.)
83
+ * These wrappers return { func, middleware, ... } but we need the actual function
84
+ */
85
+ function unwrapFunc(command: any): {
86
+ func: Function
87
+ middleware?: any[]
88
+ auth?: boolean
89
+ permissions?: any
90
+ tags?: string[]
91
+ } {
92
+ if (typeof command === 'function') {
93
+ return { func: command }
94
+ }
95
+
96
+ // If command has a func property that's an object with func, unwrap it
97
+ if (
98
+ command.func &&
99
+ typeof command.func === 'object' &&
100
+ 'func' in command.func
101
+ ) {
102
+ return {
103
+ func: command.func.func,
104
+ middleware: command.func.middleware,
105
+ auth: command.func.auth,
106
+ permissions: command.func.permissions,
107
+ tags: command.func.tags,
108
+ }
109
+ }
110
+
111
+ // Otherwise return as-is
112
+ return command
113
+ }
114
+
115
+ /**
116
+ * Registers CLI commands and their functions recursively
117
+ */
118
+ function registerCLICommands(
119
+ commands: Record<string, any>,
120
+ path: string[] = [],
121
+ inheritedOptions: Record<string, CLIOption> = {},
122
+ program: string
123
+ ) {
124
+ // Get the CLI metadata to find actual function names
125
+ const cliMeta = pikkuState('cli', 'meta')[program]
126
+
127
+ for (const [name, command] of Object.entries(commands)) {
128
+ const fullPath = [...path, name]
129
+ const commandId = fullPath.join('.')
130
+
131
+ // Navigate metadata to find the actual function name
132
+ let currentMeta: CLICommandMeta | undefined = cliMeta?.commands[fullPath[0]]
133
+ for (let i = 1; i < fullPath.length; i++) {
134
+ currentMeta = currentMeta?.subcommands?.[fullPath[i]]
135
+ }
136
+ const funcName = currentMeta?.pikkuFuncName
137
+
138
+ // Skip if no function name (could be a command group)
139
+ if (!funcName) {
140
+ // Recursively register subcommands if they exist
141
+ if (typeof command === 'object' && command.subcommands) {
142
+ const commandOptions = command.options || {}
143
+ const mergedOptions = { ...inheritedOptions, ...commandOptions }
144
+ registerCLICommands(
145
+ command.subcommands,
146
+ fullPath,
147
+ mergedOptions,
148
+ program
149
+ )
150
+ }
151
+ continue
152
+ }
153
+
154
+ // Merge options (inherited + local)
155
+ const commandOptions =
156
+ typeof command === 'object' ? command.options || {} : {}
157
+ const mergedOptions = { ...inheritedOptions, ...commandOptions }
158
+
159
+ // Store the options and middleware in program state for use during execution
160
+ const programs: Record<string, CLIProgramState> = pikkuState(
161
+ 'cli',
162
+ 'programs'
163
+ )
164
+ if (programs[program]) {
165
+ if (!programs[program].commandOptions) {
166
+ programs[program].commandOptions = {}
167
+ }
168
+ programs[program].commandOptions![commandId] = mergedOptions
169
+
170
+ // Store command middleware from the wire config
171
+ if (typeof command === 'object' && command.middleware) {
172
+ if (!programs[program].commandMiddleware) {
173
+ programs[program].commandMiddleware = {}
174
+ }
175
+ programs[program].commandMiddleware![commandId] = command.middleware
176
+ }
177
+ }
178
+
179
+ addFunction(funcName, unwrapFunc(command))
180
+
181
+ // Register renderer if provided
182
+ if (typeof command === 'object' && command.render) {
183
+ if (programs[program]) {
184
+ programs[program].renderers[commandId] = command.render
185
+ }
186
+ }
187
+
188
+ // Recursively register subcommands
189
+ if (typeof command === 'object' && command.subcommands) {
190
+ registerCLICommands(command.subcommands, fullPath, mergedOptions, program)
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Plucks only the data that the function expects based on its schema
197
+ */
198
+ function pluckCLIData(
199
+ mergedData: Record<string, any>,
200
+ funcName: string,
201
+ availableOptions: Record<string, CLIOption>
202
+ ): Record<string, any> {
203
+ const funcMeta = pikkuState('function', 'meta')[funcName]
204
+ const schemaName = funcMeta?.inputSchemaName
205
+ const schema = schemaName
206
+ ? pikkuState('misc', 'schemas').get(schemaName)
207
+ : null
208
+
209
+ if (schema && schema.properties) {
210
+ // If we have a schema, only include fields that are in the schema
211
+ const result: Record<string, any> = {}
212
+ for (const key of Object.keys(schema.properties)) {
213
+ if (key in mergedData) {
214
+ result[key] = mergedData[key]
215
+ } else if (availableOptions[key]?.default !== undefined) {
216
+ // Apply default if not provided
217
+ result[key] = availableOptions[key].default
218
+ }
219
+ }
220
+ return result
221
+ } else {
222
+ // No schema, include all data
223
+ return { ...mergedData }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Executes a CLI command for a specific program
229
+ */
230
+ export async function runCLICommand({
231
+ program,
232
+ commandPath,
233
+ data,
234
+ singletonServices,
235
+ createSessionServices,
236
+ }: {
237
+ program: string
238
+ commandPath: string[]
239
+ data: Record<string, any>
240
+ singletonServices: CoreSingletonServices
241
+ createSessionServices?: CreateSessionServices
242
+ }): Promise<any> {
243
+ // Get the command metadata to find the function name
244
+ const cliMeta = pikkuState('cli', 'meta')
245
+ const programMeta = cliMeta[program]
246
+ if (!programMeta) {
247
+ throw new NotFoundError(`Program not found: ${program}`)
248
+ }
249
+
250
+ // Navigate command tree to find the function name
251
+ let currentCommand = programMeta.commands[commandPath[0]]
252
+ if (!currentCommand) {
253
+ throw new NotFoundError(`Command not found: ${commandPath.join(' ')}`)
254
+ }
255
+
256
+ for (let i = 1; i < commandPath.length; i++) {
257
+ if (
258
+ !currentCommand.subcommands ||
259
+ !currentCommand.subcommands[commandPath[i]]
260
+ ) {
261
+ throw new NotFoundError(`Command not found: ${commandPath.join(' ')}`)
262
+ }
263
+ currentCommand = currentCommand.subcommands[commandPath[i]]
264
+ }
265
+
266
+ const funcName = currentCommand.pikkuFuncName
267
+
268
+ // Get program-specific data
269
+ const programs: Record<string, CLIProgramState> =
270
+ pikkuState('cli', 'programs') || {}
271
+ const programData = programs[program]
272
+
273
+ // Combine program middleware + command middleware from the hierarchy
274
+ const allWireMiddleware: CorePikkuMiddleware[] = [
275
+ ...(programData?.middleware || []),
276
+ ]
277
+
278
+ // Walk through the command path and collect middleware from the runtime config
279
+ const commandParts: string[] = []
280
+ for (const part of commandPath) {
281
+ commandParts.push(part)
282
+ const commandId = commandParts.join('.')
283
+ const middleware = programData?.commandMiddleware?.[commandId]
284
+ if (middleware) {
285
+ allWireMiddleware.push(...middleware)
286
+ }
287
+ }
288
+
289
+ // Get command ID and options
290
+ const commandId = commandPath.join('.')
291
+ const availableOptions = programData?.commandOptions?.[commandId] || {}
292
+
293
+ // Pluck only the fields the function expects
294
+ const pluckedData = () => pluckCLIData(data, funcName, availableOptions)
295
+
296
+ // Get the renderer
297
+ const renderer =
298
+ programData?.renderers[commandId] || programData?.defaultRenderer
299
+
300
+ // Create a CLI channel for progressive output
301
+ const channel: PikkuChannel<unknown, unknown> = {
302
+ channelId: `cli:${program}:${commandId}`,
303
+ openingData: pluckedData,
304
+ send: async (data: any) => {
305
+ if (renderer) {
306
+ await Promise.resolve(renderer(singletonServices, data, undefined))
307
+ }
308
+ },
309
+ close: () => {
310
+ if (channel) {
311
+ channel.state = 'closed'
312
+ }
313
+ },
314
+ state: 'open',
315
+ }
316
+
317
+ const userSession = new PikkuUserSessionService<CoreUserSession>()
318
+ let sessionServices: SessionServices | undefined
319
+
320
+ const interaction: PikkuInteraction = {
321
+ cli: {
322
+ program,
323
+ command: commandPath,
324
+ data: pluckedData,
325
+ channel,
326
+ },
327
+ }
328
+
329
+ const getAllServices = async (session?: CoreUserSession) => {
330
+ // Create session-specific services for handling the command
331
+ sessionServices = await createSessionServices?.(
332
+ singletonServices,
333
+ interaction,
334
+ session
335
+ )
336
+
337
+ return rpcService.injectRPCService(
338
+ {
339
+ ...singletonServices,
340
+ ...sessionServices,
341
+ userSession,
342
+ channel,
343
+ },
344
+ interaction,
345
+ false
346
+ )
347
+ }
348
+
349
+ // Build inherited middleware from tags
350
+ const inheritedMiddleware: any[] = []
351
+ if (programData?.tags) {
352
+ for (const tag of programData.tags) {
353
+ inheritedMiddleware.push({ type: 'tag', tag })
354
+ }
355
+ }
356
+
357
+ try {
358
+ const result = await runPikkuFunc(
359
+ PikkuWiringTypes.cli,
360
+ commandId,
361
+ funcName,
362
+ {
363
+ singletonServices,
364
+ getAllServices,
365
+ data: pluckedData,
366
+ auth: false,
367
+ userSession,
368
+ inheritedMiddleware,
369
+ wireMiddleware: allWireMiddleware,
370
+ interaction,
371
+ }
372
+ )
373
+
374
+ // Apply renderer one final time with the final output (if renderer exists)
375
+ if (renderer) {
376
+ await Promise.resolve(
377
+ renderer(singletonServices, result, userSession.get())
378
+ )
379
+ }
380
+
381
+ return result
382
+ } finally {
383
+ // Close the channel
384
+ channel.close()
385
+
386
+ // Clean up session services
387
+ if (sessionServices) {
388
+ await closeSessionServices(singletonServices.logger, sessionServices)
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Factory function for CLI-specific renderers
395
+ */
396
+ export const pikkuCLIRender = <
397
+ Data,
398
+ Services extends CoreSingletonServices = CoreServices,
399
+ Session extends CoreUserSession = CoreUserSession,
400
+ >(
401
+ renderer: (
402
+ services: Services,
403
+ data: Data,
404
+ session?: Session
405
+ ) => void | Promise<void>
406
+ ): CorePikkuCLIRender<Data, Services, Session> => {
407
+ return renderer
408
+ }
409
+
410
+ /**
411
+ * Execute a CLI program with the given arguments
412
+ * This is the main entry point for CLI programs
413
+ */
414
+ export async function executeCLI({
415
+ programName,
416
+ args = process.argv.slice(2),
417
+ createConfig,
418
+ createSingletonServices,
419
+ createSessionServices,
420
+ }: {
421
+ programName: string
422
+ args?: string[]
423
+ createConfig: CreateConfig<any, any>
424
+ createSingletonServices: CreateSingletonServices<any, any>
425
+ createSessionServices?: CreateSessionServices<any, any>
426
+ }): Promise<void> {
427
+ try {
428
+ // Get CLI metadata from state
429
+ const allCLIMeta = pikkuState('cli', 'meta') || {}
430
+ const programMeta = allCLIMeta[programName]
431
+
432
+ if (!programMeta) {
433
+ console.error(`Error: CLI program "${programName}" not found`)
434
+ process.exit(1)
435
+ }
436
+
437
+ // Parse arguments for this specific program
438
+ const parsed = parseCLIArguments(args, programName, allCLIMeta)
439
+
440
+ // Handle help (check after parsing to support subcommand help)
441
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
442
+ const helpText = generateCommandHelp(
443
+ programName,
444
+ allCLIMeta,
445
+ parsed.commandPath
446
+ )
447
+ console.log(helpText)
448
+ return
449
+ }
450
+
451
+ if (parsed.errors.length > 0) {
452
+ // Check if any error is about an unknown command
453
+ const hasUnknownCommand = parsed.errors.some(
454
+ (error) =>
455
+ error.startsWith('Unknown command:') ||
456
+ error.startsWith('Command not found:')
457
+ )
458
+
459
+ if (hasUnknownCommand) {
460
+ // Show help instead of error for unknown commands
461
+ const helpText = generateCommandHelp(
462
+ programName,
463
+ allCLIMeta,
464
+ parsed.commandPath
465
+ )
466
+ console.log(helpText)
467
+ process.exit(1)
468
+ } else {
469
+ // Show errors for other types of errors
470
+ console.error('Errors:')
471
+ parsed.errors.forEach((error) => console.error(` ${error}`))
472
+ process.exit(1)
473
+ }
474
+ }
475
+
476
+ // Merge positionals and options into single data object
477
+ const data = { ...parsed.positionals, ...parsed.options }
478
+
479
+ // Create config (pass data in case it needs to use any parsed options)
480
+ const config = await createConfig(new LocalVariablesService(), data)
481
+
482
+ // Create services with config
483
+ const singletonServices = await createSingletonServices(config)
484
+
485
+ // Execute the command
486
+ await runCLICommand({
487
+ program: programName,
488
+ commandPath: parsed.commandPath,
489
+ data,
490
+ singletonServices,
491
+ createSessionServices,
492
+ })
493
+ } catch (error: any) {
494
+ console.error('Error:', error)
495
+
496
+ // Show stack trace in verbose mode
497
+ if (args.includes('--verbose') || args.includes('-v')) {
498
+ console.error('Stack trace:', error.stack)
499
+ }
500
+
501
+ process.exit(1)
502
+ }
503
+ }