@pikku/core 0.9.10 → 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 (100) hide show
  1. package/CHANGELOG.md +12 -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/services/local-content.js +2 -2
  16. package/dist/types/core.types.d.ts +51 -2
  17. package/dist/types/core.types.js +21 -0
  18. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  19. package/dist/wirings/channel/channel-handler.js +12 -9
  20. package/dist/wirings/channel/channel-runner.d.ts +0 -2
  21. package/dist/wirings/channel/channel-runner.js +2 -3
  22. package/dist/wirings/channel/channel.types.d.ts +5 -3
  23. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  24. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  25. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  26. package/dist/wirings/cli/channel/cli-channel-runner.js +80 -0
  27. package/dist/wirings/cli/channel/index.d.ts +1 -0
  28. package/dist/wirings/cli/channel/index.js +1 -0
  29. package/dist/wirings/cli/cli-runner.d.ts +32 -0
  30. package/dist/wirings/cli/cli-runner.js +328 -0
  31. package/dist/wirings/cli/cli.types.d.ts +177 -0
  32. package/dist/wirings/cli/cli.types.js +1 -0
  33. package/dist/wirings/cli/command-parser.d.ts +19 -0
  34. package/dist/wirings/cli/command-parser.js +373 -0
  35. package/dist/wirings/cli/index.d.ts +5 -0
  36. package/dist/wirings/cli/index.js +5 -0
  37. package/dist/wirings/http/http-runner.d.ts +29 -10
  38. package/dist/wirings/http/http-runner.js +103 -131
  39. package/dist/wirings/http/http.types.d.ts +10 -10
  40. package/dist/wirings/http/index.d.ts +1 -1
  41. package/dist/wirings/http/index.js +1 -1
  42. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  43. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  44. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  45. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  46. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  47. package/dist/wirings/mcp/mcp-runner.js +30 -34
  48. package/dist/wirings/mcp/mcp.types.d.ts +11 -8
  49. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  50. package/dist/wirings/queue/queue-runner.js +25 -27
  51. package/dist/wirings/queue/queue.types.d.ts +6 -5
  52. package/dist/wirings/rpc/index.d.ts +1 -1
  53. package/dist/wirings/rpc/index.js +1 -1
  54. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  55. package/dist/wirings/rpc/rpc-runner.js +13 -8
  56. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  57. package/dist/wirings/scheduler/scheduler-runner.js +37 -35
  58. package/dist/wirings/scheduler/scheduler.types.d.ts +4 -3
  59. package/package.json +4 -3
  60. package/src/function/function-runner.test.ts +73 -23
  61. package/src/function/function-runner.ts +74 -55
  62. package/src/index.ts +8 -1
  63. package/src/middleware-runner.test.ts +24 -16
  64. package/src/middleware-runner.ts +136 -83
  65. package/src/permissions.ts +1 -0
  66. package/src/pikku-state.ts +47 -2
  67. package/src/services/content-service.ts +5 -4
  68. package/src/services/index.ts +0 -1
  69. package/src/services/local-content.ts +11 -6
  70. package/src/types/core.types.ts +74 -3
  71. package/src/wirings/channel/channel-handler.ts +9 -13
  72. package/src/wirings/channel/channel-runner.ts +2 -6
  73. package/src/wirings/channel/channel.types.ts +5 -1
  74. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  75. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  76. package/src/wirings/cli/channel/cli-channel-runner.ts +118 -0
  77. package/src/wirings/cli/channel/index.ts +1 -0
  78. package/src/wirings/cli/cli-runner.test.ts +382 -0
  79. package/src/wirings/cli/cli-runner.ts +503 -0
  80. package/src/wirings/cli/cli.types.ts +320 -0
  81. package/src/wirings/cli/command-parser.test.ts +440 -0
  82. package/src/wirings/cli/command-parser.ts +470 -0
  83. package/src/wirings/cli/index.ts +12 -0
  84. package/src/wirings/http/http-runner.test.ts +8 -7
  85. package/src/wirings/http/http-runner.ts +126 -159
  86. package/src/wirings/http/http.types.ts +56 -11
  87. package/src/wirings/http/index.ts +1 -1
  88. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  89. package/src/wirings/http/routers/http-router.ts +0 -2
  90. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  91. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  92. package/src/wirings/mcp/mcp-runner.ts +56 -55
  93. package/src/wirings/mcp/mcp.types.ts +23 -8
  94. package/src/wirings/queue/queue-runner.ts +44 -47
  95. package/src/wirings/queue/queue.types.ts +10 -6
  96. package/src/wirings/rpc/index.ts +1 -1
  97. package/src/wirings/rpc/rpc-runner.ts +27 -9
  98. package/src/wirings/scheduler/scheduler-runner.ts +57 -56
  99. package/src/wirings/scheduler/scheduler.types.ts +9 -2
  100. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,470 @@
