@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
@@ -0,0 +1,83 @@
1
+ import { pikkuState } from '../../../pikku-state.js';
2
+ import { generateCommandHelp, parseCLIArguments } from '../command-parser.js';
3
+ /**
4
+ * Default JSON renderer for CLI output
5
+ */
6
+ const defaultJSONRenderer = (_services, data) => {
7
+ console.log(JSON.stringify(data, null, 2));
8
+ };
9
+ /**
10
+ * Execute a CLI program via WebSocket channel
11
+ * This is the main entry point for CLI-over-Channel clients
12
+ */
13
+ export async function executeCLIViaChannel({ programName, pikkuWS, args = process.argv.slice(2), renderers = {}, defaultRenderer, }) {
14
+ // Get CLI metadata from state
15
+ const allCLIMeta = pikkuState('cli', 'meta');
16
+ if (!allCLIMeta) {
17
+ throw new Error('[PKU342] CLI metadata not found. No CLI wirings were registered. See https://pikku.dev/errors/pku342 for more information.');
18
+ }
19
+ const programMeta = allCLIMeta.programs[programName];
20
+ if (!programMeta) {
21
+ console.error(`Error: CLI program "${programName}" not found`);
22
+ process.exit(1);
23
+ }
24
+ // Parse arguments for this specific program
25
+ const parsed = parseCLIArguments(args, programName, allCLIMeta);
26
+ // Handle help (check after parsing to support subcommand help)
27
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
28
+ const helpText = generateCommandHelp(programName, allCLIMeta, parsed.commandPath);
29
+ console.log(helpText);
30
+ return;
31
+ }
32
+ if (parsed.errors.length > 0) {
33
+ // Check if any error is about an unknown command
34
+ const hasUnknownCommand = parsed.errors.some((error) => error.startsWith('Unknown command:') ||
35
+ error.startsWith('Command not found:'));
36
+ if (hasUnknownCommand) {
37
+ // Show help instead of error for unknown commands
38
+ const helpText = generateCommandHelp(programName, allCLIMeta, parsed.commandPath);
39
+ console.log(helpText);
40
+ process.exit(1);
41
+ }
42
+ else {
43
+ // Show errors for other types of errors
44
+ console.error('Errors:');
45
+ parsed.errors.forEach((error) => console.error(` ${error}`));
46
+ process.exit(1);
47
+ }
48
+ }
49
+ // Merge positionals and options into single data object
50
+ const data = { ...parsed.positionals, ...parsed.options };
51
+ // Get the renderer for this command
52
+ const commandId = parsed.commandPath.join('.');
53
+ const renderer = renderers[commandId] || defaultRenderer || defaultJSONRenderer;
54
+ return new Promise((resolve, reject) => {
55
+ // Subscribe to responses for this command
56
+ const commandRoute = pikkuWS.getRoute('command');
57
+ const responseHandler = (response) => {
58
+ // Call renderer for any output
59
+ renderer(null, response, undefined);
60
+ };
61
+ commandRoute.subscribe(commandId, responseHandler);
62
+ // Handle connection close
63
+ pikkuWS.ws.addEventListener('close', () => {
64
+ commandRoute.unsubscribe(commandId, responseHandler);
65
+ resolve(undefined);
66
+ });
67
+ pikkuWS.ws.addEventListener('error', (error) => {
68
+ commandRoute.unsubscribe(commandId, responseHandler);
69
+ reject(error);
70
+ });
71
+ // Send the command once connected
72
+ if (pikkuWS.ws.readyState === 1) {
73
+ // Already open
74
+ commandRoute.send(commandId, data);
75
+ }
76
+ else {
77
+ // Wait for open
78
+ pikkuWS.ws.addEventListener('open', () => {
79
+ commandRoute.send(commandId, data);
80
+ });
81
+ }
82
+ });
83
+ }
@@ -0,0 +1 @@
1
+ export * from './cli-channel-runner.js';
@@ -0,0 +1 @@
1
+ export * from './cli-channel-runner.js';
@@ -0,0 +1,42 @@
1
+ import { CorePikkuMiddleware, CoreUserSession } from '../../types/core.types.js';
2
+ import { CoreCLI, CorePikkuCLIRender, CoreCLICommandConfig } from './cli.types.js';
3
+ import type { CoreSingletonServices, CoreServices, CreateSessionServices, CreateConfig, CreateSingletonServices } from '../../types/core.types.js';
4
+ /**
5
+ * CLI command execution error - thrown when CLI execution fails
6
+ * Should be caught by the wrapper to call process.exit()
7
+ */
8
+ export declare class CLIError extends Error {
9
+ exitCode: number;
10
+ constructor(message: string, exitCode?: number);
11
+ }
12
+ /**
13
+ * Registers a CLI command tree and all its functions
14
+ */
15
+ export declare const wireCLI: <Commands extends Record<string, CoreCLICommandConfig<any, any, any>>, GlobalOptions, PikkuMiddleware extends CorePikkuMiddleware, GlobalOutput>(cli: CoreCLI<Commands, GlobalOptions, PikkuMiddleware, GlobalOutput>) => void;
16
+ /**
17
+ * Executes a CLI command for a specific program
18
+ */
19
+ export declare function runCLICommand({ program, commandPath, data, singletonServices, createSessionServices, }: {
20
+ program: string;
21
+ commandPath: string[];
22
+ data: Record<string, any>;
23
+ singletonServices: CoreSingletonServices;
24
+ createSessionServices?: CreateSessionServices;
25
+ }): Promise<any>;
26
+ /**
27
+ * Factory function for CLI-specific renderers
28
+ */
29
+ export declare const pikkuCLIRender: <Data, Services extends CoreSingletonServices = CoreServices, Session extends CoreUserSession = CoreUserSession>(renderer: (services: Services, data: Data, session?: Session) => void | Promise<void>) => CorePikkuCLIRender<Data, Services, Session>;
30
+ /**
31
+ * Execute a CLI program with the given arguments
32
+ * This is the main entry point for CLI programs
33
+ *
34
+ * @throws {CLIError} When CLI execution fails - should be caught by wrapper to call process.exit()
35
+ */
36
+ export declare function executeCLI({ programName, args, createConfig, createSingletonServices, createSessionServices, }: {
37
+ programName: string;
38
+ args?: string[];
39
+ createConfig: CreateConfig<any, any>;
40
+ createSingletonServices: CreateSingletonServices<any, any>;
41
+ createSessionServices?: CreateSessionServices<any, any>;
42
+ }): Promise<void>;
@@ -0,0 +1,352 @@
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 { PikkuWiringTypes, } from '../../types/core.types.js';
5
+ import { closeSessionServices } from '../../utils.js';
6
+ import { rpcService } from '../rpc/rpc-runner.js';
7
+ import { PikkuUserSessionService } from '../../services/user-session-service.js';
8
+ import { LocalVariablesService } from '../../services/local-variables.js';
9
+ import { generateCommandHelp, parseCLIArguments } from './command-parser.js';
10
+ /**
11
+ * CLI command execution error - thrown when CLI execution fails
12
+ * Should be caught by the wrapper to call process.exit()
13
+ */
14
+ export class CLIError extends Error {
15
+ exitCode;
16
+ constructor(message, exitCode = 1) {
17
+ super(message);
18
+ this.exitCode = exitCode;
19
+ this.name = 'CLIError';
20
+ }
21
+ }
22
+ /**
23
+ * Default JSON renderer for CLI output
24
+ */
25
+ const defaultJSONRenderer = (_services, data) => {
26
+ console.log(JSON.stringify(data, null, 2));
27
+ };
28
+ /**
29
+ * Registers a CLI command tree and all its functions
30
+ */
31
+ export const wireCLI = (cli) => {
32
+ // Get the existing metadata that was generated during inspection
33
+ const cliMeta = pikkuState('cli', 'meta') || {};
34
+ if (!cliMeta.programs?.[cli.program]) {
35
+ throw new Error(`CLI metadata not found for program '${cli.program}'. Did you run 'pikku all'?`);
36
+ }
37
+ // Get existing programs state and add this program
38
+ const programs = pikkuState('cli', 'programs') || {};
39
+ programs[cli.program] = {
40
+ defaultRenderer: (cli.render ||
41
+ defaultJSONRenderer),
42
+ middleware: cli.middleware || [],
43
+ renderers: {},
44
+ tags: cli.tags,
45
+ };
46
+ pikkuState('cli', 'programs', programs);
47
+ // Register all command functions recursively
48
+ registerCLICommands(cli.commands, [], cli.options || {}, cli.program);
49
+ };
50
+ /**
51
+ * Unwraps a function from pikku wrappers (pikkuFunc, pikkuSessionlessFunc, etc.)
52
+ * These wrappers return { func, middleware, ... } but we need the actual function
53
+ */
54
+ function unwrapFunc(command) {
55
+ if (typeof command === 'function') {
56
+ return { func: command };
57
+ }
58
+ // If command has a func property that's an object with func, unwrap it
59
+ if (command.func &&
60
+ typeof command.func === 'object' &&
61
+ 'func' in command.func) {
62
+ return {
63
+ func: command.func.func,
64
+ middleware: command.func.middleware,
65
+ auth: command.func.auth,
66
+ permissions: command.func.permissions,
67
+ tags: command.func.tags,
68
+ };
69
+ }
70
+ // Otherwise return as-is
71
+ return command;
72
+ }
73
+ /**
74
+ * Registers CLI commands and their functions recursively
75
+ */
76
+ function registerCLICommands(commands, path = [], inheritedOptions = {}, program) {
77
+ // Get the CLI metadata to find actual function names
78
+ const cliMeta = pikkuState('cli', 'meta').programs[program];
79
+ for (const [name, command] of Object.entries(commands)) {
80
+ const fullPath = [...path, name];
81
+ const commandId = fullPath.join('.');
82
+ // Navigate metadata to find the actual function name
83
+ let currentMeta = cliMeta?.commands[fullPath[0]];
84
+ for (let i = 1; i < fullPath.length; i++) {
85
+ currentMeta = currentMeta?.subcommands?.[fullPath[i]];
86
+ }
87
+ const funcName = currentMeta?.pikkuFuncName;
88
+ // Skip if no function name (could be a command group)
89
+ if (!funcName) {
90
+ // Recursively register subcommands if they exist
91
+ if (typeof command === 'object' && command.subcommands) {
92
+ const commandOptions = command.options || {};
93
+ const mergedOptions = { ...inheritedOptions, ...commandOptions };
94
+ registerCLICommands(command.subcommands, fullPath, mergedOptions, program);
95
+ }
96
+ continue;
97
+ }
98
+ // Merge options (inherited + local)
99
+ const commandOptions = typeof command === 'object' ? command.options || {} : {};
100
+ const mergedOptions = { ...inheritedOptions, ...commandOptions };
101
+ // Store the options and middleware in program state for use during execution
102
+ const programs = pikkuState('cli', 'programs');
103
+ if (programs[program]) {
104
+ if (!programs[program].commandOptions) {
105
+ programs[program].commandOptions = {};
106
+ }
107
+ programs[program].commandOptions[commandId] = mergedOptions;
108
+ // Store command middleware from the wire config
109
+ if (typeof command === 'object' && command.middleware) {
110
+ if (!programs[program].commandMiddleware) {
111
+ programs[program].commandMiddleware = {};
112
+ }
113
+ programs[program].commandMiddleware[commandId] = command.middleware;
114
+ }
115
+ }
116
+ addFunction(funcName, unwrapFunc(command));
117
+ // Register renderer if provided
118
+ if (typeof command === 'object' && command.render) {
119
+ if (programs[program]) {
120
+ programs[program].renderers[commandId] = command.render;
121
+ }
122
+ }
123
+ // Recursively register subcommands
124
+ if (typeof command === 'object' && command.subcommands) {
125
+ registerCLICommands(command.subcommands, fullPath, mergedOptions, program);
126
+ }
127
+ }
128
+ }
129
+ /**
130
+ * Plucks only the data that the function expects based on its schema
131
+ */
132
+ function pluckCLIData(mergedData, funcName, availableOptions) {
133
+ const funcMeta = pikkuState('function', 'meta')[funcName];
134
+ const schemaName = funcMeta?.inputSchemaName;
135
+ const schema = schemaName
136
+ ? pikkuState('misc', 'schemas').get(schemaName)
137
+ : null;
138
+ if (schema && schema.properties) {
139
+ // If we have a schema, only include fields that are in the schema
140
+ const result = {};
141
+ for (const key of Object.keys(schema.properties)) {
142
+ if (key in mergedData) {
143
+ result[key] = mergedData[key];
144
+ }
145
+ else if (availableOptions[key]?.default !== undefined) {
146
+ // Apply default if not provided
147
+ result[key] = availableOptions[key].default;
148
+ }
149
+ }
150
+ return result;
151
+ }
152
+ else {
153
+ // No schema, include all data
154
+ return { ...mergedData };
155
+ }
156
+ }
157
+ /**
158
+ * Executes a CLI command for a specific program
159
+ */
160
+ export async function runCLICommand({ program, commandPath, data, singletonServices, createSessionServices, }) {
161
+ // Get the command metadata to find the function name
162
+ const cliMeta = pikkuState('cli', 'meta');
163
+ const programMeta = cliMeta.programs?.[program];
164
+ if (!programMeta) {
165
+ throw new NotFoundError(`Program not found: ${program}`);
166
+ }
167
+ // Navigate command tree to find the function name
168
+ let currentCommand = programMeta.commands[commandPath[0]];
169
+ if (!currentCommand) {
170
+ throw new NotFoundError(`Command not found: ${commandPath.join(' ')}`);
171
+ }
172
+ for (let i = 1; i < commandPath.length; i++) {
173
+ if (!currentCommand.subcommands ||
174
+ !currentCommand.subcommands[commandPath[i]]) {
175
+ throw new NotFoundError(`Command not found: ${commandPath.join(' ')}`);
176
+ }
177
+ currentCommand = currentCommand.subcommands[commandPath[i]];
178
+ }
179
+ const funcName = currentCommand.pikkuFuncName;
180
+ // Get program-specific data
181
+ const programs = pikkuState('cli', 'programs') || {};
182
+ const programData = programs[program];
183
+ // Combine program middleware + command middleware from the hierarchy
184
+ const allWireMiddleware = [
185
+ ...(programData?.middleware || []),
186
+ ];
187
+ // Walk through the command path and collect middleware from the runtime config
188
+ const commandParts = [];
189
+ for (const part of commandPath) {
190
+ commandParts.push(part);
191
+ const commandId = commandParts.join('.');
192
+ const middleware = programData?.commandMiddleware?.[commandId];
193
+ if (middleware) {
194
+ allWireMiddleware.push(...middleware);
195
+ }
196
+ }
197
+ // Get command ID and options
198
+ const commandId = commandPath.join('.');
199
+ const availableOptions = programData?.commandOptions?.[commandId] || {};
200
+ // Pluck only the fields the function expects
201
+ const pluckedData = () => pluckCLIData(data, funcName, availableOptions);
202
+ // Get the renderer
203
+ const renderer = programData?.renderers[commandId] || programData?.defaultRenderer;
204
+ // Create a CLI channel for progressive output
205
+ const channel = {
206
+ channelId: `cli:${program}:${commandId}`,
207
+ openingData: pluckedData,
208
+ send: async (data) => {
209
+ if (renderer) {
210
+ await Promise.resolve(renderer(singletonServices, data, undefined));
211
+ }
212
+ },
213
+ close: () => {
214
+ if (channel) {
215
+ channel.state = 'closed';
216
+ }
217
+ },
218
+ state: 'open',
219
+ };
220
+ const userSession = new PikkuUserSessionService();
221
+ let sessionServices;
222
+ const interaction = {
223
+ cli: {
224
+ program,
225
+ command: commandPath,
226
+ data: pluckedData,
227
+ channel,
228
+ },
229
+ };
230
+ const getAllServices = async (session) => {
231
+ // Create session-specific services for handling the command
232
+ sessionServices = await createSessionServices?.(singletonServices, interaction, session);
233
+ return rpcService.injectRPCService({
234
+ ...singletonServices,
235
+ ...sessionServices,
236
+ userSession,
237
+ channel,
238
+ }, interaction, false);
239
+ };
240
+ try {
241
+ const result = await runPikkuFunc(PikkuWiringTypes.cli, commandId, funcName, {
242
+ singletonServices,
243
+ getAllServices,
244
+ data: pluckedData,
245
+ auth: false,
246
+ userSession,
247
+ inheritedMiddleware: currentCommand.middleware,
248
+ wireMiddleware: allWireMiddleware,
249
+ inheritedPermissions: currentCommand.permissions,
250
+ wirePermissions: undefined,
251
+ tags: programData?.tags,
252
+ interaction,
253
+ });
254
+ // Apply renderer one final time with the final output (if renderer exists)
255
+ if (renderer) {
256
+ await Promise.resolve(renderer(singletonServices, result, userSession.get()));
257
+ }
258
+ return result;
259
+ }
260
+ finally {
261
+ // Close the channel
262
+ channel.close();
263
+ // Clean up session services
264
+ if (sessionServices) {
265
+ await closeSessionServices(singletonServices.logger, sessionServices);
266
+ }
267
+ }
268
+ }
269
+ /**
270
+ * Factory function for CLI-specific renderers
271
+ */
272
+ export const pikkuCLIRender = (renderer) => {
273
+ return renderer;
274
+ };
275
+ /**
276
+ * Execute a CLI program with the given arguments
277
+ * This is the main entry point for CLI programs
278
+ *
279
+ * @throws {CLIError} When CLI execution fails - should be caught by wrapper to call process.exit()
280
+ */
281
+ export async function executeCLI({ programName, args, createConfig, createSingletonServices, createSessionServices, }) {
282
+ if (!args) {
283
+ throw new Error('CLI arguments are required, this is to satisfy release diffs');
284
+ }
285
+ try {
286
+ // Get CLI metadata from state
287
+ const allCLIMeta = pikkuState('cli', 'meta');
288
+ if (!allCLIMeta) {
289
+ throw new Error('[PKU342] CLI metadata not found. No CLI wirings were registered. See https://pikku.dev/errors/pku342 for more information.');
290
+ }
291
+ const programMeta = allCLIMeta.programs[programName];
292
+ if (!programMeta) {
293
+ throw new CLIError(`CLI program "${programName}" not found`, 1);
294
+ }
295
+ // Parse arguments for this specific program
296
+ const parsed = parseCLIArguments(args, programName, allCLIMeta);
297
+ // Handle help (check after parsing to support subcommand help)
298
+ // Show help if --help/-h is present, or if no args AND no default command
299
+ const shouldShowHelp = args.includes('--help') ||
300
+ args.includes('-h') ||
301
+ (args.length === 0 && parsed.commandPath.length === 0);
302
+ if (shouldShowHelp) {
303
+ const helpText = generateCommandHelp(programName, allCLIMeta, parsed.commandPath);
304
+ console.log(helpText);
305
+ return;
306
+ }
307
+ if (parsed.errors.length > 0) {
308
+ // Check if any error is about an unknown command
309
+ const hasUnknownCommand = parsed.errors.some((error) => error.startsWith('Unknown command:') ||
310
+ error.startsWith('Command not found:'));
311
+ if (hasUnknownCommand) {
312
+ // Show help instead of error for unknown commands
313
+ const helpText = generateCommandHelp(programName, allCLIMeta, parsed.commandPath);
314
+ console.log(helpText);
315
+ throw new CLIError('Unknown command', 1);
316
+ }
317
+ else {
318
+ // Show errors for other types of errors
319
+ console.error('Errors:');
320
+ parsed.errors.forEach((error) => console.error(` ${error}`));
321
+ throw new CLIError(parsed.errors.join('\n'), 1);
322
+ }
323
+ }
324
+ // Merge positionals and options into single data object
325
+ const data = { ...parsed.positionals, ...parsed.options };
326
+ // Create config (pass data in case it needs to use any parsed options)
327
+ const config = await createConfig(new LocalVariablesService(), data);
328
+ // Create services with config
329
+ const singletonServices = await createSingletonServices(config);
330
+ // Execute the command
331
+ await runCLICommand({
332
+ program: programName,
333
+ commandPath: parsed.commandPath,
334
+ data,
335
+ singletonServices,
336
+ createSessionServices,
337
+ });
338
+ }
339
+ catch (error) {
340
+ // Re-throw CLIError as-is
341
+ if (error instanceof CLIError) {
342
+ throw error;
343
+ }
344
+ // Wrap other errors in CLIError
345
+ console.error('Error:', error);
346
+ // Show stack trace in verbose mode
347
+ if (args.includes('--verbose') || args.includes('-v')) {
348
+ console.error('Stack trace:', error.stack);
349
+ }
350
+ throw new CLIError(error.message || String(error), 1);
351
+ }
352
+ }
@@ -0,0 +1,204 @@
1
+ import { CorePikkuMiddleware, CoreSingletonServices, CoreUserSession, PikkuDocs, CoreServices, MiddlewareMetadata, PermissionMetadata } from '../../types/core.types.js';
2
+ import { CorePikkuFunctionConfig, CorePikkuPermission, CorePikkuFunction, CorePikkuFunctionSessionless } from '../../function/functions.types.js';
3
+ import { PikkuChannel } from '../channel/channel.types.js';
4
+ /**
5
+ * CLI option definition
6
+ */
7
+ export interface CLIOption<T = any> {
8
+ description: string;
9
+ short?: string;
10
+ default?: T;
11
+ choices?: T[];
12
+ array?: boolean;
13
+ required?: boolean;
14
+ }
15
+ /**
16
+ * CLI options collection
17
+ */
18
+ export type CLIOptions<T> = {
19
+ [K in keyof T]: CLIOption<T[K]>;
20
+ };
21
+ /**
22
+ * Positional argument definition
23
+ */
24
+ export interface CLIPositional {
25
+ name: string;
26
+ required: boolean;
27
+ variadic?: boolean;
28
+ }
29
+ /**
30
+ * CLI interaction context
31
+ */
32
+ export type PikkuCLI = {
33
+ program: string;
34
+ command: string[];
35
+ data: Record<string, any>;
36
+ channel: PikkuChannel<unknown, unknown>;
37
+ };
38
+ /**
39
+ * Runtime CLI program state
40
+ */
41
+ export interface CLIProgramState {
42
+ defaultRenderer?: CorePikkuCLIRender<any>;
43
+ middleware: CorePikkuMiddleware[];
44
+ renderers: Record<string, CorePikkuCLIRender<any>>;
45
+ commandOptions?: Record<string, Record<string, CLIOption>>;
46
+ commandMiddleware?: Record<string, CorePikkuMiddleware[]>;
47
+ tags?: string[];
48
+ }
49
+ /**
50
+ * CLI command metadata for runtime
51
+ */
52
+ export interface CLICommandMeta {
53
+ parameters?: string;
54
+ pikkuFuncName: string;
55
+ positionals: CLIPositional[];
56
+ options: Record<string, CLIOption>;
57
+ renderName?: string;
58
+ description?: string;
59
+ docs?: PikkuDocs;
60
+ tags?: string[];
61
+ subcommands?: Record<string, CLICommandMeta>;
62
+ middleware?: MiddlewareMetadata[];
63
+ permissions?: PermissionMetadata[];
64
+ isDefault?: boolean;
65
+ }
66
+ /**
67
+ * CLI metadata collection (per program)
68
+ */
69
+ export type CLIProgramMeta = {
70
+ program: string;
71
+ commands: Record<string, CLICommandMeta>;
72
+ options: Record<string, CLIOption>;
73
+ defaultRenderName?: string;
74
+ };
75
+ /**
76
+ * Renderer metadata for CLI commands
77
+ */
78
+ export interface RendererMeta {
79
+ name: string;
80
+ exportedName?: string;
81
+ services: {
82
+ optimized: boolean;
83
+ services: string[];
84
+ };
85
+ filePath: string;
86
+ }
87
+ export type RenderersMeta = Record<string, RendererMeta>;
88
+ /**
89
+ * All CLI programs metadata
90
+ */
91
+ export interface CLIMeta {
92
+ programs: Record<string, CLIProgramMeta>;
93
+ renderers: RenderersMeta;
94
+ }
95
+ /**
96
+ * CLI-specific renderer that outputs to console
97
+ */
98
+ export type CorePikkuCLIRender<Data, Services extends CoreSingletonServices = CoreServices, Session extends CoreUserSession = CoreUserSession> = CorePikkuRender<Data, void, Services, Session>;
99
+ /**
100
+ * Extract input parameters from a Pikku function config type
101
+ */
102
+ export type ExtractFunctionInput<Func> = Func extends CorePikkuFunctionConfig<infer FuncType, any, any> ? FuncType extends CorePikkuFunction<infer Input, any, any, any, any> | CorePikkuFunctionSessionless<infer Input, any, any, any, any> ? Input : never : never;
103
+ /**
104
+ * Strip < > [ ] characters from a string
105
+ */
106
+ type StripBrackets<S extends string> = S extends `<${infer Inner}>` ? Inner : S extends `[${infer Inner}]` ? Inner : S;
107
+ /**
108
+ * Split string by spaces
109
+ */
110
+ type SplitBySpace<S extends string> = S extends `${infer First} ${infer Rest}` ? [First, ...SplitBySpace<Rest>] : S extends '' ? [] : [S];
111
+ /**
112
+ * Recursively build a tuple by stripping brackets from each element
113
+ */
114
+ type BuildParamsTuple<Parts extends readonly string[]> = Parts extends readonly [
115
+ infer First extends string,
116
+ ...infer Rest extends string[]
117
+ ] ? [StripBrackets<First>, ...BuildParamsTuple<Rest>] : [];
118
+ /**
119
+ * Extract parameter names from CLI parameter string into a tuple
120
+ * Example: "<env> [region]" => ["env", "region"]
121
+ */
122
+ type ExtractParameterNames<S extends string> = BuildParamsTuple<SplitBySpace<S>>;
123
+ /**
124
+ * Check if all elements of a tuple are valid keys of Input
125
+ */
126
+ type AllParamsValid<Params extends readonly any[], Input> = Params extends readonly [infer First, ...infer Rest] ? First extends keyof Input ? AllParamsValid<Rest, Input> : false : true;
127
+ /**
128
+ * Validate that all parameter names are valid keys of the function input
129
+ */
130
+ export type ValidateParameters<Params extends string, Input> = AllParamsValid<ExtractParameterNames<Params>, Input> extends true ? Params : never;
131
+ /**
132
+ * Extract output type from a Pikku function config type
133
+ */
134
+ export type ExtractFunctionOutput<Func> = Func extends CorePikkuFunctionConfig<infer FuncType, any, any> ? FuncType extends CorePikkuFunction<any, infer Output, any, any, any> | CorePikkuFunctionSessionless<any, infer Output, any, any, any> ? Output : never : never;
135
+ /**
136
+ * CLI command configuration that infers options from function input type.
137
+ * This is a helper type for creating type-safe CLI commands.
138
+ */
139
+ export type CoreCLICommandConfig<FuncConfig, PikkuMiddleware extends CorePikkuMiddleware<any> = CorePikkuMiddleware<any>, PikkuCLIRender extends CorePikkuCLIRender<any, any, any> = CorePikkuCLIRender<any, any>, Params extends string = string> = {
140
+ parameters?: ValidateParameters<Params, ExtractFunctionInput<FuncConfig>>;
141
+ func?: FuncConfig;
142
+ description?: string;
143
+ render?: PikkuCLIRender;
144
+ options?: {
145
+ [K in keyof ExtractFunctionInput<FuncConfig>]?: {
146
+ description?: string;
147
+ short?: string;
148
+ default?: ExtractFunctionInput<FuncConfig>[K];
149
+ };
150
+ };
151
+ middleware?: PikkuMiddleware[];
152
+ subcommands?: Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender, any>>;
153
+ auth?: boolean;
154
+ permissions?: any[];
155
+ isDefault?: boolean;
156
+ };
157
+ /**
158
+ * CLI command definition
159
+ */
160
+ export interface CoreCLICommand<In, Out, PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunction<In, Out, any, any, any> | CorePikkuFunctionSessionless<In, Out, any, any, any>>, PikkuPermission extends CorePikkuPermission<any, any, any>, PikkuMiddleware extends CorePikkuMiddleware, Options = any, Subcommands extends Record<string, CoreCLICommandConfig<any, any, any>> = Record<string, CoreCLICommandConfig<any, any, any>>> {
161
+ parameters?: string;
162
+ func: PikkuFunctionConfig;
163
+ render?: CorePikkuCLIRender<Out>;
164
+ description?: string;
165
+ options?: CLIOptions<Options>;
166
+ middleware?: PikkuMiddleware[];
167
+ permissions?: Record<string, PikkuPermission | PikkuPermission[]>;
168
+ auth?: boolean;
169
+ docs?: PikkuDocs;
170
+ subcommands?: Subcommands;
171
+ isDefault?: boolean;
172
+ }
173
+ /**
174
+ * Shorthand command definition (just a function)
175
+ */
176
+ export type CLICommandShorthand<In, Out, PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunction<In, Out, any, any, any> | CorePikkuFunctionSessionless<In, Out, any, any, any>>> = PikkuFunctionConfig;
177
+ /**
178
+ * Command definition (either full or shorthand)
179
+ */
180
+ export type CLICommandDefinition<In, Out, PikkuFunctionConfig extends CorePikkuFunctionConfig<CorePikkuFunction<In, Out, any, any, any> | CorePikkuFunctionSessionless<In, Out, any, any, any>>, PikkuPermission extends CorePikkuPermission<any, any, any>, PikkuMiddleware extends CorePikkuMiddleware, Options = any, Subcommands extends Record<string, CoreCLICommandConfig<any, any, any>> = Record<string, CoreCLICommandConfig<any, any, any>>> = CoreCLICommand<In, Out, PikkuFunctionConfig, PikkuPermission, PikkuMiddleware, Options, Subcommands> | CLICommandShorthand<In, Out, PikkuFunctionConfig>;
181
+ /**
182
+ * CLI wiring configuration
183
+ */
184
+ export interface CoreCLI<Commands extends Record<string, CoreCLICommandConfig<any, any, any>>, Options, PikkuMiddleware, PikkuCLIRender> {
185
+ program: string;
186
+ description?: string;
187
+ commands: Commands;
188
+ options?: CLIOptions<Options>;
189
+ middleware?: PikkuMiddleware[];
190
+ render?: PikkuCLIRender;
191
+ docs?: PikkuDocs;
192
+ tags?: string[];
193
+ }
194
+ /**
195
+ * Generic renderer type that can transform data into any output format.
196
+ * Can be used across different wirings for flexible output handling.
197
+ *
198
+ * @template Data - The input data type to be rendered
199
+ * @template Output - The output type after rendering
200
+ * @template Services - The services available to the renderer
201
+ * @template Session - The user session type
202
+ */
203
+ export type CorePikkuRender<Data, Output, Services extends CoreSingletonServices = CoreServices, Session extends CoreUserSession = CoreUserSession> = (services: Services, data: Data, session?: Session) => Output | Promise<Output>;
204
+ export {};