@pikku/inspector 0.12.10 → 0.12.12

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 (63) hide show
  1. package/CHANGELOG.md +21 -9
  2. package/dist/add/add-cli.js +10 -3
  3. package/dist/add/add-credential.js +2 -1
  4. package/dist/add/add-functions.js +99 -5
  5. package/dist/add/add-http-route.js +44 -6
  6. package/dist/add/add-keyed-wiring.js +3 -1
  7. package/dist/add/add-middleware.js +33 -4
  8. package/dist/add/add-permission.js +7 -7
  9. package/dist/add/add-workflow-graph.js +20 -1
  10. package/dist/error-codes.d.ts +2 -0
  11. package/dist/error-codes.js +2 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +2 -5
  15. package/dist/types.d.ts +10 -19
  16. package/dist/utils/extract-function-name.js +6 -0
  17. package/dist/utils/filter-inspector-state.js +187 -59
  18. package/dist/utils/filter-utils.js +13 -5
  19. package/dist/utils/get-property-value.d.ts +10 -0
  20. package/dist/utils/get-property-value.js +30 -0
  21. package/dist/utils/post-process.d.ts +2 -3
  22. package/dist/utils/post-process.js +3 -23
  23. package/dist/utils/resolve-addon-package.d.ts +4 -5
  24. package/dist/utils/resolve-addon-package.js +64 -16
  25. package/dist/utils/resolve-deploy-target.d.ts +28 -0
  26. package/dist/utils/resolve-deploy-target.js +56 -0
  27. package/dist/utils/resolve-versions.js +79 -0
  28. package/dist/utils/schema-generator.js +31 -12
  29. package/dist/utils/validate-auth-sessionless.d.ts +1 -1
  30. package/package.json +2 -2
  31. package/src/add/add-cli.ts +10 -3
  32. package/src/add/add-credential.ts +3 -0
  33. package/src/add/add-functions.test.ts +318 -0
  34. package/src/add/add-functions.ts +164 -6
  35. package/src/add/add-gateway.ts +5 -1
  36. package/src/add/add-http-route.ts +54 -7
  37. package/src/add/add-keyed-wiring.ts +7 -1
  38. package/src/add/add-mcp-prompt.ts +5 -1
  39. package/src/add/add-mcp-resource.ts +5 -1
  40. package/src/add/add-middleware.ts +42 -4
  41. package/src/add/add-permission.ts +7 -7
  42. package/src/add/add-schedule.ts +5 -1
  43. package/src/add/add-workflow-graph.ts +19 -1
  44. package/src/add/wire-name-literal.test.ts +114 -0
  45. package/src/error-codes.ts +2 -0
  46. package/src/index.ts +1 -0
  47. package/src/inspector.ts +1 -5
  48. package/src/types.ts +19 -15
  49. package/src/utils/extract-function-name.ts +8 -0
  50. package/src/utils/filter-inspector-state.test.ts +168 -64
  51. package/src/utils/filter-inspector-state.ts +290 -64
  52. package/src/utils/filter-utils.test.ts +30 -15
  53. package/src/utils/filter-utils.ts +14 -5
  54. package/src/utils/get-property-value.ts +40 -0
  55. package/src/utils/post-process.ts +3 -38
  56. package/src/utils/resolve-addon-package.ts +65 -14
  57. package/src/utils/resolve-deploy-target.test.ts +105 -0
  58. package/src/utils/resolve-deploy-target.ts +63 -0
  59. package/src/utils/resolve-versions.test.ts +108 -0
  60. package/src/utils/resolve-versions.ts +86 -0
  61. package/src/utils/schema-generator.ts +37 -13
  62. package/src/utils/validate-auth-sessionless.ts +1 -1
  63. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 0.12.12
2
+
3
+ ### Patch Changes
4
+
5
+ - 9060165: Agents now declare their model directly as `<provider>/<model>` (e.g. `openai/gpt-4o`). The `models`, `agentDefaults`, and `agentOverrides` config blocks have been removed.
6
+
7
+ **Migration:** replace any bare `model: 'alias'` values with the full provider-qualified form and remove those blocks from `pikku.config.json`.
8
+
9
+ - Updated dependencies [9060165]
10
+ - Updated dependencies [9060165]
11
+ - Updated dependencies [9060165]
12
+ - @pikku/core@0.12.21
13
+
14
+ ## 0.12.11
15
+
16
+ ### Patch Changes
17
+
18
+ - 033d172: Log a critical inspector error when multiple functions resolve to the same `pikku` function name, instead of silently allowing routing map collisions. This may cause builds to fail if multiple functions previously resolved to the same `pikku` function name.
19
+ - Updated dependencies [b9ed73e]
20
+ - @pikku/core@0.12.19
21
+
1
22
  ## 0.12.0
