@pikku/inspector 0.12.2 → 0.12.4

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 (70) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/add/add-ai-agent.js +4 -0
  3. package/dist/add/add-approval-description.d.ts +5 -0
  4. package/dist/add/add-approval-description.js +52 -0
  5. package/dist/add/add-channel.js +42 -4
  6. package/dist/add/add-cli.js +73 -13
  7. package/dist/add/add-file-with-factory.js +1 -0
  8. package/dist/add/add-functions.js +22 -3
  9. package/dist/add/add-gateway.js +5 -0
  10. package/dist/add/add-http-route.js +5 -0
  11. package/dist/add/add-mcp-prompt.js +5 -0
  12. package/dist/add/add-mcp-resource.js +5 -0
  13. package/dist/add/add-middleware.js +6 -10
  14. package/dist/add/add-permission.js +10 -12
  15. package/dist/add/add-queue-worker.js +5 -0
  16. package/dist/add/add-schedule.js +5 -0
  17. package/dist/add/add-wire-addon.js +7 -0
  18. package/dist/add/add-workflow.js +7 -1
  19. package/dist/error-codes.d.ts +1 -0
  20. package/dist/error-codes.js +2 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/inspector.js +21 -7
  24. package/dist/types.d.ts +12 -0
  25. package/dist/utils/custom-types-generator.js +1 -0
  26. package/dist/utils/load-addon-functions-meta.d.ts +12 -0
  27. package/dist/utils/load-addon-functions-meta.js +76 -0
  28. package/dist/utils/post-process.d.ts +9 -0
  29. package/dist/utils/post-process.js +72 -0
  30. package/dist/utils/resolve-function-meta.d.ts +11 -0
  31. package/dist/utils/resolve-function-meta.js +17 -0
  32. package/dist/utils/schema-generator.js +26 -6
  33. package/dist/utils/serialize-inspector-state.d.ts +2 -0
  34. package/dist/utils/serialize-inspector-state.js +5 -0
  35. package/dist/utils/serialize-mcp-json.js +13 -7
  36. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +1 -0
  37. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +2 -0
  38. package/dist/visit.js +2 -0
  39. package/package.json +4 -3
  40. package/src/add/add-ai-agent.ts +6 -0
  41. package/src/add/add-approval-description.ts +76 -0
  42. package/src/add/add-channel.ts +44 -4
  43. package/src/add/add-cli.ts +108 -21
  44. package/src/add/add-file-with-factory.ts +1 -0
  45. package/src/add/add-functions.ts +28 -3
  46. package/src/add/add-gateway.ts +6 -0
  47. package/src/add/add-http-route.ts +6 -0
  48. package/src/add/add-mcp-prompt.ts +6 -0
  49. package/src/add/add-mcp-resource.ts +6 -0
  50. package/src/add/add-middleware.ts +6 -14
  51. package/src/add/add-permission.ts +10 -16
  52. package/src/add/add-queue-worker.ts +6 -0
  53. package/src/add/add-schedule.ts +6 -0
  54. package/src/add/add-wire-addon.ts +8 -0
  55. package/src/add/add-workflow.ts +11 -1
  56. package/src/error-codes.ts +3 -0
  57. package/src/index.ts +1 -0
  58. package/src/inspector.ts +33 -6
  59. package/src/types.ts +13 -0
  60. package/src/utils/custom-types-generator.ts +1 -0
  61. package/src/utils/load-addon-functions-meta.ts +94 -0
  62. package/src/utils/post-process.ts +84 -0
  63. package/src/utils/resolve-function-meta.ts +25 -0
  64. package/src/utils/schema-generator.ts +38 -10
  65. package/src/utils/serialize-inspector-state.ts +7 -0
  66. package/src/utils/serialize-mcp-json.ts +12 -7
  67. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -0
  68. package/src/utils/workflow/graph/workflow-graph.types.ts +2 -0
  69. package/src/visit.ts +2 -0
  70. package/tsconfig.tsbuildinfo +1 -1
@@ -38,13 +38,15 @@ function renameTempDefinitions(
38
38
  }