1
+ import {
2
+ CLIMeta,
3
+ CLIProgramMeta,
4
+ CLICommandMeta,
5
+ CLIPositional,
6
+ CLIOption,
7
+ } from './cli.types.js'
8
+
9
+ /**
10
+ * Result of parsing CLI arguments
11
+ */
12
+ export interface ParsedCommand {
13
+ program: string
14
+ commandPath: string[]
15
+ positionals: Record<string, any>
16
+ options: Record<string, any>
17
+ errors: string[]
18
+ }
19
+
20
+ /**
21
+ * Parses raw CLI arguments into structured data for a specific program
22
+ */
23
+ export function parseCLIArguments(
24
+ args: string[],
25
+ programName: string,
26
+ allMeta: CLIMeta
27
+ ): ParsedCommand {
28
+ const result: ParsedCommand = {
29
+ program: programName,
30
+ commandPath: [],
31
+ positionals: {},
32
+ options: {},
33
+ errors: [],
34
+ }
35
+
36
+ const meta = allMeta[programName]
37
+ if (!meta) {
38
+ result.errors.push(`Program not found: ${programName}`)
39
+ return result
40
+ }
41
+
42
+ let currentIndex = 0
43
+ let currentMeta = meta
44
+
45
+ // Parse command path (non-flag arguments at the beginning)
46
+ while (currentIndex < args.length && !args[currentIndex].startsWith('-')) {
47
+ const arg = args[currentIndex]
48
+
49
+ // Check if this is a known subcommand
50
+ if (currentMeta.commands && currentMeta.commands[arg]) {
51
+ result.commandPath.push(arg)
52
+ currentMeta = {
53
+ program: currentMeta.program,
54
+ commands: currentMeta.commands[arg].subcommands || {},
55
+ options: {
56
+ ...currentMeta.options,
57
+ ...currentMeta.commands[arg].options,
58
+ },
59
+ defaultRenderName:
60
+ currentMeta.commands[arg].renderName || currentMeta.defaultRenderName,
61
+ }
62
+ currentIndex++
63
+ } else {
64
+ // Not a subcommand, must be a positional argument
65
+ break
66
+ }
67
+ }
68
+
69
+ // Get the final command metadata
70
+ const commandMeta = getCommandMeta(meta, result.commandPath)
71
+ if (!commandMeta) {
72
+ result.errors.push(`Unknown command: ${result.commandPath.join(' ')}`)
73
+ return result
74
+ }
75
+
76
+ // Collect all available options (global + inherited)
77
+ const availableOptions = collectAvailableOptions(meta, result.commandPath)
78
+
79
+ // Parse remaining arguments as positionals and options
80
+ const positionalArgs: string[] = []
81
+ const optionArgs: Record<string, any> = {}
82
+
83
+ while (currentIndex < args.length) {
84
+ const arg = args[currentIndex]
85
+
86
+ if (arg.startsWith('--')) {
87
+ // Long option
88
+ const equalIndex = arg.indexOf('=')
89
+ if (equalIndex > 0) {
90
+ // --option=value format
91
+ const key = arg.slice(2, equalIndex)
92
+ const optionDef = availableOptions[key]
93
+
94
+ // Unknown options are allowed for forward compatibility
95
+ const value = arg.slice(equalIndex + 1)
96
+ optionArgs[key] = parseOptionValue(value, optionDef)
97
+ } else {
98
+ // --option value format
99
+ const key = arg.slice(2)
100
+ const optionDef = availableOptions[key]
101
+
102
+ // Unknown options are allowed for forward compatibility
103
+ if (optionDef && optionDef.array) {
104
+ // Array option - collect all following non-flag values
105
+ currentIndex++
106
+ const values: any[] = []
107
+ while (
108
+ currentIndex < args.length &&
109
+ !args[currentIndex].startsWith('-')
110
+ ) {
111
+ values.push(parseOptionValue(args[currentIndex], optionDef))
112
+ currentIndex++
113
+ }
114
+ currentIndex-- // Back up one since we'll increment at loop end
115
+ optionArgs[key] = values
116
+ } else if (
117
+ currentIndex + 1 < args.length &&
118
+ !args[currentIndex + 1].startsWith('-')
119
+ ) {
120
+ // Next arg is the value
121
+ currentIndex++
122
+ optionArgs[key] = parseOptionValue(args[currentIndex], optionDef)
123
+ } else {
124
+ // Boolean flag
125
+ optionArgs[key] = true
126
+ }
127
+ }
128
+ } else if (arg.startsWith('-') && arg.length > 1) {
129
+ // Short option(s)
130
+ for (let i = 1; i < arg.length; i++) {
131
+ const shortFlag = arg[i]
132
+
133
+ // Find the corresponding long option
134
+ const longOption = findLongOption(shortFlag, availableOptions)
135
+ if (longOption) {
136
+ // Check if this is the last character and there's a value
137
+ if (
138
+ i === arg.length - 1 &&
139
+ currentIndex + 1 < args.length &&
140
+ !args[currentIndex + 1].startsWith('-')
141
+ ) {
142
+ currentIndex++
143
+ optionArgs[longOption] = parseOptionValue(
144
+ args[currentIndex],
145
+ availableOptions[longOption]
146
+ )
147
+ } else {
148
+ // Boolean flag
149
+ optionArgs[longOption] = true
150
+ }
151
+ } else {
152
+ result.errors.push(`Unknown option: -${shortFlag}`)
153
+ }
154
+ }
155
+ } else {
156
+ // Positional argument
157
+ positionalArgs.push(arg)
158
+ }
159
+
160
+ currentIndex++
161
+ }
162
+
163
+ // Map positional arguments to named parameters
164
+ mapPositionalArguments(commandMeta.positionals, positionalArgs, result)
165
+
166
+ // Apply option defaults and validation
167
+ applyOptionDefaults(availableOptions, optionArgs, result)
168
+
169
+ result.options = optionArgs
170
+
171
+ return result
172
+ }
173
+
174
+ /**
175
+ * Gets the command metadata for a given path
176
+ */
177
+ function getCommandMeta(
178
+ meta: CLIProgramMeta,
179
+ path: string[]
180
+ ): CLICommandMeta | null {
181
+ if (path.length === 0) {
182
+ return null
183
+ }
184
+
185
+ let current = meta.commands[path[0]]
186
+ if (!current) {
187
+ return null
188
+ }
189
+
190
+ for (let i = 1; i < path.length; i++) {
191
+ if (!current.subcommands || !current.subcommands[path[i]]) {
192
+ return null
193
+ }
194
+ current = current.subcommands[path[i]]
195
+ }
196
+
197
+ return current
198
+ }
199
+
200
+ /**
201
+ * Collects all available options through the inheritance chain
202
+ */
203
+ function collectAvailableOptions(
204
+ meta: CLIProgramMeta,
205
+ path: string[]
206
+ ): Record<string, CLIOption> {
207
+ let options: Record<string, CLIOption> = { ...meta.options }
208
+
209
+ if (path.length === 0) {
210
+ return options
211
+ }
212
+
213
+ // Walk through the command path, merging options
214
+ let current = meta.commands[path[0]]
215
+ if (current) {
216
+ options = { ...options, ...current.options }
217
+
218
+ for (let i = 1; i < path.length; i++) {
219
+ if (current.subcommands && current.subcommands[path[i]]) {
220
+ current = current.subcommands[path[i]]
221
+ options = { ...options, ...current.options }
222
+ }
223
+ }
224
+ }
225
+
226
+ return options
227
+ }
228
+
229
+ /**
230
+ * Finds the long option name for a short flag
231
+ */
232
+ function findLongOption(
233
+ shortFlag: string,
234
+ options: Record<string, CLIOption>
235
+ ): string | null {
236
+ for (const [name, option] of Object.entries(options)) {
237
+ if (option.short === shortFlag) {
238
+ return name
239
+ }
240
+ }
241
+ return null
242
+ }
243
+
244
+ /**
245
+ * Parses an option value based on its definition
246
+ */
247
+ function parseOptionValue(value: string, optionDef?: CLIOption): any {
248
+ if (!optionDef) {
249
+ // No definition, try to infer type
250
+ if (value === 'true') return true
251
+ if (value === 'false') return false
252
+ if (/^\d+$/.test(value)) return parseInt(value, 10)
253
+ if (/^\d*\.\d+$/.test(value)) return parseFloat(value)
254
+ return value
255
+ }
256
+
257
+ // Use default type from option definition
258
+ const defaultValue = optionDef.default
259
+ if (typeof defaultValue === 'boolean') {
260
+ return value === 'true' || value === '1' || value === 'yes'
261
+ }
262
+ if (typeof defaultValue === 'number') {
263
+ return parseFloat(value)
264
+ }
265
+
266
+ // Check choices
267
+ if (optionDef.choices && !optionDef.choices.includes(value as any)) {
268
+ // Invalid choice, but return anyway (validation will catch it)
269
+ return value
270
+ }
271
+
272
+ return value
273
+ }
274
+
275
+ /**
276
+ * Maps positional arguments to named parameters
277
+ */
278
+ function mapPositionalArguments(
279
+ positionalDefs: CLIPositional[],
280
+ args: string[],
281
+ result: ParsedCommand
282
+ ) {
283
+ let argIndex = 0
284
+
285
+ for (const def of positionalDefs) {
286
+ if (def.variadic) {
287
+ // Collect all remaining arguments
288
+ if (argIndex < args.length) {
289
+ result.positionals[def.name] = args.slice(argIndex)
290
+ argIndex = args.length
291
+ } else if (def.required) {
292
+ result.errors.push(`Missing required argument: ${def.name}`)
293
+ } else {
294
+ result.positionals[def.name] = []
295
+ }
296
+ } else {
297
+ // Single argument
298
+ if (argIndex < args.length) {
299
+ result.positionals[def.name] = args[argIndex]
300
+ argIndex++
301
+ } else if (def.required) {
302
+ result.errors.push(`Missing required argument: ${def.name}`)
303
+ }
304
+ }
305
+ }
306
+
307
+ // Check for extra positional arguments
308
+ if (argIndex < args.length) {
309
+ result.errors.push(
310
+ `Unexpected arguments: ${args.slice(argIndex).join(' ')}`
311
+ )
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Applies option defaults and validates choices
317
+ */
318
+ function applyOptionDefaults(
319
+ optionDefs: Record<string, CLIOption>,
320
+ options: Record<string, any>,
321
+ result: ParsedCommand
322
+ ) {
323
+ for (const [name, def] of Object.entries(optionDefs)) {
324
+ // Apply default if not provided
325
+ if (!(name in options) && def.default !== undefined) {
326
+ options[name] = def.default
327
+ }
328
+
329
+ // Check required
330
+ if (def.required && !(name in options)) {
331
+ result.errors.push(`Missing required option: --${name}`)
332
+ }
333
+
334
+ // Validate choices
335
+ if (def.choices && name in options) {
336
+ const value = options[name]
337
+ if (Array.isArray(value)) {
338
+ for (const v of value) {
339
+ if (!def.choices.includes(v)) {
340
+ result.errors.push(
341
+ `Invalid value for --${name}: ${v}. Valid choices: ${def.choices.join(', ')}`
342
+ )
343
+ }
344
+ }
345
+ } else if (!def.choices.includes(value)) {
346
+ result.errors.push(
347
+ `Invalid value for --${name}: ${value}. Valid choices: ${def.choices.join(', ')}`
348
+ )
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Generates help text for a command in a specific program
356
+ */
357
+ export function generateCommandHelp(
358
+ programName: string,
359
+ allMeta: CLIMeta,
360
+ commandPath: string[] = []
361
+ ): string {
362
+ const lines: string[] = []
363
+ const meta = allMeta[programName]
364
+
365
+ if (!meta) {
366
+ return `Program not found: ${programName}`
367
+ }
368
+
369
+ if (commandPath.length === 0) {
370
+ // Root help for the program
371
+ lines.push(`Usage: ${programName} <command> [options]`)
372
+ lines.push('')
373
+ lines.push('Commands:')
374
+
375
+ for (const [name, cmd] of Object.entries(meta.commands)) {
376
+ const desc = cmd.description || ''
377
+ lines.push(` ${name.padEnd(20)} ${desc}`)
378
+ }
379
+
380
+ if (Object.keys(meta.options).length > 0) {
381
+ lines.push('')
382
+ lines.push('Options:')
383
+ formatOptions(meta.options, lines)
384
+ }
385
+ } else {
386
+ // Command-specific help
387
+ const commandMeta = getCommandMeta(meta, commandPath)
388
+ if (!commandMeta) {
389
+ return `Unknown command: ${commandPath.join(' ')}`
390
+ }
391
+
392
+ // Usage line
393
+ let usage = `${programName} ${commandPath.join(' ')}`
394
+ if (commandMeta.parameters) {
395
+ usage += ' ' + commandMeta.parameters
396
+ }
397
+ lines.push(`Usage: ${usage} [options]`)
398
+
399
+ if (commandMeta.description) {
400
+ lines.push('')
401
+ lines.push(commandMeta.description)
402
+ }
403
+
404
+ // Positional arguments
405
+ if (commandMeta.positionals.length > 0) {
406
+ lines.push('')
407
+ lines.push('Arguments:')
408
+ for (const pos of commandMeta.positionals) {
409
+ const marker = pos.required ? '<required>' : '[optional]'
410
+ const variadic = pos.variadic ? '...' : ''
411
+ lines.push(` ${pos.name}${variadic} ${marker}`)
412
+ }
413
+ }
414
+
415
+ // Subcommands
416
+ if (
417
+ commandMeta.subcommands &&
418
+ Object.keys(commandMeta.subcommands).length > 0
419
+ ) {
420
+ lines.push('')
421
+ lines.push('Subcommands:')
422
+ for (const [name, sub] of Object.entries(commandMeta.subcommands)) {
423
+ const desc = sub.description || ''
424
+ lines.push(` ${name.padEnd(20)} ${desc}`)
425
+ }
426
+ }
427
+
428
+ // Options
429
+ const availableOptions = collectAvailableOptions(meta, commandPath)
430
+ if (Object.keys(availableOptions).length > 0) {
431
+ lines.push('')
432
+ lines.push('Options:')
433
+ formatOptions(availableOptions, lines)
434
+ }
435
+ }
436
+
437
+ return lines.join('\n')
438
+ }
439
+
440
+ /**
441
+ * Formats options for help text
442
+ */
443
+ function formatOptions(options: Record<string, CLIOption>, lines: string[]) {
444
+ for (const [name, opt] of Object.entries(options)) {
445
+ let line = ' '
446
+ if (opt.short) {
447
+ line += `-${opt.short}, `
448
+ } else {
449
+ line += ' '
450
+ }
451
+ line += `--${name}`
452
+
453
+ if (opt.default !== undefined && typeof opt.default !== 'boolean') {
454
+ line += ` <value>`
455
+ }
456
+
457
+ line = line.padEnd(30)
458
+ line += opt.description || ''
459
+
460
+ if (opt.default !== undefined) {
461
+ line += ` (default: ${JSON.stringify(opt.default)})`
462
+ }
463
+
464
+ if (opt.choices) {
465
+ line += ` [choices: ${opt.choices.join(', ')}]`
466
+ }
467
+
468
+ lines.push(line)
469
+ }
470
+ }
@@ -0,0 +1,12 @@
1
+ export * from './cli.types.js'
2
+ export * from './cli-runner.js'
3
+ export * from './command-parser.js'
4
+
5
+ export {
6
+ wireCLI,
7
+ runCLICommand,
8
+ pikkuCLIRender,
9
+ executeCLI,
10
+ } from './cli-runner.js'
11
+
12
+ export { parseCLIArguments, generateCommandHelp } from './command-parser.js'
@@ -49,6 +49,7 @@ describe('fetch', () => {
49
49
 
50
50
  beforeEach(() => {
51
51
  resetPikkuState()
52
+ httpRouter.reset()
52
53
 
53
54
  singletonServices = {
54
55
  logger: {
@@ -90,8 +91,7 @@ describe('fetch', () => {
90
91
  wireHTTP({
91
92
  route: 'test',
92
93
  method: 'get',
93
- func: routeFunc,
94
- middleware: [sessionMiddleware],
94
+ func: { func: routeFunc, middleware: [sessionMiddleware] },
95
95
  })
96
96
 
97
97
  // Initialize router after adding route (for tests)
@@ -113,9 +113,11 @@ describe('fetch', () => {
113
113
  wireHTTP({
114
114
  route: 'test',
115
115
  method: 'get',
116
- func: routeFunc,
117
- permissions,
118
- middleware: [sessionMiddleware],
116
+ func: {
117
+ func: routeFunc,
118
+ permissions,
119
+ middleware: [sessionMiddleware],
120
+ },
119
121
  })
120
122
 
121
123
  await fetch(request, {
@@ -135,8 +137,7 @@ describe('fetch', () => {
135
137
  wireHTTP({
136
138
  route: 'test',
137
139
  method: 'get',
138
- func: routeFunc,
139
- middleware: [sessionMiddleware],
140
+ func: { func: routeFunc, middleware: [sessionMiddleware] },
140
141
  })
141
142
  await assert.rejects(
142
143
  async () =>