@pikku/cli 0.10.0 → 0.10.2

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 (152) hide show
  1. package/.pikku/channel/pikku-channel-types.gen.ts +4 -3
  2. package/.pikku/channel/pikku-channels-map.gen.d.ts +2 -2
  3. package/.pikku/channel/pikku-channels-meta.gen.ts +1 -1
  4. package/.pikku/channel/pikku-channels.gen.ts +1 -1
  5. package/.pikku/cli/pikku-cli-types.gen.ts +23 -1
  6. package/.pikku/cli/pikku-cli-wirings-meta.gen.ts +4 -115
  7. package/.pikku/cli/pikku-cli-wirings.gen.ts +2 -3
  8. package/.pikku/function/pikku-function-types.gen.ts +1 -1
  9. package/.pikku/function/pikku-functions-meta.gen.ts +156 -138
  10. package/.pikku/function/pikku-functions-meta.min.gen.ts +37 -32
  11. package/.pikku/function/pikku-functions.gen.ts +1 -1
  12. package/.pikku/http/pikku-http-types.gen.ts +1 -1
  13. package/.pikku/http/pikku-http-wirings-map.gen.d.ts +2 -2
  14. package/.pikku/http/pikku-http-wirings-meta.gen.ts +1 -1
  15. package/.pikku/http/pikku-http-wirings.gen.ts +1 -1
  16. package/.pikku/mcp/pikku-mcp-types.gen.ts +1 -1
  17. package/.pikku/mcp/pikku-mcp-wirings-meta.gen.ts +1 -1
  18. package/.pikku/mcp/pikku-mcp-wirings.gen.ts +1 -1
  19. package/.pikku/pikku-bootstrap.gen.ts +1 -1
  20. package/.pikku/pikku-services.gen.ts +16 -12
  21. package/.pikku/pikku-types.gen.ts +1 -1
  22. package/.pikku/pikku-websocket.gen.ts +15 -1
  23. package/.pikku/queue/pikku-queue-types.gen.ts +1 -1
  24. package/.pikku/queue/pikku-queue-workers-wirings-map.gen.d.ts +2 -2
  25. package/.pikku/queue/pikku-queue-workers-wirings-meta.gen.ts +1 -1
  26. package/.pikku/queue/pikku-queue-workers-wirings.gen.ts +1 -1
  27. package/.pikku/rpc/pikku-rpc-wirings-map.gen.d.ts +2 -2
  28. package/.pikku/rpc/pikku-rpc-wirings-map.internal.gen.d.ts +10 -9
  29. package/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.ts +9 -8
  30. package/.pikku/scheduler/pikku-scheduler-types.gen.ts +1 -1
  31. package/.pikku/scheduler/pikku-schedulers-wirings-meta.gen.ts +1 -1
  32. package/.pikku/scheduler/pikku-schedulers-wirings.gen.ts +1 -1
  33. package/.pikku/schemas/register.gen.ts +5 -5
  34. package/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  35. package/.pikku/schemas/schemas/PikkuChannelsOutput.schema.json +1 -1
  36. package/.pikku/schemas/schemas/PikkuSchemasOutput.schema.json +1 -1
  37. package/CHANGELOG.md +58 -0
  38. package/bin/pikku.ts +30 -21
  39. package/cli.schema.json +1 -1
  40. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +4 -3
  41. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  42. package/dist/.pikku/channel/pikku-channels-meta.gen.js +1 -1
  43. package/dist/.pikku/channel/pikku-channels.gen.d.ts +1 -1
  44. package/dist/.pikku/channel/pikku-channels.gen.js +1 -1
  45. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +18 -1
  46. package/dist/.pikku/cli/pikku-cli-types.gen.js +20 -1
  47. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +4 -115
  48. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -2
  49. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -2
  50. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  51. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  52. package/dist/.pikku/function/pikku-functions-meta.gen.js +156 -138
  53. package/dist/.pikku/function/pikku-functions-meta.min.gen.js +37 -32
  54. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  55. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  56. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  57. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  58. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  59. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  60. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  61. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  62. package/dist/.pikku/mcp/pikku-mcp-wirings-meta.gen.js +1 -1
  63. package/dist/.pikku/mcp/pikku-mcp-wirings.gen.d.ts +1 -1
  64. package/dist/.pikku/mcp/pikku-mcp-wirings.gen.js +1 -1
  65. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  66. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  67. package/dist/.pikku/pikku-services.gen.d.ts +9 -6
  68. package/dist/.pikku/pikku-services.gen.js +8 -2
  69. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  70. package/dist/.pikku/pikku-types.gen.js +1 -1
  71. package/dist/.pikku/pikku-websocket.gen.d.ts +15 -1
  72. package/dist/.pikku/pikku-websocket.gen.js +15 -1
  73. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  74. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  75. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  76. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  77. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  78. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +9 -8
  79. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  80. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  81. package/dist/.pikku/scheduler/pikku-schedulers-wirings-meta.gen.js +1 -1
  82. package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.d.ts +1 -1
  83. package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.js +1 -1
  84. package/dist/.pikku/schemas/register.gen.js +3 -3
  85. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  86. package/dist/.pikku/schemas/schemas/PikkuChannelsOutput.schema.json +1 -1
  87. package/dist/.pikku/schemas/schemas/PikkuSchemasOutput.schema.json +1 -1
  88. package/dist/bin/pikku.js +24 -19
  89. package/dist/src/cli.wiring.js +107 -99
  90. package/dist/src/functions/commands/all.js +31 -2
  91. package/dist/src/functions/commands/bootstrap.d.ts +1 -0
  92. package/dist/src/functions/commands/bootstrap.js +23 -0
  93. package/dist/src/functions/runtimes/nextjs/serialize-nextjs-backend-wrapper.js +46 -2
  94. package/dist/src/functions/wirings/channels/serialize-channel-types.js +3 -2
  95. package/dist/src/functions/wirings/channels/serialize-websocket-wrapper.js +14 -0
  96. package/dist/src/functions/wirings/cli/pikku-command-cli-entry.js +4 -4
  97. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.js +24 -4
  98. package/dist/src/functions/wirings/cli/serialize-channel-cli.js +32 -7
  99. package/dist/src/functions/wirings/cli/serialize-cli-types.js +22 -0
  100. package/dist/src/functions/wirings/functions/pikku-command-services.d.ts +1 -1
  101. package/dist/src/functions/wirings/functions/pikku-command-services.js +54 -26
  102. package/dist/src/functions/wirings/functions/schemas.js +2 -2
  103. package/dist/src/functions/wirings/http/pikku-command-openapi.js +1 -1
  104. package/dist/src/functions/wirings/middleware/pikku-command-middleware.js +3 -10
  105. package/dist/src/middleware/log-command-info-and-time.d.ts +1 -1
  106. package/dist/src/middleware/log-command-info-and-time.js +8 -5
  107. package/dist/src/services/cli-logger.service.d.ts +7 -2
  108. package/dist/src/services/cli-logger.service.js +16 -4
  109. package/dist/src/services.js +77 -12
  110. package/dist/src/utils/check-required-types.js +11 -1
  111. package/dist/src/utils/command-summary.d.ts +43 -0
  112. package/dist/src/utils/command-summary.js +73 -0
  113. package/dist/src/utils/file-writer.js +2 -2
  114. package/dist/src/utils/pikku-cli-config.js +28 -0
  115. package/dist/src/utils/schema-generator.d.ts +2 -2
  116. package/dist/src/utils/schema-generator.js +3 -3
  117. package/dist/tsconfig.tsbuildinfo +1 -1
  118. package/package.json +3 -4
  119. package/pikku.config.json +5 -2
  120. package/src/cli.wiring.ts +106 -101
  121. package/src/functions/commands/all.ts +38 -2
  122. package/src/functions/commands/bootstrap.ts +27 -0
  123. package/src/functions/runtimes/nextjs/serialize-nextjs-backend-wrapper.ts +46 -2
  124. package/src/functions/wirings/channels/serialize-channel-types.ts +3 -2
  125. package/src/functions/wirings/channels/serialize-websocket-wrapper.ts +14 -0
  126. package/src/functions/wirings/cli/pikku-command-cli-entry.ts +4 -4
  127. package/src/functions/wirings/cli/serialize-channel-cli-client.ts +24 -4
  128. package/src/functions/wirings/cli/serialize-channel-cli.ts +40 -8
  129. package/src/functions/wirings/cli/serialize-cli-types.ts +22 -0
  130. package/src/functions/wirings/functions/pikku-command-services.ts +57 -28
  131. package/src/functions/wirings/functions/schemas.ts +4 -3
  132. package/src/functions/wirings/http/pikku-command-openapi.ts +2 -1
  133. package/src/functions/wirings/middleware/pikku-command-middleware.ts +11 -22
  134. package/src/middleware/log-command-info-and-time.ts +8 -5
  135. package/src/services/cli-logger.service.ts +20 -5
  136. package/src/services.ts +86 -11
  137. package/src/utils/check-required-types.ts +16 -1
  138. package/src/utils/command-summary.ts +101 -0
  139. package/src/utils/file-writer.ts +2 -2
  140. package/src/utils/pikku-cli-config.ts +28 -0
  141. package/src/utils/schema-generator.ts +5 -4
  142. package/types/application-types.d.ts +5 -1
  143. package/types/config.d.ts +16 -6
  144. package/.pikku/cli/pikku-cli-channel.gen.ts +0 -34
  145. package/.pikku/cli/pikku-cli-client.gen.ts +0 -43
  146. package/.pikku/cli/pikku-cli.gen.ts +0 -41
  147. package/dist/.pikku/cli/pikku-cli-channel.gen.d.ts +0 -1
  148. package/dist/.pikku/cli/pikku-cli-channel.gen.js +0 -33
  149. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +0 -10
  150. package/dist/.pikku/cli/pikku-cli-client.gen.js +0 -34
  151. package/dist/.pikku/cli/pikku-cli.gen.d.ts +0 -10
  152. package/dist/.pikku/cli/pikku-cli.gen.js +0 -38