2
23
 
3
24
  ## 0.12.10
@@ -32,7 +53,6 @@
32
53
  ### Patch Changes
33
54
 
34
55
  - 624097e: Add deploy pipeline with provider-agnostic architecture
35
-
36
56
  - Add MetaService with explicit typed API, absorb WiringService reads
37
57
  - Add deployment service, traceId propagation, scoped logger
38
58
  - Rewrite analyzer: one function = one worker, gateways dispatch via RPC
@@ -79,7 +99,6 @@
79
99
 
80
100
  - 5866b66: Add critical error (PKU490) when Zod schemas and wiring calls (wireHTTPRoutes, addPermission, addHTTPMiddleware) coexist in the same file. The CLI uses tsImport to extract Zod schemas at runtime, which executes all top-level code — wiring side-effects crash in this context because pikku state metadata doesn't exist. Schemas and wirings must be in separate files.
81
101
  - e412b4d: Optimize CLI codegen performance: 12x faster `pikku all`
82
-
83
102
  - Reuse schemas across re-inspections (skip redundant `ts-json-schema-generator` runs)
84
103
  - Cache TS schemas to disk (`.pikku/schema-cache.json`) for cross-run reuse
85
104
  - Pass `oldProgram` to `ts.createProgram` for incremental TS compilation
@@ -208,14 +227,12 @@
208
227
  - 1967172: Update code generation to support channel middleware enhancements
209
228
 
210
229
  **Code Generation Updates:**
211
-
212
230
  - Update channel type serialization to include middleware support
213
231
  - Improve WebSocket wrapper generation for middleware handling
214
232
  - Update CLI channel client generation with better type support
215
233
  - Enhance services and schema generation for channel configurations
216
234
 
217
235
  **Inspector Updates:**
218
-
219
236
  - Improve channel metadata extraction for middleware
220
237
  - Better type analysis for channel lifecycle functions
221
238
  - Enhanced post-processing for channel configurations
@@ -223,19 +240,16 @@
223
240
  - 753481a: Add bootstrap command, performance optimizations, and CLI improvements
224
241
 
225
242
  **New Features:**
226
-
227
243
  - Add `pikku bootstrap` command for type-only generation (~13.5% faster than `pikku all`)
228
244
  - Add configurable `ignoreFiles` option to pikku.config.json with sensible defaults (_.gen.ts, _.test.ts, \*.spec.ts)
229
245
  - Export pikkuCLIRender helper from serialize-cli-types.ts with JSDoc documentation
230
246
 
231
247
  **Performance Improvements:**
232
-
233
248
  - Add aggressive TypeScript compiler options (skipDefaultLibCheck, types: []) - ~37% faster TypeScript setup
234
249
  - Add detailed performance timing to inspector phases (--logLevel=debug)
235
250
  - Optimize file inspection with ignore patterns - ~10-20% faster overall
236
251
 
237
252
  **Enhancements:**
238
-
239
253
  - Fix --logLevel flag to properly apply log level to logger
240
254
  - Update middleware logging to use structured log format
241
255
  - Improve CLI renderers to consistently use destructured logger service
@@ -336,7 +350,6 @@ For complete details, see https://pikku.dev/changelogs/0_10_0.md
336
350
  ### Patch Changes
337
351
 
338
352
  - 44e3ff4: feat: enhance CLI filtering with type and directory filters
339
-
340
353
  - Add --types filter to filter by PikkuEventTypes (http, channel, queue, scheduler, rpc, mcp)
341
354
  - Add --directories filter to filter by file paths/directories
342
355
  - All filters (tags, types, directories) now work together with AND logic
@@ -347,7 +360,6 @@ For complete details, see https://pikku.dev/changelogs/0_10_0.md
347
360
  - 7c592b8: feat: support for required services and improved service configuration
348
361
 
349
362
  This release includes several enhancements to service management and configuration:
350
-
351
363
  - Added support for required services configuration
352
364
  - Improved service discovery and registration
353
365
  - Added typed RPC clients for service communication