39
39
  }
40
40
 
41
- function isInsidePermissionFactory(node: ts.Node): boolean {
41
+ function isInsidePermissionContainer(node: ts.Node): boolean {
42
42
  let current = node.parent
43
43
  while (current) {
44
44
  if (
45
45
  ts.isCallExpression(current) &&
46
46
  ts.isIdentifier(current.expression) &&
47
- current.expression.text === 'pikkuPermissionFactory'
47
+ (current.expression.text === 'pikkuPermissionFactory' ||
48
+ current.expression.text === 'addPermission' ||
49
+ current.expression.text === 'addHTTPPermission')
48
50
  ) {
49
51
  return true
50
52
  }
@@ -69,7 +71,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
69
71
  // Handle pikkuPermission(...) - individual permission function definition
70
72
  if (expression.text === 'pikkuPermission') {
71
73
  // Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
72
- if (isInsidePermissionFactory(node)) return
74
+ if (isInsidePermissionContainer(node)) return
73
75
 
74
76
  const arg = args[0]
75
77
  if (!arg) return
@@ -156,7 +158,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
156
158
  }
157
159
 
158
160
  if (expression.text === 'pikkuAuth') {
159
- if (isInsidePermissionFactory(node)) return
161
+ if (isInsidePermissionContainer(node)) return
160
162
 
161
163
  const arg = args[0]
162
164
  if (!arg) return
@@ -373,13 +375,10 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
373
375
  state.rootDir
374
376
  )
375
377
 
376
- if (permissionNames.length === 0) {
377
- logger.warn(`• addPermission('${tag}', ...) has empty permissions array`)
378
- return
378
+ if (permissionNames.length > 0) {
379
+ renameTempDefinitions(state, permissionNames, 'tag', tag)
379
380
  }
380
381
 
381
- renameTempDefinitions(state, permissionNames, 'tag', tag)
382
-
383
382
  const allServices = new Set<string>()
384
383
  for (const permissionName of permissionNames) {
385
384
  const permissionMeta = state.permissions.definitions[permissionName]
@@ -479,15 +478,10 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
479
478
  state.rootDir
480
479
  )
481
480
 
482
- if (permissionNames.length === 0) {
483
- logger.warn(
484
- `• addHTTPPermission('${pattern}', ...) has empty permissions array`
485
- )
486
- return
481
+ if (permissionNames.length > 0) {
482
+ renameTempDefinitions(state, permissionNames, 'http', pattern)
487
483
  }
488
484
 
489
- renameTempDefinitions(state, permissionNames, 'http', pattern)
490
-
491
485
  const allServices = new Set<string>()
492
486
  for (const permissionName of permissionNames) {
493
487
  const permissionMeta = state.permissions.definitions[permissionName]
@@ -11,6 +11,7 @@ import {
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
12
  import { resolveMiddleware } from '../utils/middleware.js'
13
13
  import { extractWireNames } from '../utils/post-process.js'
14
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
14
15
  import { ErrorCode } from '../error-codes.js'
15
16
 
16
17
  export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
@@ -65,6 +66,10 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
65
66
  pikkuFuncId = makeContextBasedId('queue', name)
66
67
  }
67
68
 
69
+ const packageName = ts.isIdentifier(funcInitializer)
70
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
71
+ : null
72
+
68
73
  if (!name) {
69
74
  logger.critical(
70
75
  ErrorCode.MISSING_QUEUE_NAME,
@@ -85,6 +90,7 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
85
90
  state.queueWorkers.files.add(node.getSourceFile().fileName)
86
91
  state.queueWorkers.meta[name] = {
87
92
  pikkuFuncId,
93
+ ...(packageName && { packageName }),
88
94
  name,
89
95
  summary,
90
96
  description,
@@ -11,6 +11,7 @@ import {
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
12
  import { resolveMiddleware } from '../utils/middleware.js'
13
13
  import { extractWireNames } from '../utils/post-process.js'
14
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
14
15
 
15
16
  import { ErrorCode } from '../error-codes.js'
16
17
  export const addSchedule: AddWiring = (
@@ -71,6 +72,10 @@ export const addSchedule: AddWiring = (
71
72
  pikkuFuncId = makeContextBasedId('scheduler', nameValue)
72
73
  }
73
74
 
75
+ const packageName = ts.isIdentifier(funcInitializer)
76
+ ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
77
+ : null
78
+
74
79
  if (!nameValue || !scheduleValue) {
75
80
  return
76
81
  }
@@ -87,6 +92,7 @@ export const addSchedule: AddWiring = (
87
92
  state.scheduledTasks.files.add(node.getSourceFile().fileName)
88
93
  state.scheduledTasks.meta[nameValue] = {
89
94
  pikkuFuncId,
95
+ ...(packageName && { packageName }),
90
96
  name: nameValue,
91
97
  schedule: scheduleValue,
92
98
  summary,
@@ -40,6 +40,7 @@ export function addWireAddon(
40
40
  let name: string | undefined
41
41
  let pkg: string | undefined
42
42
  let rpcEndpoint: string | undefined
43
+ let mcp: boolean | undefined
43
44
  let secretOverrides: Record<string, string> | undefined
44
45
  let variableOverrides: Record<string, string> | undefined
45
46
 
@@ -53,6 +54,12 @@ export function addWireAddon(
53
54
  pkg = prop.initializer.text
54
55
  } else if (key === 'rpcEndpoint' && ts.isStringLiteral(prop.initializer)) {
55
56
  rpcEndpoint = prop.initializer.text
57
+ } else if (
58
+ key === 'mcp' &&
59
+ (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
60
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
61
+ ) {
62
+ mcp = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
56
63
  } else if (
57
64
  key === 'secretOverrides' &&
58
65
  ts.isObjectLiteralExpression(prop.initializer)
@@ -72,6 +79,7 @@ export function addWireAddon(
72
79
  state.rpc.wireAddonDeclarations.set(name, {
73
80
  package: pkg,
74
81
  rpcEndpoint,
82
+ mcp,
75
83
  secretOverrides,
76
84
  variableOverrides,
77
85
  })
@@ -11,7 +11,10 @@ import {
11
11
  extractDescription,
12
12
  extractDuration,
13
13
  } from '../utils/extract-node-value.js'
14
- import { getCommonWireMetaData } from '../utils/get-property-value.js'
14
+ import {
15
+ getCommonWireMetaData,
16
+ getPropertyValue,
17
+ } from '../utils/get-property-value.js'
15
18
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
16
19
 
17
20
  /**
@@ -206,6 +209,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
206
209
  let summary: string | undefined
207
210
  let description: string | undefined
208
211
  let errors: string[] | undefined
212
+ let inline: boolean | undefined
209
213
 
210
214
  if (ts.isObjectLiteralExpression(firstArg)) {
211
215
  const metadata = getCommonWireMetaData(
@@ -219,6 +223,11 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
219
223
  summary = metadata.summary
220
224
  description = metadata.description
221
225
  errors = metadata.errors
226
+
227
+ const inlineProp = getPropertyValue(firstArg, 'inline')
228
+ if (inlineProp === true) {
229
+ inline = true
230
+ }
222
231
  }
223
232
 
224
233
  // Validate that we got a valid function
@@ -324,5 +333,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
324
333
  description,
325
334
  errors,
326
335
  tags,
336
+ inline,
327
337
  }
328
338
  }
@@ -66,6 +66,9 @@ export enum ErrorCode {
66
66
  MISSING_MODEL = 'PKU145',
67
67
  INVALID_MODEL = 'PKU146',
68
68
 
69
+ // File structure errors
70
+ SCHEMA_AND_WIRING_COLOCATED = 'PKU490',
71
+
69
72
  // Optimization diagnostics
70
73
  SERVICES_NOT_DESTRUCTURED = 'PKU410',
71
74
  WIRES_NOT_DESTRUCTURED = 'PKU411',
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  deserializeAllDslWorkflows,
33
33
  } from './utils/workflow/dsl/index.js'
34
34
  export { getFilesAndMethods } from './utils/get-files-and-methods.js'
35
+ export { resolveFunctionMeta } from './utils/resolve-function-meta.js'
35
36
  export type {
36
37
  SerializedWorkflowGraph,
37
38
  SerializedWorkflowGraphs,
package/src/inspector.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  computePermissionsGroupsMeta,
21
21
  computeRequiredSchemas,
22
22
  computeDiagnostics,
23
+ validateSchemaWiringSeparation,
23
24
  } from './utils/post-process.js'
24
25
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js'
25
26
  import { pikkuState } from '@pikku/core/internal'
@@ -30,6 +31,10 @@ import {
30
31
  finalizeWorkflowWires,
31
32
  } from './utils/workflow/graph/finalize-workflow-wires.js'
32
33
  import { generateAllSchemas } from './utils/schema-generator.js'
34
+ import {
35
+ loadAddonFunctionsMeta,
36
+ loadAddonSchemas,
37
+ } from './utils/load-addon-functions-meta.js'
33
38
  import {
34
39
  computeContractHashes,
35
40
  extractContractsFromMeta,
@@ -63,6 +68,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
63
68
  typesMap: new TypesMap(),
64
69
  meta: {},
65
70
  files: new Map(),
71
+ approvalDescriptions: {},
66
72
  },
67
73
  http: {
68
74
  metaInputTypes: new Map(),
@@ -197,6 +203,8 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
197
203
  requiredSchemas: new Set(),
198
204
  openAPISpec: null,
199
205
  diagnostics: [],
206
+ addonFunctions: {},
207
+ program: null,
200
208
  }
201
209
  }
202
210
 
@@ -205,8 +213,7 @@ export const inspect = async (
205
213
  routeFiles: string[],
206
214
  options: InspectorOptions = {}
207
215
  ): Promise<InspectorState> => {
208
- const startProgram = performance.now()
209
- const program = ts.createProgram(routeFiles, {
216
+ const compilerOptions: ts.CompilerOptions = {
210
217
  target: ts.ScriptTarget.ESNext,
211
218
  module: ts.ModuleKind.Node16,
212
219
  skipLibCheck: true,
@@ -215,9 +222,16 @@ export const inspect = async (
215
222
  types: [],
216
223
  allowJs: false,
217
224
  checkJs: false,
218
- })
225
+ }
226
+ const startProgram = performance.now()
227
+ const program = ts.createProgram(
228
+ routeFiles,
229
+ compilerOptions,
230
+ undefined, // host
231
+ options.oldProgram
232
+ )
219
233
  logger.debug(
220
- `Created program in ${(performance.now() - startProgram).toFixed(2)}ms`
234
+ `Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`
221
235
  )
222
236
 
223
237
  const startChecker = performance.now()
@@ -249,9 +263,12 @@ export const inspect = async (
249
263
  )
250
264
  }
251
265
  logger.debug(
252
- `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`
266
+ `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`
253
267
  )
254
268
 
269
+ // Load addon function metadata so wirings can reference addon functions
270
+ await loadAddonFunctionsMeta(logger, state)
271
+
255
272
  if (!options.setupOnly) {
256
273
  // Second sweep: add all transports
257
274
  const startRoutes = performance.now()
@@ -261,17 +278,21 @@ export const inspect = async (
261
278
  )
262
279
  }
263
280
  logger.debug(
264
- `Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`
281
+ `Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`
265
282
  )
266
283
 
267
284
  resolveLatestVersions(state, logger)
268
285
 
269
286
  if (options.schemaConfig) {
287
+ const startSchemas = performance.now()
270
288
  state.schemas = await generateAllSchemas(
271
289
  logger,
272
290
  options.schemaConfig,
273
291
  state
274
292
  )
293
+ logger.debug(
294
+ `generateAllSchemas took ${(performance.now() - startSchemas).toFixed(0)}ms`
295
+ )
275
296
  computeContractHashes(
276
297
  state.schemas,
277
298
  state.functions.typesMap,
@@ -280,6 +301,9 @@ export const inspect = async (
280
301
  computeRequiredSchemas(state, options)
281
302
  }
282
303
 
304
+ // Re-load addon schemas (generateAllSchemas replaces state.schemas)
305
+ await loadAddonSchemas(logger, state)
306
+
283
307
  state.manifest.initial = options.manifest ?? null
284
308
  const contracts = extractContractsFromMeta(state.functions.meta)
285
309
  const baseManifest = state.manifest.initial ?? createEmptyManifest()
@@ -311,6 +335,7 @@ export const inspect = async (
311
335
  computeMiddlewareGroupsMeta(state)
312
336
  computePermissionsGroupsMeta(state)
313
337
  computeDiagnostics(state)
338
+ validateSchemaWiringSeparation(logger, state)
314
339
 
315
340
  if (options.openAPI) {
316
341
  state.openAPISpec = await generateOpenAPISpec(
@@ -329,5 +354,7 @@ export const inspect = async (
329
354
  validateVariableOverrides(logger, state)
330
355
  }
331
356
 
357
+ state.program = program
358
+
332
359
  return state
333
360
  }
package/src/types.ts CHANGED
@@ -98,6 +98,7 @@ export interface InspectorFunctionState {
98
98
  typesMap: TypesMap
99
99
  meta: FunctionsMeta
100
100
  files: Map<string, { path: string; exportedName: string }>
101
+ approvalDescriptions: Record<string, InspectorApprovalDescriptionDefinition>
101
102
  }
102
103
 
103
104
  export interface InspectorChannelState {
@@ -140,6 +141,14 @@ export interface InspectorAIMiddlewareState {
140
141
  definitions: Record<string, InspectorMiddlewareDefinition>
141
142
  }
142
143
 
144
+ export interface InspectorApprovalDescriptionDefinition {
145
+ services: FunctionServicesMeta
146
+ wires?: FunctionWiresMeta
147
+ sourceFile: string
148
+ position: number
149
+ exportedName: string | null
150
+ }
151
+
143
152
  export interface InspectorPermissionDefinition {
144
153
  services: FunctionServicesMeta
145
154
  wires?: FunctionWiresMeta
@@ -216,6 +225,7 @@ export type InspectorOptions = Partial<{
216
225
  tags: string[]
217
226
  manifest: VersionManifest
218
227
  modelConfig: InspectorModelConfig
228
+ oldProgram: ts.Program | undefined
219
229
  }>
220
230
 
221
231
  export interface InspectorLogger {
@@ -340,6 +350,7 @@ export interface InspectorState {
340
350
  {
341
351
  package: string
342
352
  rpcEndpoint?: string
353
+ mcp?: boolean
343
354
  secretOverrides?: Record<string, string>
344
355
  variableOverrides?: Record<string, string>
345
356
  }
@@ -409,4 +420,6 @@ export interface InspectorState {
409
420
  requiredSchemas: Set<string>
410
421
  openAPISpec: Record<string, any> | null
411
422
  diagnostics: InspectorDiagnostic[]
423
+ addonFunctions: Record<string, FunctionsMeta> // namespace -> addon's function metadata
424
+ program: ts.Program | null // Retained for incremental re-inspection
412
425
  }
@@ -16,6 +16,7 @@ export function generateCustomTypes(
16
16
  requiredTypes: Set<string>
17
17
  ) {
18
18
  const typeDeclarations = Array.from(typesMap.customTypes.entries())
19
+ .sort(([a], [b]) => a.localeCompare(b))
19
20
  .filter(([_name, { type }]) => {
20
21
  const hasUndefinedGeneric =
21
22
  /\b(Name|In|Out|Key)\b/.test(type) && /\[.*\]/.test(type)
@@ -0,0 +1,94 @@
1
+ import { readFile, readdir } from 'fs/promises'
2
+ import { createRequire } from 'module'
3
+ import { join, dirname } from 'path'
4
+ import type { InspectorState, InspectorLogger } from '../types.js'
5
+
6
+ /**
7
+ * After the setup sweep discovers wireAddon() declarations, load each addon
8
+ * package's function metadata so that wiring handlers (channels, HTTP routes,
9
+ * schedules, etc.) can look up addon function types during the routes sweep.
10
+ */
11
+ export async function loadAddonFunctionsMeta(
12
+ logger: InspectorLogger,
13
+ state: InspectorState
14
+ ): Promise<void> {
15
+ const { wireAddonDeclarations } = state.rpc
16
+ if (wireAddonDeclarations.size === 0) return
17
+
18
+ const require = createRequire(join(state.rootDir, 'package.json'))
19
+
20
+ for (const [namespace, decl] of wireAddonDeclarations) {
21
+ try {
22
+ const metaPath = require.resolve(
23
+ `${decl.package}/.pikku/function/pikku-functions-meta.gen.json`
24
+ )
25
+ const raw = await readFile(metaPath, 'utf-8')
26
+ const meta = JSON.parse(raw)
27
+ state.addonFunctions[namespace] = meta
28
+ logger.debug(
29
+ `Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`
30
+ )
31
+
32
+ // If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
33
+ if (decl.mcp) {
34
+ for (const [funcName, funcMeta] of Object.entries<any>(meta)) {
35
+ if (funcMeta.mcp) {
36
+ const toolName = `${namespace}:${funcName}`
37
+ state.mcpEndpoints.toolsMeta[toolName] = {
38
+ pikkuFuncId: `${namespace}:${funcName}`,
39
+ name: toolName,
40
+ description: funcMeta.description || funcMeta.title || funcName,
41
+ inputSchema: funcMeta.inputSchemaName ?? null,
42
+ outputSchema: funcMeta.outputSchemaName ?? null,
43
+ tags: funcMeta.tags,
44
+ }
45
+ }
46
+ }
47
+ }
48
+ } catch (error: any) {
49
+ logger.warn(
50
+ `Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`
51
+ )
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Load addon schemas into state.schemas. Called after generateAllSchemas
58
+ * to ensure addon schemas aren't overwritten.
59
+ */
60
+ export async function loadAddonSchemas(
61
+ logger: InspectorLogger,
62
+ state: InspectorState
63
+ ): Promise<void> {
64
+ const { wireAddonDeclarations } = state.rpc
65
+ if (wireAddonDeclarations.size === 0) return
66
+
67
+ const require = createRequire(join(state.rootDir, 'package.json'))
68
+
69
+ for (const [namespace, decl] of wireAddonDeclarations) {
70
+ try {
71
+ const metaPath = require.resolve(
72
+ `${decl.package}/.pikku/function/pikku-functions-meta.gen.json`
73
+ )
74
+ const schemasDir = join(dirname(metaPath), '..', 'schemas', 'schemas')
75
+ try {
76
+ const schemaFiles = await readdir(schemasDir)
77
+ for (const file of schemaFiles) {
78
+ if (!file.endsWith('.schema.json')) continue
79
+ const schemaName = file.replace('.schema.json', '')
80
+ if (!state.schemas[schemaName]) {
81
+ const schemaRaw = await readFile(join(schemasDir, file), 'utf-8')
82
+ state.schemas[schemaName] = JSON.parse(schemaRaw)
83
+ }
84
+ }
85
+ } catch {
86
+ // No schemas directory — that's fine
87
+ }
88
+ } catch (error: any) {
89
+ logger.warn(
90
+ `Failed to load addon schemas for '${namespace}' (${decl.package}): ${error.message}`
91
+ )
92
+ }
93
+ }
94
+ }
@@ -296,6 +296,31 @@ export function computeResolvedIOTypes(state: InspectorState): void {
296
296
  }
297
297
 
298
298
  state.resolvedIOTypes[pikkuFuncId] = { inputType, outputType }
299
+
300
+ if (meta.inputSchemaName && inputType !== 'null') {
301
+ meta.inputSchemaName = inputType
302
+ }
303
+ if (meta.outputSchemaName && outputType !== 'null') {
304
+ meta.outputSchemaName = outputType
305
+ }
306
+ if (meta.inputs) {
307
+ meta.inputs = meta.inputs.map((name) => {
308
+ try {
309
+ return functions.typesMap.getTypeMeta(name).uniqueName
310
+ } catch {
311
+ return name
312
+ }
313
+ })
314
+ }
315
+ if (meta.outputs) {
316
+ meta.outputs = meta.outputs.map((name) => {
317
+ try {
318
+ return functions.typesMap.getTypeMeta(name).uniqueName
319
+ } catch {
320
+ return name
321
+ }
322
+ })
323
+ }
299
324
  }
300
325
  }
301
326
 
@@ -467,6 +492,65 @@ export function validateAgentOverrides(
467
492
  }
468
493
  }
469
494
 
495
+ /**
496
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
497
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
498
+ *
499
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
500
+ * all top-level code in the file. Wiring calls crash during this process
501
+ * because the pikku state metadata doesn't exist in the CLI context.
502
+ */
503
+ export function validateSchemaWiringSeparation(
504
+ logger: InspectorLogger,
505
+ state: InspectorState
506
+ ): void {
507
+ // Collect files that contain schemas
508
+ const schemaFiles = new Set<string>()
509
+ for (const ref of state.schemaLookup.values()) {
510
+ schemaFiles.add(ref.sourceFile)
511
+ }
512
+
513
+ // Collect files that contain wiring side-effects
514
+ const wiringFiles = new Set<string>()
515
+
516
+ // HTTP route wirings
517
+ for (const file of state.http.files) {
518
+ wiringFiles.add(file)
519
+ }
520
+
521
+ // Permission wirings (addPermission calls)
522
+ for (const meta of state.permissions.tagPermissions.values()) {
523
+ wiringFiles.add(meta.sourceFile)
524
+ }
525
+ for (const meta of state.http.routePermissions.values()) {
526
+ wiringFiles.add(meta.sourceFile)
527
+ }
528
+
529
+ // Middleware wirings (addHTTPMiddleware calls)
530
+ for (const meta of state.http.routeMiddleware.values()) {
531
+ wiringFiles.add(meta.sourceFile)
532
+ }
533
+ for (const meta of state.middleware.tagMiddleware.values()) {
534
+ wiringFiles.add(meta.sourceFile)
535
+ }
536
+
537
+ // Check for overlap
538
+ for (const file of schemaFiles) {
539
+ if (wiringFiles.has(file)) {
540
+ const schemas = Array.from(state.schemaLookup.entries())
541
+ .filter(([, ref]) => ref.sourceFile === file)
542
+ .map(([name]) => name)
543
+
544
+ logger.critical(
545
+ ErrorCode.SCHEMA_AND_WIRING_COLOCATED,
546
+ `File '${file}' contains both Zod schemas (${schemas.join(', ')}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
547
+ `These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
548
+ `Move the route/wiring definitions to a dedicated wiring file.`
549
+ )
550
+ }
551
+ }
552
+ }
553
+
470
554
  export function computeDiagnostics(state: InspectorState): void {
471
555
  const diagnostics: InspectorDiagnostic[] = []
472
556
 
@@ -0,0 +1,25 @@
1
+ import type { FunctionMeta, FunctionsMeta } from '@pikku/core'
2
+
3
+ /**
4
+ * Look up function metadata by pikkuFuncId, checking both local functions
5
+ * and addon functions. Addon functions use namespaced IDs like 'namespace:funcName'.
6
+ */
7
+ export function resolveFunctionMeta(
8
+ state: {
9
+ functions: { meta: FunctionsMeta }
10
+ addonFunctions: Record<string, FunctionsMeta>
11
+ },
12
+ pikkuFuncId: string
13
+ ): FunctionMeta | undefined {
14
+ // Check local functions first
15
+ const local = state.functions.meta[pikkuFuncId]
16
+ if (local) return local
17
+
18
+ // Check addon functions (namespaced like 'swaggerPetstore:addPet')
19
+ const colonIndex = pikkuFuncId.indexOf(':')
20
+ if (colonIndex === -1) return undefined
21
+
22
+ const namespace = pikkuFuncId.substring(0, colonIndex)
23
+ const funcName = pikkuFuncId.substring(colonIndex + 1)
24
+ return state.addonFunctions[namespace]?.[funcName]
25
+ }