@@ -2,8 +2,10 @@ import { existsSync } from 'fs';
2
2
  import { pikkuVoidFunc } from '../../../.pikku/pikku-types.gen.js';
3
3
  import { getFileImportRelativePath } from '../../utils/file-import-path.js';
4
4
  import { writeFileInDir } from '../../utils/file-writer.js';
5
+ import { CommandSummary } from '../../utils/command-summary.js';
5
6
  export const all = pikkuVoidFunc({
6
7
  func: async ({ logger, config, rpc, getInspectorState }) => {
8
+ const summary = new CommandSummary('all');
7
9
  const allImports = [];
8
10
  let typesDeclarationFileExists = true;
9
11
  if (!existsSync(config.typesDeclarationFile)) {
@@ -12,7 +14,7 @@ export const all = pikkuVoidFunc({
12
14
  await rpc.invoke('pikkuFunctionTypes', null);
13
15
  // This is needed since the wireHTTP function will add the routes to the visitState
14
16
  if (!typesDeclarationFileExists) {
15
- logger.info(`• Type file first created, inspecting again...\x1b[0m`);
17
+ logger.debug(`• Type file first created, inspecting again...`);
16
18
  await getInspectorState(true);
17
19
  }
18
20
  // Generate wiring-specific type files for tree-shaking
@@ -97,7 +99,7 @@ export const all = pikkuVoidFunc({
97
99
  await rpc.invoke('pikkuNext', null);
98
100
  }
99
101
  if (config.openAPI) {
100
- logger.info(`• OpenAPI requires a reinspection to pickup new generated types..`);
102
+ logger.debug(`• OpenAPI requires a reinspection to pickup new generated types..`);
101
103
  await getInspectorState(true);
102
104
  await rpc.invoke('pikkuOpenAPI', null);
103
105
  }
@@ -106,6 +108,33 @@ export const all = pikkuVoidFunc({
106
108
  .map((to) => `import '${getFileImportRelativePath(config.bootstrapFile, to, config.packageMappings)}'`)
107
109
  .sort((to) => (to.includes('meta') ? -1 : 1)) // Ensure meta files are at the top
108
110
  .join('\n'));
111
+ // Get final inspector state and collect stats for summary
112
+ const state = await getInspectorState();
113
+ if (state.http?.meta)
114
+ summary.set('httpRoutes', Object.keys(state.http.meta).length);
115
+ if (state.channels?.meta)
116
+ summary.set('channels', Object.keys(state.channels.meta).length);
117
+ if (state.scheduledTasks?.meta)
118
+ summary.set('scheduledTasks', Object.keys(state.scheduledTasks.meta).length);
119
+ if (state.queueWorkers?.meta)
120
+ summary.set('queueWorkers', Object.keys(state.queueWorkers.meta).length);
121
+ if (state.mcpEndpoints) {
122
+ const mcpTotal = Object.keys(state.mcpEndpoints.toolsMeta || {}).length +
123
+ Object.keys(state.mcpEndpoints.resourcesMeta || {}).length +
124
+ Object.keys(state.mcpEndpoints.promptsMeta || {}).length;
125
+ if (mcpTotal > 0)
126
+ summary.set('mcpEndpoints', mcpTotal);
127
+ }
128
+ if (state.cli?.meta) {
129
+ // Count total CLI commands across all programs
130
+ const totalCommands = Object.values(state.cli.meta).reduce((sum, program) => sum + (program.commands?.length || 0), 0);
131
+ if (totalCommands > 0)
132
+ summary.set('cliCommands', totalCommands);
133
+ }
134
+ // Display summary (unless in silent mode)
135
+ if (!logger.isSilent()) {
136
+ console.log(summary.format());
137
+ }
109
138
  // Check for critical errors and exit if any were logged
110
139
  if (logger.hasCriticalErrors()) {
111
140
  process.exit(1);
@@ -0,0 +1 @@
1
+ export declare const bootstrap: any;
@@ -0,0 +1,23 @@
1
+ import { pikkuVoidFunc } from '../../../.pikku/pikku-types.gen.js';
2
+ export const bootstrap = pikkuVoidFunc({
3
+ func: async ({ logger, config, rpc, getInspectorState }) => {
4
+ // Initialize inspector state in bootstrap mode with core types only
5
+ // This allows bootstrap to run immediately without inspecting the codebase
6
+ // All subsequent RPC commands will use this cached state
7
+ await getInspectorState(false, false, true);
8
+ await rpc.invoke('pikkuFunctionTypes', null);
9
+ // Generate wiring-specific type files for tree-shaking
10
+ // These use the bootstrap mode state with core types
11
+ await rpc.invoke('pikkuFunctionTypesSplit', null);
12
+ await rpc.invoke('pikkuHTTPTypes', null);
13
+ await rpc.invoke('pikkuChannelTypes', null);
14
+ await rpc.invoke('pikkuSchedulerTypes', null);
15
+ await rpc.invoke('pikkuQueueTypes', null);
16
+ await rpc.invoke('pikkuMCPTypes', null);
17
+ await rpc.invoke('pikkuCLITypes', null);
18
+ // Check for critical errors and exit if any were logged
19
+ if (logger.hasCriticalErrors()) {
20
+ process.exit(1);
21
+ }
22
+ },
23
+ });
@@ -1,13 +1,16 @@
1
1
  export const serializeNextJsBackendWrapper = (bootstrapPath, routesMapPath, configImport, singleServicesFactoryImport, sessionServicesImport) => {
2
2
  return `'server-only'
3
-
3
+
4
4
  /**
5
5
  * This file provides a wrapper around the PikkuNextJS class to allow for methods to be type checked against your routes.
6
6
  * It ensures type safety for route handling methods when integrating with the @pikku/core framework.
7
7
  */
8
8
  import { PikkuNextJS } from '@pikku/next'
9
+ import { NextRequest } from 'next/server.js'
9
10
  import type { HTTPWiringsMap, HTTPWiringHandlerOf, HTTPWiringsWithMethod } from '${routesMapPath}'
10
11
 
12
+ type RouteContext = { params: Promise<Record<string, string | string[]>> }
13
+
11
14
  ${configImport}
12
15
  ${singleServicesFactoryImport}
13
16
  ${sessionServicesImport}
@@ -15,6 +18,11 @@ ${sessionServicesImport}
15
18
  import '${bootstrapPath}'
16
19
 
17
20
  let _pikku: PikkuNextJS | undefined
21
+ let _removeAPIPrefix = true
22
+
23
+ export const removeAPIPrefix = (enable: boolean) => {
24
+ _removeAPIPrefix = enable
25
+ }
18
26
 
19
27
  /**
20
28
  * Initializes and returns an instance of PikkuNextJS with helper methods for handling route requests.
@@ -172,8 +180,44 @@ export const pikku = (_options?: any) => {
172
180
  patch: dynamicPatch,
173
181
  del: dynamicDel,
174
182
  staticGet,
175
- staticPost
183
+ staticPost,
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Pre-bound API request handler for Next.js App Router route handlers.
189
+ * Use this to directly export route handlers without losing context.
190
+ *
191
+ * @param req - The Next.js request object.
192
+ * @param context - Next.js route context (unused by Pikku, but required by Next.js signature).
193
+ * @returns A promise that resolves to a Next.js Response object.
194
+ *
195
+ * @example
196
+ * export const GET = pikkuAPIRequest
197
+ * export const POST = pikkuAPIRequest
198
+ */
199
+ export const pikkuAPIRequest = (
200
+ req: NextRequest,
201
+ context: RouteContext
202
+ ): Promise<Response> => {
203
+ if (!_pikku) {
204
+ _pikku = new PikkuNextJS(
205
+ createConfig as any,
206
+ createSingletonServices as any,
207
+ createSessionServices
208
+ )
209
+ }
210
+ if (_removeAPIPrefix) {
211
+ const url = new URL(req.url)
212
+ url.pathname = url.pathname.replace(/^\\/api/, '') || '/'
213
+ req = new NextRequest(url.toString(), {
214
+ method: req.method,
215
+ headers: req.headers,
216
+ body: req.body,
217
+ duplex: 'half',
218
+ } as any)
176
219
  }
220
+ return _pikku.apiRequest(req)
177
221
  }
178
222
  `;
179
223
  };
@@ -10,11 +10,12 @@ export const serializeChannelTypes = (functionTypesImportPath) => {
10
10
  import { CoreChannel, wireChannel as wireChannelCore } from '@pikku/core/channel'
11
11
  import { CorePikkuFunctionConfig } from '@pikku/core'
12
12
  import { AssertHTTPWiringParams } from '@pikku/core/http'
13
- import type { PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware } from '${functionTypesImportPath}'
13
+ import type { PikkuFunction, PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware } from '${functionTypesImportPath}'
14
14
 
15
15
  /**
16
16
  * Type definition for WebSocket channels with typed data exchange.
17
17
  * Supports connection, disconnection, and message handling.
18
+ * Accepts both session-based (PikkuFunction) and sessionless (PikkuFunctionSessionless) functions.
18
19
  *
19
20
  * @template ChannelData - Type of data exchanged through the channel
20
21
  * @template Channel - String literal type for the channel name
@@ -24,7 +25,7 @@ type ChannelWiring<ChannelData, Channel extends string> = CoreChannel<
24
25
  Channel,
25
26
  CorePikkuFunctionConfig<PikkuFunctionSessionless<void, any, ChannelData>, PikkuPermission<void>, PikkuMiddleware>,
26
27
  CorePikkuFunctionConfig<PikkuFunctionSessionless<void, void, ChannelData>, PikkuPermission<void>, PikkuMiddleware>,
27
- CorePikkuFunctionConfig<PikkuFunctionSessionless<any, any, ChannelData>, PikkuPermission<any>, PikkuMiddleware>,
28
+ CorePikkuFunctionConfig<PikkuFunctionSessionless<any, any, ChannelData> | PikkuFunction<any, any, ChannelData>, PikkuPermission<any>, PikkuMiddleware>,
28
29
  PikkuPermission,
29
30
  PikkuMiddleware
30
31
  >
@@ -26,6 +26,20 @@ class PikkuWebSocketRoute<Channel extends keyof ChannelsMap, Route extends keyof
26
26
  }
27
27
  }
28
28
 
29
+ /**
30
+ * Type-safe WebSocket wrapper for Pikku channels.
31
+ *
32
+ * @example
33
+ * // Browser usage
34
+ * const ws = new WebSocket('ws://localhost:3000')
35
+ * const pikkuWS = new PikkuWebSocket<'events'>(ws)
36
+ *
37
+ * @example
38
+ * // Node.js usage
39
+ * import WebSocket from 'ws'
40
+ * const ws = new WebSocket('ws://localhost:3000')
41
+ * const pikkuWS = new PikkuWebSocket<'events'>(ws)
42
+ */
29
43
  export class PikkuWebSocket<Channel extends keyof ChannelsMap, EventHubTopics extends Record<string, any> = {}> extends CorePikkuWebsocket {
30
44
  /**
31
45
  * Send a message to a specific route and method.
@@ -35,8 +35,8 @@ export const pikkuCLIEntry = pikkuSessionlessFunc({
35
35
  for (const entrypointConfig of configs) {
36
36
  // Normalize entrypoint config to get type
37
37
  const entrypointType = typeof entrypointConfig === 'string'
38
- ? 'cli'
39
- : entrypointConfig.type || 'cli';
38
+ ? 'local'
39
+ : entrypointConfig.type || 'local';
40
40
  // Handle channel type entrypoint
41
41
  if (entrypointType === 'channel') {
42
42
  if (typeof entrypointConfig === 'string' ||
@@ -85,10 +85,10 @@ export const pikkuCLIEntry = pikkuSessionlessFunc({
85
85
  }
86
86
  continue;
87
87
  }
88
- // Handle CLI type entrypoint (default)
88
+ // Handle local CLI type entrypoint (default)
89
89
  const entrypointPath = typeof entrypointConfig === 'string'
90
90
  ? entrypointConfig
91
- : entrypointConfig.type === 'cli'
91
+ : entrypointConfig.type === 'local'
92
92
  ? entrypointConfig.path
93
93
  : undefined;
94
94
  if (!entrypointPath) {
@@ -102,8 +102,27 @@ export async function ${capitalizedName}CLIClient(
102
102
  url: string,
103
103
  args?: string[]
104
104
  ): Promise<void> {
105
+ // Get WebSocket implementation (browser or Node.js)
106
+ let WebSocketImpl: any
107
+ if (typeof WebSocket !== 'undefined') {
108
+ WebSocketImpl = WebSocket
109
+ } else {
110
+ // Node.js environment - dynamically import 'ws'
111
+ try {
112
+ const wsModule = await import('ws')
113
+ WebSocketImpl = wsModule.default
114
+ } catch (e) {
115
+ throw new Error(
116
+ 'No WebSocket implementation found. In Node.js environments, you need to:\\n' +
117
+ '1. Install the "ws" package: npm install ws\\n' +
118
+ 'Learn more: https://www.npmjs.com/package/ws'
119
+ )
120
+ }
121
+ }
122
+
105
123
  // Create WebSocket connection
106
- const pikkuWS = new CorePikkuWebsocket(url)
124
+ const ws = new WebSocketImpl(url) as WebSocket
125
+ const pikkuWS = new CorePikkuWebsocket(ws)
107
126
 
108
127
  // Register renderers for CLI commands
109
128
  const renderers = ${renderersMap}
@@ -121,10 +140,11 @@ export default ${capitalizedName}CLIClient
121
140
 
122
141
  // For direct execution (if this file is run directly)
123
142
  if (import.meta.url === \`file://\${process.argv[1]}\`) {
124
- const url = process.env.PIKKU_WS_URL || 'ws://localhost:3000${finalChannelRoute}'
143
+ const url = process.env.PIKKU_WS_URL || 'ws://localhost:4002${finalChannelRoute}'
125
144
  ${capitalizedName}CLIClient(url, process.argv.slice(2)).catch(error => {
126
- console.error('Fatal error:', error.message)
127
- process.exit(1)
145
+ console.error('Fatal channel CLI error:', error)
146
+ // TODO: We get an error code even when it exists cleanly, investigate
147
+ // process.exit(1)
128
148
  })
129
149
  }
130
150
  `;
@@ -24,12 +24,6 @@ export function serializeChannelCLI(programName, programMeta, channelFile, funct
24
24
  }
25
25
  };
26
26
  collectCommands(programMeta.commands);
27
- // Generate the wireChannel call
28
- const commandEntries = Object.entries(commandMap)
29
- .map(([commandKey, { pikkuFuncName }]) => {
30
- return ` '${commandKey}': ${pikkuFuncName}`;
31
- })
32
- .join(',\n');
33
27
  // Generate imports from function file locations
34
28
  const funcNames = [
35
29
  ...new Set(Object.values(commandMap).map((v) => v.pikkuFuncName)),
@@ -46,19 +40,50 @@ export function serializeChannelCLI(programName, programMeta, channelFile, funct
46
40
  .join('\n');
47
41
  // Get relative path to channel types file
48
42
  const channelTypesPath = getFileImportRelativePath(channelFile, channelTypesFile, packageMappings);
43
+ // Get relative path to function types file
44
+ const functionTypesPath = getFileImportRelativePath(channelFile, functionTypesFile, packageMappings);
49
45
  return `/**
50
46
  * WebSocket channel backend for '${programName}' CLI commands
51
47
  */
52
48
  import { wireChannel } from '${channelTypesPath}'
49
+ import { pikkuMiddleware } from '${functionTypesPath}'
53
50
  ${imports}
54
51
 
52
+ // Middleware to close the channel after CLI command completes
53
+ const cliCloseOnComplete = pikkuMiddleware(async (services, { channel }, next) => {
54
+ const closeChannel = () => {
55
+ setTimeout(async () => {
56
+ try {
57
+ // This gives time for the response to be sent before closing
58
+ await channel?.close()
59
+ } catch (err) {
60
+ // Ignore errors on close
61
+ }
62
+ }, 200)
63
+ }
64
+
65
+ try {
66
+ const result = await next()
67
+ closeChannel()
68
+ return result
69
+ } catch (error) {
70
+ closeChannel()
71
+ throw error
72
+ }
73
+ })
74
+
55
75
  wireChannel({
56
76
  name: '${finalChannelName}',
57
77
  route: '${finalChannelRoute}',
58
78
  auth: false,
59
79
  onMessageWiring: {
60
80
  command: {
61
- ${commandEntries}
81
+ ${Object.entries(commandMap)
82
+ .map(([commandKey, { pikkuFuncName }]) => ` '${commandKey}': {
83
+ func: ${pikkuFuncName},
84
+ middleware: [cliCloseOnComplete],
85
+ }`)
86
+ .join(',\n')}
62
87
  }
63
88
  },
64
89
  tags: ['cli', '${programName}']
@@ -24,6 +24,28 @@ ${userSessionTypeName !== 'Session' ? `type Session = ${userSessionTypeName}` :
24
24
  */
25
25
  type PikkuCLIRender<Data, RequiredServices extends SingletonServices = SingletonServices> = CorePikkuCLIRender<Data, RequiredServices, Session>
26
26
 
27
+ /**
28
+ * Creates a type-safe CLI renderer with access to your application's singleton services.
29
+ * The renderer receives the full singleton services and output data to format and display results.
30
+ *
31
+ * @template Data - The output data type from the CLI command
32
+ * @template RequiredServices - The minimum services required for type checking (defaults to SingletonServices)
33
+ * @param render - Function that receives singleton services and data to render output
34
+ * @returns A CLI renderer configuration
35
+ *
36
+ * @example
37
+ * \`\`\`typescript
38
+ * const myRenderer = pikkuCLIRender<MyData>(({ logger }, data) => {
39
+ * logger.info(data.message)
40
+ * })
41
+ * \`\`\`
42
+ */
43
+ export const pikkuCLIRender = <Data, RequiredServices extends SingletonServices = SingletonServices>(
44
+ render: (services: SingletonServices, data: Data) => void | Promise<void>
45
+ ): PikkuCLIRender<Data, RequiredServices> => {
46
+ return render as any
47
+ }
48
+
27
49
  /**
28
50
  * CLI command configuration with project-specific types.
29
51
  * Uses CoreCLICommandConfig from @pikku/core with local middleware and render types.
@@ -1,2 +1,2 @@
1
- export declare const serializeServicesMap: (requiredServices: Set<string>, forceRequiredServices: string[] | undefined, servicesImport: string, sessionServicesImport: string) => string;
1
+ export declare const serializeServicesMap: (allSingletonServices: string[], allSessionServices: string[], requiredServices: Set<string>, forceRequiredServices: string[] | undefined, servicesImport: string, sessionServicesImport: string) => string;
2
2
  export declare const pikkuServices: any;
@@ -3,7 +3,7 @@ import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
3
3
  import { checkRequiredTypes } from '../../../utils/check-required-types.js';
4
4
  import { writeFileInDir } from '../../../utils/file-writer.js';
5
5
  import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
6
- export const serializeServicesMap = (requiredServices, forceRequiredServices = [], servicesImport, sessionServicesImport) => {
6
+ export const serializeServicesMap = (allSingletonServices, allSessionServices, requiredServices, forceRequiredServices = [], servicesImport, sessionServicesImport) => {
7
7
  // Use pre-aggregated services from inspector state
8
8
  // This includes services from:
9
9
  // - Wired functions (HTTP, channels, queues, schedulers, MCP, CLI, RPC)
@@ -11,45 +11,73 @@ export const serializeServicesMap = (requiredServices, forceRequiredServices = [
11
11
  // - Permissions used by wired functions
12
12
  // - Session factories
13
13
  const usedServices = new Set(requiredServices);
14
- // Internal services that are created internally and not via the create service script
15
- const internalServices = new Set(['rpc', 'mcp', 'channel', 'userSession']);
14
+ // Internal services that are created internally by the framework (PikkuInteraction)
15
+ // These should not appear in the services maps
16
+ const internalServices = new Set([
17
+ 'rpc',
18
+ 'mcp',
19
+ 'channel',
20
+ 'userSession',
21
+ 'cli',
22
+ 'http',
23
+ 'queue',
24
+ 'scheduledTask',
25
+ ]);
16
26
  // Add force-required services that might not be detected from function inspection
17
27
  forceRequiredServices.forEach((service) => {
18
28
  if (!internalServices.has(service)) {
19
29
  usedServices.add(service);
20
30
  }
21
31
  });
22
- // Create a map of services with true for all needed services
23
- const servicesMap = Object.fromEntries(Array.from(usedServices)
24
- .sort()
25
- .map((service) => [service, true]));
26
- // Generate the TypeScript code
27
- const serviceKeys = Object.keys(servicesMap).sort();
28
32
  // Services that are always required internally by the framework
29
33
  const defaultServices = ['config', 'logger', 'variables', 'schema'];
30
- // Combine default services with detected services
31
- const allRequiredServices = [
32
- ...new Set([...defaultServices, ...serviceKeys]),
33
- ].sort();
34
- // For RequiredSingletonServices, we need to pick from the actual SingletonServices interface
35
- // This will be resolved at compile time based on what's actually in the SingletonServices interface
36
- // We don't need to hardcode which services are singletons beyond the core framework ones
34
+ defaultServices.forEach((service) => usedServices.add(service));
35
+ // Create singleton services map: all singleton services with true/false based on usage
36
+ const singletonServicesMap = {};
37
+ allSingletonServices.forEach((service) => {
38
+ singletonServicesMap[service] = usedServices.has(service);
39
+ });
40
+ // Create session services map: all session services with true/false based on usage
41
+ // Exclude internal framework services (PikkuInteraction)
42
+ const sessionServicesMap = {};
43
+ allSessionServices.forEach((service) => {
44
+ if (!internalServices.has(service)) {
45
+ sessionServicesMap[service] = usedServices.has(service);
46
+ }
47
+ });
48
+ // Get all required service names (those marked as true)
49
+ const requiredSingletonServiceNames = Object.keys(singletonServicesMap)
50
+ .filter((key) => singletonServicesMap[key])
51
+ .sort();
52
+ const requiredSessionServiceNames = Object.keys(sessionServicesMap)
53
+ .filter((key) => sessionServicesMap[key])
54
+ .sort();
37
55
  const code = [
38
56
  servicesImport,
39
57
  sessionServicesImport,
40
- "import type { PikkuInteraction } from '@pikku/core'",
41
58
  '',
42
- 'export const singletonServices = {',
43
- ...Object.keys(servicesMap).map((service) => ` '${service}': true,`),
59
+ '// Singleton services map: true if required, false if available but unused',
60
+ 'export const requiredSingletonServices = {',
61
+ ...Object.keys(singletonServicesMap)
62
+ .sort()
63
+ .map((service) => ` '${service}': ${singletonServicesMap[service]},`),
64
+ '} as const',
65
+ '',
66
+ '// Session services map: true if required, false if available but unused',
67
+ 'export const requiredSessionServices = {',
68
+ ...Object.keys(sessionServicesMap)
69
+ .sort()
70
+ .map((service) => ` '${service}': ${sessionServicesMap[service]},`),
44
71
  '} as const',
45
72
  '',
46
- '// Singleton services (created once at startup)',
47
- '// Only includes services that are both required and available in SingletonServices',
48
- `export type RequiredSingletonServices = Pick<SingletonServices, Extract<keyof SingletonServices, ${allRequiredServices.map((key) => `'${key}'`).join(' | ')}>> & Partial<Omit<SingletonServices, ${allRequiredServices.map((key) => `'${key}'`).join(' | ')}>>`,
73
+ '// Type exports',
74
+ requiredSingletonServiceNames.length > 0
75
+ ? `export type RequiredSingletonServices = Pick<SingletonServices, ${requiredSingletonServiceNames.map((key) => `'${key}'`).join(' | ')}> & Partial<Omit<SingletonServices, ${requiredSingletonServiceNames.map((key) => `'${key}'`).join(' | ')}>>`
76
+ : 'export type RequiredSingletonServices = Partial<SingletonServices>',
49
77
  '',
50
- '// Session services (created per request, can access singleton services)',
51
- '// Omits singleton services and PikkuInteraction (mcp, rpc, http, channel)',
52
- `export type RequiredSessionServices = Omit<Services, keyof SingletonServices | keyof PikkuInteraction>`,
78
+ requiredSessionServiceNames.length > 0
79
+ ? `export type RequiredSessionServices = Pick<Services, ${requiredSessionServiceNames.map((key) => `'${key}'`).join(' | ')}> & Partial<Omit<Services, ${requiredSessionServiceNames.map((key) => `'${key}'`).join(' | ')}>>`
80
+ : 'export type RequiredSessionServices = Partial<Services>',
53
81
  '',
54
82
  ].join('\n');
55
83
  return code;
@@ -68,7 +96,7 @@ export const pikkuServices = pikkuSessionlessFunc({
68
96
  }
69
97
  const servicesImport = `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(config.typesDeclarationFile, singletonServicesType.typePath, config.packageMappings)}'`;
70
98
  const sessionServicesImport = `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(config.typesDeclarationFile, sessionServicesType.typePath, config.packageMappings)}'`;
71
- const servicesCode = serializeServicesMap(visitState.serviceAggregation.requiredServices, config.forceRequiredServices, servicesImport, sessionServicesImport);
99
+ const servicesCode = serializeServicesMap(visitState.serviceAggregation.allSingletonServices, visitState.serviceAggregation.allSessionServices, visitState.serviceAggregation.requiredServices, config.forceRequiredServices, servicesImport, sessionServicesImport);
72
100
  await writeFileInDir(logger, config.servicesFile, servicesCode);
73
101
  },
74
102
  middleware: [
@@ -7,8 +7,8 @@ import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-
7
7
  export const pikkuSchemas = pikkuSessionlessFunc({
8
8
  func: async ({ logger, config, getInspectorState }) => {
9
9
  const visitState = await getInspectorState();
10
- const schemas = await generateSchemas(logger, config.tsconfig, visitState.functions.typesMap, visitState.functions.meta, visitState.http.meta, config.schemasFromTypes);
11
- await saveSchemas(logger, config.schemaDirectory, schemas, visitState.functions.typesMap, visitState.functions.meta, config.supportsImportAttributes, config.schemasFromTypes);
10
+ const schemas = await generateSchemas(logger, config.tsconfig, visitState.functions.typesMap, visitState.functions.meta, visitState.http.meta, config.schemasFromTypes, config.schema?.additionalProperties);
11
+ await saveSchemas(logger, config.schemaDirectory, schemas, visitState.functions.typesMap, visitState.functions.meta, config.schemasFromTypes, config.schema?.supportsImportAttributes);
12
12
  return true;
13
13
  },
14
14
  middleware: [
@@ -16,7 +16,7 @@ export const pikkuOpenAPI = pikkuSessionlessFunc({
16
16
  return;
17
17
  }
18
18
  const { http, functions } = await getInspectorState();
19
- const schemas = await generateSchemas(logger, tsconfig, functions.typesMap, functions.meta, http.meta, schemasFromTypes);
19
+ const schemas = await generateSchemas(logger, tsconfig, functions.typesMap, functions.meta, http.meta, schemasFromTypes, config.schema?.additionalProperties);
20
20
  const openAPISpec = await generateOpenAPISpec(logger, functions.meta, http.meta, schemas, openAPI.additionalInfo);
21
21
  if (openAPI.outputFile.endsWith('.json')) {
22
22
  await writeFileInDir(logger, openAPI.outputFile, JSON.stringify(openAPISpec, null, 2), { ignoreModifyComment: true });
@@ -9,20 +9,13 @@ export const pikkuMiddleware = pikkuSessionlessFunc({
9
9
  const { middleware } = state;
10
10
  const { middlewareFile, packageMappings } = config;
11
11
  let filesGenerated = false;
12
- // Check if there are any middleware group factories
13
- const hasHTTPFactories = Array.from(state.http.routeMiddleware.values()).some((meta) => meta.exportName && meta.isFactory);
14
- const hasTagFactories = Array.from(state.middleware.tagMiddleware.values()).some((meta) => meta.exportName && meta.isFactory);
15
- const hasFactories = hasHTTPFactories || hasTagFactories;
16
- // Generate middleware imports file if there are factories
17
- if (hasFactories) {
18
- await writeFileInDir(logger, middlewareFile, serializeMiddlewareImports(middlewareFile, middleware, state.http, packageMappings));
19
- filesGenerated = true;
20
- }
21
- // Generate middleware groups metadata file
12
+ // Check if there are any middleware groups
22
13
  const hasHTTPGroups = state.http.routeMiddleware.size > 0;
23
14
  const hasTagGroups = state.middleware.tagMiddleware.size > 0;
24
15
  if (hasHTTPGroups || hasTagGroups) {
25
16
  await writeFileInDir(logger, config.middlewareGroupsMetaFile, serializeMiddlewareGroupsMeta(state));
17
+ // Always generate middleware imports file when groups exist (even if empty)
18
+ await writeFileInDir(logger, middlewareFile, serializeMiddlewareImports(middlewareFile, middleware, state.http, packageMappings));
26
19
  filesGenerated = true;
27
20
  }
28
21
  return filesGenerated;
@@ -7,7 +7,7 @@ export interface LogCommandInfoOptions {
7
7
  }
8
8
  /**
9
9
  * Middleware to log command execution timing and status
10
- * Replaces the logCommandInfoAndTime wrapper function
10
+ * Uses debug level so it only shows with --verbose flag
11
11
  */
12
12
  export declare const logCommandInfoAndTime: ({ commandStart, commandEnd, }: LogCommandInfoOptions) => PikkuMiddleware;
13
13
  export {};
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Middleware to log command execution timing and status
3
- * Replaces the logCommandInfoAndTime wrapper function
3
+ * Uses debug level so it only shows with --verbose flag
4
4
  */
5
5
  export const logCommandInfoAndTime = ({ commandStart, commandEnd, }) => {
6
6
  return async ({ logger }, _interaction, next) => {
7
- // Log start
7
+ // Log start (debug level - only shows with --verbose)
8
8
  const start = Date.now();
9
- logger.info(`• ${commandStart}...`);
9
+ logger.debug(`• ${commandStart}...`);
10
10
  // Execute the function
11
11
  await next();
12
- // Log completion
13
- logger.info(`✓ ${commandEnd} in ${Date.now() - start}ms.`);
12
+ // Log completion (debug level - only shows with --verbose)
13
+ logger.debug({
14
+ type: 'success',
15
+ message: `✓ ${commandEnd} in ${Date.now() - start}ms.`,
16
+ });
14
17
  };
15
18
  };
@@ -9,15 +9,20 @@ export declare class CLILogger implements Logger {
9
9
  silent?: boolean;
10
10
  });
11
11
  setLevel(level: LogLevel): void;
12
+ setSilent(silent: boolean): void;
13
+ isSilent(): boolean;
12
14
  info(message: string | {
13
15
  message: string;
14
16
  type?: string;
15
17
  }): void;
16
18
  error(message: string): void;
17
19
  warn(message: string): void;
18
- debug(message: string): void;
20
+ debug(message: string | {
21
+ message: string;
22
+ type?: string;
23
+ }): void;
19
24
  critical(code: ErrorCode, message: string): void;
20
25
  hasCriticalErrors(): boolean;
21
- private logPikkuLogo;
26
+ logLogo(): void;
22
27
  private primary;
23
28
  }