@@ -295,10 +295,16 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
295
295
  meta.options = processOptions(logger, optionsNode, typeChecker, inspectorState, options, pikkuFuncId);
296
296
  }
297
297
  break;
298
- case 'subcommands':
299
- if (ts.isObjectLiteralExpression(prop.initializer)) {
298
+ case 'subcommands': {
299
+ let subcommandsNode = prop.initializer;
300
+ if (ts.isIdentifier(prop.initializer)) {
301
+ subcommandsNode = resolveIdentifier(prop.initializer, typeChecker, [
302
+ 'defineCLICommands',
303
+ ]);
304
+ }
305
+ if (subcommandsNode && ts.isObjectLiteralExpression(subcommandsNode)) {
300
306
  meta.subcommands = {};
301
- for (const subProp of prop.initializer.properties) {
307
+ for (const subProp of subcommandsNode.properties) {
302
308
  if (!ts.isPropertyAssignment(subProp))
303
309
  continue;
304
310
  const subName = getPropertyName(subProp);
@@ -311,6 +317,7 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
311
317
  }
312
318
  }
313
319
  break;
320
+ }
314
321
  case 'isDefault':
315
322
  if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
316
323
  prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { getPropertyValue, getArrayPropertyValue, } from '../utils/get-property-value.js';
2
+ import { getPropertyValue, getArrayPropertyValue, assertStringLiteralProperty, } from '../utils/get-property-value.js';
3
3
  import { ErrorCode } from '../error-codes.js';
4
4
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
5
5
  export const addCredential = (logger, node, checker, state, _options) => {
@@ -17,6 +17,7 @@ export const addCredential = (logger, node, checker, state, _options) => {
17
17
  }
18
18
  if (ts.isObjectLiteralExpression(firstArg)) {
19
19
  const obj = firstArg;
20
+ assertStringLiteralProperty(obj, 'name', 'Credential', logger);
20
21
  const nameValue = getPropertyValue(obj, 'name');
21
22
  const displayNameValue = getPropertyValue(obj, 'displayName');
22
23
  const descriptionValue = getPropertyValue(obj, 'description');
@@ -3,8 +3,9 @@ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
3
3
  import { extractFunctionName, funcIdToTypeName, } from '../utils/extract-function-name.js';
4
4
  import { extractFunctionNode } from '../utils/extract-function-node.js';
5
5
  import { extractUsedWires } from '../utils/extract-services.js';
6
- import { formatVersionedId } from '@pikku/core';
6
+ import { formatVersionedId, parseVersionedId } from '@pikku/core';
7
7
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
8
+ import { canonicalJSON, hashString } from '../utils/hash.js';
8
9
  import { resolveMiddleware } from '../utils/middleware.js';
9
10
  import { resolvePermissions } from '../utils/permissions.js';
10
11
  import { extractWireNames } from '../utils/post-process.js';
@@ -209,6 +210,33 @@ function unwrapPromise(checker, type) {
209
210
  }
210
211
  return type;
211
212
  }
213
+ const resolveExistingFunctionSource = (state, pikkuFuncId) => {
214
+ return (state.functions.meta[pikkuFuncId]?.sourceFile ||
215
+ state.rpc.internalFiles.get(pikkuFuncId)?.path ||
216
+ null);
217
+ };
218
+ const areCompatibleFunctionIds = (existingId, incomingId) => {
219
+ if (existingId === incomingId) {
220
+ return true;
221
+ }
222
+ const existingParsed = parseVersionedId(existingId);
223
+ const incomingParsed = parseVersionedId(incomingId);
224
+ return existingParsed.baseName === incomingParsed.baseName;
225
+ };
226
+ function printNode(node) {
227
+ return ts
228
+ .createPrinter({ removeComments: true })
229
+ .printNode(ts.EmitHint.Unspecified, node, node.getSourceFile());
230
+ }
231
+ function computeImplementationHash(args) {
232
+ const { wrapper, handler, objectNode, isDirectFunction } = args;
233
+ return hashString(canonicalJSON({
234
+ wrapper,
235
+ isDirectFunction,
236
+ handler: printNode(handler),
237
+ config: objectNode ? printNode(objectNode) : null,
238
+ }));
239
+ }
212
240
  /**
213
241
  * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
214
242
  * then push into state.functions.meta.
@@ -396,10 +424,16 @@ export const addFunctions = (logger, node, checker, state, options) => {
396
424
  }
397
425
  }
398
426
  if (version !== undefined) {
399
- const baseName = explicitName || exportedName || pikkuFuncId;
427
+ let baseName = explicitName || exportedName || pikkuFuncId;
428
+ // Strip trailing VN suffix if it matches the version (e.g. getDataV1 + version:1 → getData@v1)
429
+ const vSuffix = `V${version}`;
430
+ if (baseName.endsWith(vSuffix) && baseName.length > vSuffix.length) {
431
+ baseName = baseName.slice(0, -vSuffix.length);
432
+ }
400
433
  pikkuFuncId = formatVersionedId(baseName, version);
401
434
  }
402
435
  const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc';
436
+ const isListFunc = expression.text === 'pikkuListFunc';
403
437
  const mcpEnabled = mcp || isMCPToolFunc;
404
438
  // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
405
439
  const handler = resolvedFunc &&
@@ -461,7 +495,22 @@ export const addFunctions = (logger, node, checker, state, options) => {
461
495
  state.schemaLookup.set(schemaName, inputSchemaRef);
462
496
  state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
463
497
  }
464
- else if (genericTypes.length >= 1 && genericTypes[0]) {
498
+ else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
499
+ const inputAliasName = `${capitalizedName}Input`;
500
+ const filterType = genericTypes[0];
501
+ const filterTypeText = checker.typeToString(filterType, undefined, ts.TypeFormatFlags.NoTruncation);
502
+ const refs = resolveTypeImports(filterType, state.functions.typesMap, true, checker);
503
+ state.functions.typesMap.addCustomType(inputAliasName, `{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`, [...new Set(refs)]);
504
+ inputNames = [inputAliasName];
505
+ const secondParam = handler.parameters[1];
506
+ if (secondParam) {
507
+ inputTypes = [checker.getTypeAtLocation(secondParam)];
508
+ }
509
+ else {
510
+ inputTypes = [filterType];
511
+ }
512
+ }
513
+ else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
465
514
  // Fall back to extracting from generic type arguments
466
515
  const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
467
516
  inputNames = result.names;
@@ -485,7 +534,15 @@ export const addFunctions = (logger, node, checker, state, options) => {
485
534
  state.schemaLookup.set(schemaName, outputSchemaRef);
486
535
  state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
487
536
  }
488
- else if (genericTypes.length >= 2) {
537
+ else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
538
+ const outputAliasName = `${capitalizedName}Output`;
539
+ const rowType = genericTypes[1];
540
+ const rowTypeText = checker.typeToString(rowType, undefined, ts.TypeFormatFlags.NoTruncation);
541
+ const refs = resolveTypeImports(rowType, state.functions.typesMap, true, checker);
542
+ state.functions.typesMap.addCustomType(outputAliasName, `{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`, [...new Set(refs)]);
543
+ outputNames = [outputAliasName];
544
+ }
545
+ else if (!isListFunc && genericTypes.length >= 2) {
489
546
  outputNames = getNamesAndTypes(checker, state.functions.typesMap, 'Output', name, genericTypes[1]).names;
490
547
  }
491
548
  else {
@@ -539,6 +596,36 @@ export const addFunctions = (logger, node, checker, state, options) => {
539
596
  if (inputTypes.length > 0) {
540
597
  state.typesLookup.set(pikkuFuncId, inputTypes);
541
598
  }
599
+ const sourceFile = node.getSourceFile().fileName;
600
+ const existingFunction = state.functions.meta[pikkuFuncId];
601
+ if (existingFunction &&
602
+ existingFunction.sourceFile &&
603
+ existingFunction.sourceFile !== sourceFile) {
604
+ logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Function name '${name}' is not unique. ` +
605
+ `'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`);
606
+ return;
607
+ }
608
+ if (exportedName || explicitName) {
609
+ const existingInternal = state.rpc.internalMeta[name];
610
+ if (existingInternal &&
611
+ !areCompatibleFunctionIds(existingInternal, pikkuFuncId)) {
612
+ const existingSource = resolveExistingFunctionSource(state, existingInternal) || 'unknown file';
613
+ logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Function name '${name}' is not unique. ` +
614
+ `It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`);
615
+ return;
616
+ }
617
+ if (expose) {
618
+ const existingExposed = state.rpc.exposedMeta[name];
619
+ if (existingExposed &&
620
+ !areCompatibleFunctionIds(existingExposed, pikkuFuncId)) {
621
+ const existingSource = resolveExistingFunctionSource(state, existingExposed) ||
622
+ 'unknown file';
623
+ logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Exposed function name '${name}' is not unique. ` +
624
+ `It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`);
625
+ return;
626
+ }
627
+ }
628
+ }
542
629
  // --- resolve middleware ---
543
630
  let middleware = objectNode
544
631
  ? resolveMiddleware(state, objectNode, tags, checker)
@@ -569,6 +656,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
569
656
  }
570
657
  }
571
658
  const sessionless = expression.text !== 'pikkuFunc';
659
+ const implementationHash = computeImplementationHash({
660
+ wrapper: expression.text,
661
+ handler,
662
+ objectNode,
663
+ isDirectFunction,
664
+ });
572
665
  state.functions.meta[pikkuFuncId] = {
573
666
  pikkuFuncId,
574
667
  functionType: 'user',
@@ -588,6 +681,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
588
681
  deploy: deploy || undefined,
589
682
  approvalRequired: approvalRequired || undefined,
590
683
  approvalDescription: approvalDescription || undefined,
684
+ implementationHash,
591
685
  version,
592
686
  title,
593
687
  tags: tags || undefined,
@@ -597,7 +691,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
597
691
  middleware,
598
692
  permissions,
599
693
  isDirectFunction,
600
- sourceFile: node.getSourceFile().fileName,
694
+ sourceFile,
601
695
  exportedName: exportedName || undefined,
602
696
  };
603
697
  // Populate node metadata if node config is present
@@ -7,6 +7,7 @@ import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js';
7
7
  import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js';
8
8
  import { extractWireNames } from '../utils/post-process.js';
9
9
  import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js';
10
+ import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
10
11
  import { ErrorCode } from '../error-codes.js';
11
12
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
12
13
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
@@ -113,12 +114,30 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
113
114
  if (funcName.startsWith('__temp_')) {
114
115
  funcName = makeContextBasedId('http', method, fullRoute);
115
116
  }
117
+ let refAddonTarget = null;
118
+ if (ts.isCallExpression(funcInitializer) &&
119
+ ts.isIdentifier(funcInitializer.expression) &&
120
+ funcInitializer.expression.text === 'ref') {
121
+ const [firstArg] = funcInitializer.arguments;
122
+ if (firstArg &&
123
+ ts.isStringLiteral(firstArg) &&
124
+ firstArg.text.includes(':')) {
125
+ refAddonTarget = firstArg.text;
126
+ }
127
+ }
116
128
  const packageName = ts.isIdentifier(funcInitializer)
117
129
  ? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
118
130
  : null;
131
+ if (refAddonTarget) {
132
+ const targetMeta = resolveFunctionMeta(state, refAddonTarget);
133
+ if (!targetMeta) {
134
+ logger.warn(`Skipping route '${fullRoute}': addon function metadata for '${refAddonTarget}' is not available yet.`);
135
+ return;
136
+ }
137
+ }
119
138
  ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
120
139
  // Lookup existing function metadata
121
- const fnMeta = state.functions.meta[funcName];
140
+ const fnMeta = resolveFunctionMeta(state, funcName);
122
141
  if (!fnMeta) {
123
142
  logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for '${funcName}'.`);
124
143
  return;
@@ -127,17 +146,36 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
127
146
  return;
128
147
  }
129
148
  const input = fnMeta.inputs?.[0] || null;
149
+ const getRouteInputKeys = () => {
150
+ const targetFuncName = refAddonTarget ?? funcName;
151
+ const inputTypes = state.typesLookup.get(targetFuncName);
152
+ if (inputTypes && inputTypes.length > 0) {
153
+ return extractTypeKeys(inputTypes[0]);
154
+ }
155
+ const targetMeta = resolveFunctionMeta(state, targetFuncName);
156
+ if (targetMeta?.inputSchemaName) {
157
+ const schema = state.schemas[targetMeta.inputSchemaName];
158
+ const properties = schema?.properties;
159
+ if (properties && typeof properties === 'object') {
160
+ return Object.keys(properties);
161
+ }
162
+ }
163
+ return null;
164
+ };
130
165
  // Validate that route params and query params exist in function input type
131
166
  if (params.length > 0 || query.length > 0) {
132
- const inputTypes = state.typesLookup.get(funcName);
133
- if (inputTypes && inputTypes.length > 0) {
134
- const inputKeys = extractTypeKeys(inputTypes[0]);
167
+ const inputKeys = getRouteInputKeys();
168
+ if (!inputKeys) {
169
+ // Input shape isn't inspectable at this phase (e.g. addon ref or opaque handler).
170
+ // Skip param/query validation rather than emitting a false positive.
171
+ }
172
+ else {
135
173
  // Check path params
136
174
  if (params.length > 0) {
137
175
  const missingParams = params.filter((p) => !inputKeys.includes(p));
138
176
  if (missingParams.length > 0) {
139
177
  logger.critical(ErrorCode.ROUTE_PARAM_MISMATCH, `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
140
- `not found in function '${funcName}' input type. ` +
178
+ `not found in function '${refAddonTarget ?? funcName}' input type. ` +
141
179
  `Input type has: [${inputKeys.join(', ')}]`);
142
180
  return;
143
181
  }
@@ -147,7 +185,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
147
185
  const missingQuery = query.filter((q) => !inputKeys.includes(q));
148
186
  if (missingQuery.length > 0) {
149
187
  logger.critical(ErrorCode.ROUTE_QUERY_MISMATCH, `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
150
- `not found in function '${funcName}' input type. ` +
188
+ `not found in function '${refAddonTarget ?? funcName}' input type. ` +
151
189
  `Input type has: [${inputKeys.join(', ')}]`);
152
190
  return;
153
191
  }
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { getPropertyValue } from '../utils/get-property-value.js';
2
+ import { getPropertyValue, assertStringLiteralProperty, } from '../utils/get-property-value.js';
3
3
  import { ErrorCode } from '../error-codes.js';
4
4
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
5
5
  export const createAddKeyedWiring = (config) => {
@@ -19,6 +19,8 @@ export const createAddKeyedWiring = (config) => {
19
19
  }
20
20
  if (ts.isObjectLiteralExpression(firstArg)) {
21
21
  const obj = firstArg;
22
+ assertStringLiteralProperty(obj, 'name', config.label, logger);
23
+ assertStringLiteralProperty(obj, config.idField, config.label, logger);
22
24
  const nameValue = getPropertyValue(obj, 'name');
23
25
  const displayNameValue = getPropertyValue(obj, 'displayName');
24
26
  const descriptionValue = getPropertyValue(obj, 'description');
@@ -171,7 +171,7 @@ export const addMiddleware = (logger, node, checker, state) => {
171
171
  logger.debug(`• Found middleware factory with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`);
172
172
  return;
173
173
  }
174
- if (expression.text === 'addMiddleware') {
174
+ if (expression.text === 'addTagMiddleware') {
175
175
  const tagArg = args[0];
176
176
  const middlewareArrayArg = args[1];
177
177
  if (!tagArg || !middlewareArrayArg)
@@ -181,11 +181,11 @@ export const addMiddleware = (logger, node, checker, state) => {
181
181
  tag = tagArg.text;
182
182
  }
183
183
  if (!tag) {
184
- logger.warn(`• addMiddleware call without valid tag string`);
184
+ logger.warn(`• addTagMiddleware call without valid tag string`);
185
185
  return;
186
186
  }
187
187
  if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
188
- logger.error(`• addMiddleware('${tag}', ...) must have a literal array as second argument`);
188
+ logger.error(`• addTagMiddleware('${tag}', ...) must have a literal array as second argument`);
189
189
  return;
190
190
  }
191
191
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
@@ -236,7 +236,7 @@ export const addMiddleware = (logger, node, checker, state) => {
236
236
  }
237
237
  if (!isFactory && exportedName) {
238
238
  logger.warn(`• Middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
239
- `For tree-shaking, use: export const ${exportedName} = () => addMiddleware('${tag}', [...])`);
239
+ `For tree-shaking, use: export const ${exportedName} = () => addTagMiddleware('${tag}', [...])`);
240
240
  }
241
241
  state.middleware.tagMiddleware.set(tag, {
242
242
  exportName: exportedName,
@@ -253,6 +253,34 @@ export const addMiddleware = (logger, node, checker, state) => {
253
253
  logger.debug(`• Found tag middleware group: ${tag} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
254
254
  return;
255
255
  }
256
+ if (expression.text === 'addGlobalMiddleware') {
257
+ const middlewareArrayArg = args[0];
258
+ if (!middlewareArrayArg ||
259
+ !ts.isArrayLiteralExpression(middlewareArrayArg)) {
260
+ logger.error(`• addGlobalMiddleware(...) must have a literal array as its only argument`);
261
+ return;
262
+ }
263
+ const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
264
+ const definitionIds = refs.map((r) => r.definitionId);
265
+ if (definitionIds.length > 0) {
266
+ renameTempDefinitions(state, definitionIds, 'global', 'middleware');
267
+ }
268
+ const sourceFile = node.getSourceFile().fileName;
269
+ for (let i = 0; i < refs.length; i++) {
270
+ const instanceId = makeContextBasedId('global', 'middleware', String(i));
271
+ state.middleware.instances[instanceId] = {
272
+ definitionId: definitionIds[i],
273
+ sourceFile,
274
+ position: node.getStart(),
275
+ isFactoryCall: refs[i].isFactoryCall,
276
+ };
277
+ }
278
+ // Without this, bootstrap codegen's "import every file with a wire-call"
279
+ // pass skips middleware-only files and the registration never runs.
280
+ state.http.files.add(sourceFile);
281
+ logger.debug(`• Found global middleware group with ${refs.length} entries`);
282
+ return;
283
+ }
256
284
  if (expression.text === 'addHTTPMiddleware') {
257
285
  const patternArg = args[0];
258
286
  const middlewareArrayArg = args[1];
@@ -332,6 +360,7 @@ export const addMiddleware = (logger, node, checker, state) => {
332
360
  instanceIds,
333
361
  isFactory,
334
362
  });
363
+ state.http.files.add(sourceFile);
335
364
  logger.debug(`• Found HTTP route middleware group: ${pattern} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
336
365
  return;
337
366
  }
@@ -27,7 +27,7 @@ function isInsidePermissionContainer(node) {
27
27
  if (ts.isCallExpression(current) &&
28
28
  ts.isIdentifier(current.expression) &&
29
29
  (current.expression.text === 'pikkuPermissionFactory' ||
30
- current.expression.text === 'addPermission' ||
30
+ current.expression.text === 'addTagPermission' ||
31
31
  current.expression.text === 'addHTTPPermission')) {
32
32
  return true;
33
33
  }
@@ -247,9 +247,9 @@ export const addPermission = (logger, node, checker, state) => {
247
247
  }
248
248
  // Handle addPermission('tag', [permission1, permission2])
249
249
  // Supports two patterns:
250
- // 1. export const x = () => addPermission('tag', [...]) (factory - tree-shakeable)
251
- // 2. export const x = addPermission('tag', [...]) (direct - no tree-shaking)
252
- if (expression.text === 'addPermission') {
250
+ // 1. export const x = () => addTagPermission('tag', [...]) (factory - tree-shakeable)
251
+ // 2. export const x = addTagPermission('tag', [...]) (direct - no tree-shaking)
252
+ if (expression.text === 'addTagPermission') {
253
253
  const tagArg = args[0];
254
254
  const permissionsArrayArg = args[1];
255
255
  if (!tagArg || !permissionsArrayArg)
@@ -260,13 +260,13 @@ export const addPermission = (logger, node, checker, state) => {
260
260
  tag = tagArg.text;
261
261
  }
262
262
  if (!tag) {
263
- logger.warn(`• addPermission call without valid tag string`);
263
+ logger.warn(`• addTagPermission call without valid tag string`);
264
264
  return;
265
265
  }
266
266
  // Check if permissions is a literal array or object
267
267
  if (!ts.isArrayLiteralExpression(permissionsArrayArg) &&
268
268
  !ts.isObjectLiteralExpression(permissionsArrayArg)) {
269
- logger.error(`• addPermission('${tag}', ...) must have a literal array or object as second argument`);
269
+ logger.error(`• addTagPermission('${tag}', ...) must have a literal array or object as second argument`);
270
270
  return;
271
271
  }
272
272
  // Extract permission pikkuFuncIds from array
@@ -305,7 +305,7 @@ export const addPermission = (logger, node, checker, state) => {
305
305
  }
306
306
  if (!isFactory && exportedName) {
307
307
  logger.warn(`• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
308
- `For tree-shaking, use: export const ${exportedName} = () => addPermission('${tag}', [...])`);
308
+ `For tree-shaking, use: export const ${exportedName} = () => addTagPermission('${tag}', [...])`);
309
309
  }
310
310
  state.permissions.tagPermissions.set(tag, {
311
311
  exportName: exportedName,
@@ -281,6 +281,8 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
281
281
  input: {},
282
282
  next: undefined,
283
283
  onError: undefined,
284
+ retries: undefined,
285
+ retryDelay: undefined,
284
286
  };
285
287
  }
286
288
  // Extract config for each node from 'config' property
@@ -301,6 +303,8 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
301
303
  nodes[nodeId].next = nodeConfig.next;
302
304
  nodes[nodeId].onError = nodeConfig.onError;
303
305
  nodes[nodeId].input = nodeConfig.input;
306
+ nodes[nodeId].retries = nodeConfig.retries;
307
+ nodes[nodeId].retryDelay = nodeConfig.retryDelay;
304
308
  }
305
309
  }
306
310
  }
@@ -314,6 +318,8 @@ function extractNodeConfigFromObject(obj, checker) {
314
318
  let next = undefined;
315
319
  let onError = undefined;
316
320
  let input = {};
321
+ let retries = undefined;
322
+ let retryDelay = undefined;
317
323
  for (const prop of obj.properties) {
318
324
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
319
325
  continue;
@@ -327,8 +333,21 @@ function extractNodeConfigFromObject(obj, checker) {
327
333
  else if (propName === 'input') {
328
334
  input = extractInputMapping(prop.initializer, checker);
329
335
  }
336
+ else if (propName === 'retries') {
337
+ if (ts.isNumericLiteral(prop.initializer)) {
338
+ retries = Number(prop.initializer.text);
339
+ }
340
+ }
341
+ else if (propName === 'retryDelay') {
342
+ if (ts.isNumericLiteral(prop.initializer)) {
343
+ retryDelay = Number(prop.initializer.text);
344
+ }
345
+ else if (ts.isStringLiteral(prop.initializer)) {
346
+ retryDelay = prop.initializer.text;
347
+ }
348
+ }
330
349
  }
331
- return { next, onError, input };
350
+ return { next, onError, input, retries, retryDelay };
332
351
  }
333
352
  /**
334
353
  * Inspector for pikkuWorkflowGraph() calls
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export declare enum ErrorCode {
10
10
  MISSING_NAME = "PKU111",
11
+ NON_LITERAL_WIRE_NAME = "PKU118",
11
12
  MISSING_DESCRIPTION = "PKU123",
12
13
  INVALID_VALUE = "PKU124",
13
14
  MISSING_URI = "PKU220",
@@ -41,6 +42,7 @@ export declare enum ErrorCode {
41
42
  PERMISSION_EMPTY_ARRAY = "PKU937",
42
43
  PERMISSION_PATTERN_INVALID = "PKU975",
43
44
  DUPLICATE_FUNCTION_VERSION = "PKU850",
45
+ DUPLICATE_FUNCTION_NAME = "PKU851",
44
46
  MANIFEST_MISSING = "PKU860",
45
47
  FUNCTION_VERSION_MODIFIED = "PKU861",
46
48
  CONTRACT_CHANGED_REQUIRES_BUMP = "PKU862",
@@ -10,6 +10,7 @@ export var ErrorCode;
10
10
  (function (ErrorCode) {
11
11
  // Validation errors
12
12
  ErrorCode["MISSING_NAME"] = "PKU111";
13
+ ErrorCode["NON_LITERAL_WIRE_NAME"] = "PKU118";
13
14
  ErrorCode["MISSING_DESCRIPTION"] = "PKU123";
14
15
  ErrorCode["INVALID_VALUE"] = "PKU124";
15
16
  ErrorCode["MISSING_URI"] = "PKU220";
@@ -48,6 +49,7 @@ export var ErrorCode;
48
49
  ErrorCode["PERMISSION_PATTERN_INVALID"] = "PKU975";
49
50
  // Versioning errors
50
51
  ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
52
+ ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
51
53
  // Contract versioning errors
52
54
  ErrorCode["MANIFEST_MISSING"] = "PKU860";
53
55
  ErrorCode["FUNCTION_VERSION_MODIFIED"] = "PKU861";
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { ErrorCode } from './error-codes.js';
6
6
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
7
7
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
8
8
  export { filterInspectorState } from './utils/filter-inspector-state.js';
9
+ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
9
10
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
10
11
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
11
12
  export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { inspect, getInitialInspectorState } from './inspector.js';
2
2
  export { ErrorCode } from './error-codes.js';
3
3
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
4
4
  export { filterInspectorState } from './utils/filter-inspector-state.js';
5
+ export { resolveDeployTarget } from './utils/resolve-deploy-target.js';
5
6
  export { generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
6
7
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
7
8
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';