@pikku/inspector 0.12.42 → 0.12.44

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 (126) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/dist/add/add-addon-bans.js +1 -0
  3. package/dist/add/add-ai-agent.d.ts +3 -1
  4. package/dist/add/add-ai-agent.js +82 -0
  5. package/dist/add/add-credential.js +5 -1
  6. package/dist/add/add-functions.js +10 -9
  7. package/dist/add/add-gateway.js +3 -0
  8. package/dist/add/add-http-route.js +2 -2
  9. package/dist/add/add-keyed-wiring.js +2 -0
  10. package/dist/add/add-mcp-prompt.js +0 -1
  11. package/dist/add/add-mcp-resource.js +0 -1
  12. package/dist/add/add-permission.d.ts +1 -1
  13. package/dist/add/add-permission.js +3 -177
  14. package/dist/add/add-queue-worker.js +3 -0
  15. package/dist/add/add-schedule.js +3 -0
  16. package/dist/add/add-scope.d.ts +2 -0
  17. package/dist/add/add-scope.js +146 -0
  18. package/dist/add/add-trigger.js +3 -0
  19. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  20. package/dist/add/add-wire-remote-addon.js +66 -0
  21. package/dist/add/add-workflow-graph.js +32 -3
  22. package/dist/add/add-workflow.js +9 -4
  23. package/dist/error-codes.d.ts +3 -0
  24. package/dist/error-codes.js +4 -0
  25. package/dist/inspector.js +11 -5
  26. package/dist/types.d.ts +19 -13
  27. package/dist/utils/custom-types-generator.js +2 -1
  28. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  29. package/dist/utils/ensure-function-metadata.js +24 -0
  30. package/dist/utils/filter-inspector-state.js +9 -0
  31. package/dist/utils/get-property-value.d.ts +7 -2
  32. package/dist/utils/get-property-value.js +48 -1
  33. package/dist/utils/load-addon-functions-meta.js +81 -6
  34. package/dist/utils/permissions.d.ts +1 -21
  35. package/dist/utils/permissions.js +17 -108
  36. package/dist/utils/post-process.d.ts +30 -1
  37. package/dist/utils/post-process.js +150 -72
  38. package/dist/utils/schema-generator.js +19 -4
  39. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  40. package/dist/utils/serialize-inspector-state.js +12 -6
  41. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  42. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  43. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  44. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
  45. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +274 -100
  46. package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
  47. package/dist/utils/workflow/dsl/patterns.js +11 -0
  48. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +76 -5
  49. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  50. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  51. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  52. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  53. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -4
  54. package/dist/visit.js +4 -0
  55. package/package.json +3 -3
  56. package/run-tests.sh +1 -1
  57. package/src/add/add-addon-bans.ts +1 -0
  58. package/src/add/add-ai-agent.test.ts +87 -0
  59. package/src/add/add-ai-agent.ts +122 -0
  60. package/src/add/add-credential.ts +8 -0
  61. package/src/add/add-functions.ts +10 -12
  62. package/src/add/add-gateway.ts +11 -0
  63. package/src/add/add-http-route.ts +2 -8
  64. package/src/add/add-keyed-wiring.ts +2 -0
  65. package/src/add/add-mcp-prompt.ts +0 -1
  66. package/src/add/add-mcp-resource.ts +0 -1
  67. package/src/add/add-permission.ts +4 -242
  68. package/src/add/add-queue-worker.ts +11 -0
  69. package/src/add/add-schedule.ts +11 -0
  70. package/src/add/add-scope.test.ts +346 -0
  71. package/src/add/add-scope.ts +225 -0
  72. package/src/add/add-trigger.ts +11 -0
  73. package/src/add/add-wire-remote-addon.ts +77 -0
  74. package/src/add/add-workflow-edge-cases.test.ts +201 -0
  75. package/src/add/add-workflow-fanout.test.ts +60 -0
  76. package/src/add/add-workflow-graph-input.test.ts +94 -0
  77. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  78. package/src/add/add-workflow-graph.ts +35 -3
  79. package/src/add/add-workflow-on-error.test.ts +118 -0
  80. package/src/add/add-workflow-return-switch.test.ts +121 -0
  81. package/src/add/add-workflow-silent-drops.test.ts +163 -0
  82. package/src/add/add-workflow-sleep-expression.test.ts +97 -0
  83. package/src/add/add-workflow.ts +9 -4
  84. package/src/add/config-value-metadata.test.ts +169 -0
  85. package/src/add/inline-wiring-function.test.ts +104 -0
  86. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  87. package/src/error-codes.ts +5 -0
  88. package/src/inspector.ts +14 -4
  89. package/src/types.ts +16 -17
  90. package/src/utils/custom-types-generator.test.ts +26 -1
  91. package/src/utils/custom-types-generator.ts +2 -1
  92. package/src/utils/ensure-function-metadata.ts +42 -0
  93. package/src/utils/filter-inspector-state.test.ts +0 -2
  94. package/src/utils/filter-inspector-state.ts +9 -0
  95. package/src/utils/get-property-value.test.ts +141 -0
  96. package/src/utils/get-property-value.ts +62 -1
  97. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  98. package/src/utils/load-addon-functions-meta.ts +94 -6
  99. package/src/utils/load-addon-scopes.test.ts +138 -0
  100. package/src/utils/permissions.test.ts +7 -232
  101. package/src/utils/permissions.ts +20 -160
  102. package/src/utils/post-process.test.ts +269 -4
  103. package/src/utils/post-process.ts +216 -95
  104. package/src/utils/schema-generator.test.ts +164 -0
  105. package/src/utils/schema-generator.ts +26 -3
  106. package/src/utils/serialize-inspector-state.ts +22 -25
  107. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  108. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  109. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  110. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
  111. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
  112. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
  113. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
  114. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
  115. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
  116. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
  117. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +311 -105
  118. package/src/utils/workflow/dsl/patterns.ts +18 -0
  119. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +82 -10
  120. package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
  121. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  122. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  123. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  124. package/src/utils/workflow/graph/workflow-graph.types.ts +11 -2
  125. package/src/visit.ts +4 -0
  126. package/tsconfig.tsbuildinfo +1 -1
@@ -1,34 +1,14 @@
1
1
  import * as ts from 'typescript';
2
- import { extractFunctionName, isNamedExport, makeContextBasedId, } from '../utils/extract-function-name.js';
2
+ import { extractFunctionName } from '../utils/extract-function-name.js';
3
3
  import { extractServicesFromFunction, extractUsedWires, } from '../utils/extract-services.js';
4
- import { extractPermissionPikkuNames } from '../utils/permissions.js';
5
4
  import { getPropertyValue } from '../utils/get-property-value.js';
6
5
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
- function renameTempDefinitions(state, definitionIds, groupType, groupKey) {
8
- const tempIndices = definitionIds
9
- .map((name, i) => (name.startsWith('__temp_') ? i : -1))
10
- .filter((i) => i >= 0);
11
- for (const idx of tempIndices) {
12
- const oldId = definitionIds[idx];
13
- const newId = tempIndices.length === 1
14
- ? makeContextBasedId(groupType, groupKey)
15
- : makeContextBasedId(groupType, groupKey, String(idx));
16
- const existing = state.permissions.definitions[oldId];
17
- if (existing) {
18
- delete state.permissions.definitions[oldId];
19
- state.permissions.definitions[newId] = existing;
20
- }
21
- definitionIds[idx] = newId;
22
- }
23
- }
24
6
  function isInsidePermissionContainer(node) {
25
7
  let current = node.parent;
26
8
  while (current) {
27
9
  if (ts.isCallExpression(current) &&
28
10
  ts.isIdentifier(current.expression) &&
29
- (current.expression.text === 'pikkuPermissionFactory' ||
30
- current.expression.text === 'addTagPermission' ||
31
- current.expression.text === 'addHTTPPermission')) {
11
+ current.expression.text === 'pikkuPermissionFactory') {
32
12
  return true;
33
13
  }
34
14
  current = current.parent;
@@ -36,7 +16,7 @@ function isInsidePermissionContainer(node) {
36
16
  return false;
37
17
  }
38
18
  /**
39
- * Inspect pikkuPermission calls, addPermission calls, and addHTTPPermission calls
19
+ * Inspect pikkuPermission, pikkuAuth, and pikkuPermissionFactory definitions.
40
20
  */
41
21
  export const addPermission = (logger, node, checker, state) => {
42
22
  if (!ts.isCallExpression(node))
@@ -249,158 +229,4 @@ export const addPermission = (logger, node, checker, state) => {
249
229
  logger.debug(`• Found permission factory with services: ${services.services.join(', ')}`);
250
230
  return;
251
231
  }
252
- // Handle addPermission('tag', [permission1, permission2])
253
- // Supports two patterns:
254
- // 1. export const x = () => addTagPermission('tag', [...]) (factory - tree-shakeable)
255
- // 2. export const x = addTagPermission('tag', [...]) (direct - no tree-shaking)
256
- if (expression.text === 'addTagPermission') {
257
- const tagArg = args[0];
258
- const permissionsArrayArg = args[1];
259
- if (!tagArg || !permissionsArrayArg)
260
- return;
261
- // Extract tag name
262
- let tag;
263
- if (ts.isStringLiteral(tagArg)) {
264
- tag = tagArg.text;
265
- }
266
- if (!tag) {
267
- logger.warn(`• addTagPermission call without valid tag string`);
268
- return;
269
- }
270
- // Check if permissions is a literal array or object
271
- if (!ts.isArrayLiteralExpression(permissionsArrayArg) &&
272
- !ts.isObjectLiteralExpression(permissionsArrayArg)) {
273
- logger.error(`• addTagPermission('${tag}', ...) must have a literal array or object as second argument`);
274
- return;
275
- }
276
- // Extract permission pikkuFuncIds from array
277
- const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
278
- if (permissionNames.length > 0) {
279
- renameTempDefinitions(state, permissionNames, 'tag', tag);
280
- }
281
- const allServices = new Set();
282
- for (const permissionName of permissionNames) {
283
- const permissionMeta = state.permissions.definitions[permissionName];
284
- if (permissionMeta && permissionMeta.services) {
285
- for (const service of permissionMeta.services.services) {
286
- allServices.add(service);
287
- }
288
- }
289
- }
290
- let isFactory = false;
291
- let exportedName = null;
292
- let parent = node.parent;
293
- if (parent && ts.isArrowFunction(parent)) {
294
- if (parent.parameters.length === 0) {
295
- isFactory = true;
296
- const arrowParent = parent.parent;
297
- if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
298
- if (ts.isIdentifier(arrowParent.name)) {
299
- if (isNamedExport(arrowParent)) {
300
- exportedName = arrowParent.name.text;
301
- }
302
- }
303
- }
304
- }
305
- }
306
- if (!isFactory) {
307
- const extracted = extractFunctionName(node, checker, state.rootDir);
308
- exportedName = extracted.exportedName;
309
- }
310
- if (!isFactory && exportedName) {
311
- logger.warn(`• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
312
- `For tree-shaking, use: export const ${exportedName} = () => addTagPermission('${tag}', [...])`);
313
- }
314
- state.permissions.tagPermissions.set(tag, {
315
- exportName: exportedName,
316
- sourceFile: node.getSourceFile().fileName,
317
- position: node.getStart(),
318
- services: {
319
- optimized: false,
320
- services: Array.from(allServices),
321
- },
322
- count: permissionNames.length,
323
- instanceIds: permissionNames,
324
- isFactory,
325
- });
326
- logger.debug(`• Found tag permission group: ${tag} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
327
- return;
328
- }
329
- // Handle addHTTPPermission(pattern, [permission1, permission2])
330
- // Supports two patterns:
331
- // 1. export const x = () => addHTTPPermission('*', [...]) (factory - tree-shakeable)
332
- // 2. export const x = addHTTPPermission('*', [...]) (direct - no tree-shaking)
333
- if (expression.text === 'addHTTPPermission') {
334
- const patternArg = args[0];
335
- const permissionsArrayArg = args[1];
336
- if (!patternArg || !permissionsArrayArg)
337
- return;
338
- // Extract route pattern
339
- let pattern;
340
- if (ts.isStringLiteral(patternArg)) {
341
- pattern = patternArg.text;
342
- }
343
- if (!pattern) {
344
- logger.warn(`• addHTTPPermission call without valid pattern string`);
345
- return;
346
- }
347
- // Check if permissions is a literal array or object
348
- if (!ts.isArrayLiteralExpression(permissionsArrayArg) &&
349
- !ts.isObjectLiteralExpression(permissionsArrayArg)) {
350
- logger.error(`• addHTTPPermission('${pattern}', ...) must have a literal array or object as second argument`);
351
- return;
352
- }
353
- // Extract permission pikkuFuncIds from array
354
- const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
355
- if (permissionNames.length > 0) {
356
- renameTempDefinitions(state, permissionNames, 'http', pattern);
357
- }
358
- const allServices = new Set();
359
- for (const permissionName of permissionNames) {
360
- const permissionMeta = state.permissions.definitions[permissionName];
361
- if (permissionMeta && permissionMeta.services) {
362
- for (const service of permissionMeta.services.services) {
363
- allServices.add(service);
364
- }
365
- }
366
- }
367
- let isFactory = false;
368
- let exportedName = null;
369
- let parent = node.parent;
370
- if (parent && ts.isArrowFunction(parent)) {
371
- if (parent.parameters.length === 0) {
372
- isFactory = true;
373
- const arrowParent = parent.parent;
374
- if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
375
- if (ts.isIdentifier(arrowParent.name)) {
376
- if (isNamedExport(arrowParent)) {
377
- exportedName = arrowParent.name.text;
378
- }
379
- }
380
- }
381
- }
382
- }
383
- if (!isFactory) {
384
- const extracted = extractFunctionName(node, checker, state.rootDir);
385
- exportedName = extracted.exportedName;
386
- }
387
- if (!isFactory && exportedName) {
388
- logger.warn(`• HTTP permission group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
389
- `For tree-shaking, use: export const ${exportedName} = () => addHTTPPermission('${pattern}', [...])`);
390
- }
391
- state.http.routePermissions.set(pattern, {
392
- exportName: exportedName,
393
- sourceFile: node.getSourceFile().fileName,
394
- position: node.getStart(),
395
- services: {
396
- optimized: false,
397
- services: Array.from(allServices),
398
- },
399
- count: permissionNames.length,
400
- instanceIds: permissionNames,
401
- isFactory,
402
- });
403
- logger.debug(`• Found HTTP route permission group: ${pattern} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
404
- return;
405
- }
406
232
  };
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
3
  import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
+ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js';
5
6
  import { resolveMiddleware } from '../utils/middleware.js';
6
7
  import { extractWireNames } from '../utils/post-process.js';
7
8
  import { resolveAddonName } from '../utils/resolve-addon-package.js';
@@ -44,6 +45,8 @@ export const addQueueWorker = (logger, node, checker, state) => {
44
45
  logger.critical(ErrorCode.MISSING_QUEUE_NAME, `No 'name' provided for queue processor function '${pikkuFuncId}'.`);
45
46
  return;
46
47
  }
48
+ // Register metadata for a func inlined into the wiring (see helper).
49
+ ensureInlineWiringFunction(state, pikkuFuncId, name, funcInitializer, checker, extracted.isHelper);
47
50
  // --- resolve middleware ---
48
51
  const middleware = resolveMiddleware(state, obj, tags, checker);
49
52
  // --- track used functions/middleware for service aggregation ---
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
3
  import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
+ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js';
5
6
  import { resolveMiddleware } from '../utils/middleware.js';
6
7
  import { extractWireNames } from '../utils/post-process.js';
7
8
  import { resolveAddonName } from '../utils/resolve-addon-package.js';
@@ -43,6 +44,8 @@ export const addSchedule = (logger, node, checker, state, options) => {
43
44
  if (!nameValue || !scheduleValue) {
44
45
  return;
45
46
  }
47
+ // Register metadata for a func inlined into the wiring (see helper).
48
+ ensureInlineWiringFunction(state, pikkuFuncId, nameValue, funcInitializer, checker, extracted.isHelper);
46
49
  // --- resolve middleware ---
47
50
  const middleware = resolveMiddleware(state, obj, tags, checker);
48
51
  // --- track used functions/middleware for service aggregation ---
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addScope: AddWiring;
@@ -0,0 +1,146 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue } from '../utils/get-property-value.js';
3
+ import { ErrorCode } from '../error-codes.js';
4
+ const SEPARATOR = ':';
5
+ const WILDCARD = '*';
6
+ /**
7
+ * Validates a single scope segment, reporting rather than throwing so the
8
+ * inspector can surface every problem in one pass.
9
+ *
10
+ * @returns true when the segment is usable.
11
+ */
12
+ const isValidSegment = (segment, scopeName, logger) => {
13
+ if (segment.length === 0) {
14
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope '${scopeName}' contains an empty segment.`);
15
+ return false;
16
+ }
17
+ if (segment.includes(SEPARATOR)) {
18
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope segment '${segment}' in '${scopeName}' contains the '${SEPARATOR}' separator. ` +
19
+ `Nest scopes with the 'scopes' property instead of embedding '${SEPARATOR}' in a name.`);
20
+ return false;
21
+ }
22
+ if (segment === WILDCARD) {
23
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope segment '${segment}' in '${scopeName}' is the wildcard. ` +
24
+ `'${WILDCARD}' is reserved for granting a scope and its descendants, and cannot be declared.`);
25
+ return false;
26
+ }
27
+ return true;
28
+ };
29
+ /**
30
+ * Recursively extracts a `scopes: { ... }` object literal into nested metadata.
31
+ */
32
+ const extractScopeNodes = (obj, scopeName, logger) => {
33
+ const nodes = {};
34
+ for (const prop of obj.properties) {
35
+ if (!ts.isPropertyAssignment(prop)) {
36
+ continue;
37
+ }
38
+ let segment;
39
+ if (ts.isIdentifier(prop.name)) {
40
+ segment = prop.name.text;
41
+ }
42
+ else if (ts.isStringLiteral(prop.name)) {
43
+ segment = prop.name.text;
44
+ }
45
+ if (segment === undefined) {
46
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `Scope '${scopeName}' has a nested scope whose key is not a literal.`);
47
+ continue;
48
+ }
49
+ if (!isValidSegment(segment, scopeName, logger)) {
50
+ continue;
51
+ }
52
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
53
+ logger.critical(ErrorCode.INVALID_VALUE, `Nested scope '${segment}' in '${scopeName}' must be an object literal.`);
54
+ continue;
55
+ }
56
+ const node = {};
57
+ const displayName = getPropertyValue(prop.initializer, 'displayName');
58
+ if (displayName) {
59
+ node.displayName = displayName;
60
+ }
61
+ const description = getPropertyValue(prop.initializer, 'description');
62
+ if (description) {
63
+ node.description = description;
64
+ }
65
+ const nestedProp = prop.initializer.properties.find((p) => ts.isPropertyAssignment(p) &&
66
+ ts.isIdentifier(p.name) &&
67
+ p.name.text === 'scopes');
68
+ if (nestedProp &&
69
+ ts.isPropertyAssignment(nestedProp) &&
70
+ ts.isObjectLiteralExpression(nestedProp.initializer)) {
71
+ const nested = extractScopeNodes(nestedProp.initializer, scopeName, logger);
72
+ if (nested) {
73
+ node.scopes = nested;
74
+ }
75
+ }
76
+ nodes[segment] = node;
77
+ }
78
+ return Object.keys(nodes).length > 0 ? nodes : undefined;
79
+ };
80
+ /**
81
+ * Unwraps `x as const` / `x as any` so a cast declaration is still extracted
82
+ * rather than silently skipped.
83
+ */
84
+ const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
85
+ ? unwrapAs(node.expression)
86
+ : node;
87
+ export const addScope = (logger, node, checker, state, _options) => {
88
+ if (!ts.isCallExpression(node)) {
89
+ return;
90
+ }
91
+ const expression = node.expression;
92
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireScope') {
93
+ return;
94
+ }
95
+ const firstArg = node.arguments[0];
96
+ if (!firstArg) {
97
+ return;
98
+ }
99
+ const unwrapped = unwrapAs(firstArg);
100
+ if (!ts.isObjectLiteralExpression(unwrapped)) {
101
+ return;
102
+ }
103
+ const sourceFile = node.getSourceFile().fileName;
104
+ // Roots are keyed exactly like the nodes beneath them, so each property of
105
+ // the call's single argument is one tree.
106
+ for (const prop of unwrapped.properties) {
107
+ if (!ts.isPropertyAssignment(prop)) {
108
+ continue;
109
+ }
110
+ let name;
111
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
112
+ name = prop.name.text;
113
+ }
114
+ if (name === undefined) {
115
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A scope is declared with a key that is not a literal.');
116
+ continue;
117
+ }
118
+ if (!isValidSegment(name, name, logger)) {
119
+ continue;
120
+ }
121
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
122
+ logger.critical(ErrorCode.INVALID_VALUE, `Scope '${name}' must be an object literal.`);
123
+ continue;
124
+ }
125
+ const root = prop.initializer;
126
+ const displayNameValue = getPropertyValue(root, 'displayName');
127
+ const descriptionValue = getPropertyValue(root, 'description');
128
+ let scopes;
129
+ const scopesProp = root.properties.find((p) => ts.isPropertyAssignment(p) &&
130
+ ts.isIdentifier(p.name) &&
131
+ p.name.text === 'scopes');
132
+ if (scopesProp &&
133
+ ts.isPropertyAssignment(scopesProp) &&
134
+ ts.isObjectLiteralExpression(scopesProp.initializer)) {
135
+ scopes = extractScopeNodes(scopesProp.initializer, name, logger);
136
+ }
137
+ state.scopes.files.add(sourceFile);
138
+ state.scopes.definitions.push({
139
+ name,
140
+ displayName: displayNameValue || undefined,
141
+ description: descriptionValue || undefined,
142
+ scopes,
143
+ sourceFile,
144
+ });
145
+ }
146
+ };
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
3
3
  import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
4
4
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
+ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js';
5
6
  import { resolveMiddleware } from '../utils/middleware.js';
6
7
  import { extractWireNames } from '../utils/post-process.js';
7
8
  import { resolveAddonName } from '../utils/resolve-addon-package.js';
@@ -45,6 +46,8 @@ const addWireTrigger = (logger, node, checker, state, firstArg) => {
45
46
  if (!nameValue) {
46
47
  return;
47
48
  }
49
+ // Register metadata for a func inlined into the wiring (see helper).
50
+ ensureInlineWiringFunction(state, pikkuFuncId, nameValue, funcInitializer, checker, extracted.isHelper);
48
51
  // --- resolve middleware ---
49
52
  const middleware = resolveMiddleware(state, obj, tags, checker);
50
53
  // --- track used functions/middleware for service aggregation ---
@@ -0,0 +1,10 @@
1
+ import * as ts from 'typescript';
2
+ import type { InspectorState, InspectorLogger } from '../types.js';
3
+ /**
4
+ * Detect wireRemoteAddon({ name: '...', package: '...' }) call expressions and
5
+ * record the namespace as a **remote** addon declaration. Only name + package
6
+ * are needed for codegen (serverUrl/auth are runtime closures resolved by the
7
+ * RPC runner); the `remote` marker tells the consumer's RPC-map generator to
8
+ * import the addon's `.remote.gen` map instead of `.internal.gen`.
9
+ */
10
+ export declare function addWireRemoteAddon(node: ts.Node, state: InspectorState, logger: InspectorLogger): void;
@@ -0,0 +1,66 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Detect wireRemoteAddon({ name: '...', package: '...' }) call expressions and
4
+ * record the namespace as a **remote** addon declaration. Only name + package
5
+ * are needed for codegen (serverUrl/auth are runtime closures resolved by the
6
+ * RPC runner); the `remote` marker tells the consumer's RPC-map generator to
7
+ * import the addon's `.remote.gen` map instead of `.internal.gen`.
8
+ */
9
+ export function addWireRemoteAddon(node, state, logger) {
10
+ if (!ts.isCallExpression(node))
11
+ return;
12
+ const { expression, arguments: args } = node;
13
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireRemoteAddon')
14
+ return;
15
+ const [firstArg] = args;
16
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
17
+ return;
18
+ let name;
19
+ let pkg;
20
+ // The auth binding is a runtime closure; we only extract the statically
21
+ // knowable slot names so `pikku verify` can check they exist in the
22
+ // consumer's own wirings. `hasAuth` distinguishes "public" (omitted) from
23
+ // "bound via a custom resolve()" (present but no static id).
24
+ let hasAuth = false;
25
+ let authCredentialId;
26
+ let authSecretId;
27
+ for (const prop of firstArg.properties) {
28
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
29
+ continue;
30
+ const key = prop.name.text;
31
+ if (key === 'name' && ts.isStringLiteral(prop.initializer)) {
32
+ name = prop.initializer.text;
33
+ }
34
+ else if (key === 'package' && ts.isStringLiteral(prop.initializer)) {
35
+ pkg = prop.initializer.text;
36
+ }
37
+ else if (key === 'auth' &&
38
+ ts.isObjectLiteralExpression(prop.initializer)) {
39
+ hasAuth = true;
40
+ for (const authProp of prop.initializer.properties) {
41
+ if (!ts.isPropertyAssignment(authProp) || !ts.isIdentifier(authProp.name))
42
+ continue;
43
+ if (authProp.name.text === 'credentialId' &&
44
+ ts.isStringLiteral(authProp.initializer)) {
45
+ authCredentialId = authProp.initializer.text;
46
+ }
47
+ else if (authProp.name.text === 'secretId' &&
48
+ ts.isStringLiteral(authProp.initializer)) {
49
+ authSecretId = authProp.initializer.text;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ if (!name || !pkg)
55
+ return;
56
+ logger.debug(`• Found wireRemoteAddon: ${name} → ${pkg} (remote)`);
57
+ state.rpc.wireAddonDeclarations.set(name, {
58
+ package: pkg,
59
+ remote: true,
60
+ hasAuth,
61
+ authCredentialId,
62
+ authSecretId,
63
+ });
64
+ state.rpc.usedAddons.add(name);
65
+ state.rpc.wireAddonFiles.add(node.getSourceFile().fileName);
66
+ }
@@ -2,6 +2,11 @@ import * as ts from 'typescript';
2
2
  import { ErrorCode } from '../error-codes.js';
3
3
  import { extractStringLiteral } from '../utils/extract-node-value.js';
4
4
  function extractAstValue(expr, refParamName, templateParamName) {
5
+ // `'GET' as const` / parenthesised values wrap the real initializer — see
6
+ // through them or the whole property is silently dropped from the meta.
7
+ while (ts.isAsExpression(expr) || ts.isParenthesizedExpression(expr)) {
8
+ expr = expr.expression;
9
+ }
5
10
  if (ts.isStringLiteral(expr)) {
6
11
  return expr.text;
7
12
  }
@@ -113,9 +118,13 @@ function extractInputMapping(node, _checker) {
113
118
  const refParamName = node.parameters.length > 0 && ts.isIdentifier(node.parameters[0].name)
114
119
  ? node.parameters[0].name.text
115
120
  : 'ref';
121
+ // The canonical form imports `template` from '@pikku/core/workflow' and calls
122
+ // it inside a single-param `(ref) => (...)` arrow; the older form passes it as
123
+ // a second arrow parameter. Support both — default to the imported name so a
124
+ // `template(...)` value is never dropped just because it isn't a 2nd param.
116
125
  const templateParamName = node.parameters.length > 1 && ts.isIdentifier(node.parameters[1].name)
117
126
  ? node.parameters[1].name.text
118
- : undefined;
127
+ : 'template';
119
128
  const input = {};
120
129
  for (const prop of bodyObj.properties) {
121
130
  if (!ts.isPropertyAssignment(prop))
@@ -138,6 +147,12 @@ function extractInputMapping(node, _checker) {
138
147
  * Extract next config (string, array, or record)
139
148
  */
140
149
  function extractNextConfig(node, _checker) {
150
+ // Key-based branch `next` (`{ key: [targets] }`) is authored with an `as any`
151
+ // cast because the graph's NextConfig can't narrow record targets to node-id
152
+ // literals; see through that (and parens) so the routing survives into the meta.
153
+ while (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
154
+ node = node.expression;
155
+ }
141
156
  if (ts.isStringLiteral(node)) {
142
157
  return node.text;
143
158
  }
@@ -207,6 +222,7 @@ function extractWorkflowGraphConfig(configArg, checker) {
207
222
  let name;
208
223
  let description;
209
224
  let tags;
225
+ let notes;
210
226
  let disabled;
211
227
  let nodesNode;
212
228
  let configNode;
@@ -231,6 +247,12 @@ function extractWorkflowGraphConfig(configArg, checker) {
231
247
  .filter(ts.isStringLiteral)
232
248
  .map((el) => el.text);
233
249
  }
250
+ else if (propName === 'notes' &&
251
+ ts.isArrayLiteralExpression(prop.initializer)) {
252
+ notes = prop.initializer.elements
253
+ .filter(ts.isStringLiteral)
254
+ .map((el) => el.text);
255
+ }
234
256
  else if (propName === 'nodes' &&
235
257
  ts.isObjectLiteralExpression(prop.initializer)) {
236
258
  nodesNode = prop.initializer;
@@ -240,7 +262,7 @@ function extractWorkflowGraphConfig(configArg, checker) {
240
262
  configNode = prop.initializer;
241
263
  }
242
264
  }
243
- return { name, description, tags, disabled, nodesNode, configNode };
265
+ return { name, description, tags, notes, disabled, nodesNode, configNode };
244
266
  }
245
267
  /**
246
268
  * Extract graph nodes from the new pikkuWorkflowGraph format
@@ -283,6 +305,7 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
283
305
  onError: undefined,
284
306
  retries: undefined,
285
307
  retryDelay: undefined,
308
+ notes: undefined,
286
309
  };
287
310
  }
288
311
  // Extract config for each node from 'config' property
@@ -305,6 +328,7 @@ function extractGraphFromNewFormat(nodesNode, configNode, checker, state) {
305
328
  nodes[nodeId].input = nodeConfig.input;
306
329
  nodes[nodeId].retries = nodeConfig.retries;
307
330
  nodes[nodeId].retryDelay = nodeConfig.retryDelay;
331
+ nodes[nodeId].notes = nodeConfig.notes;
308
332
  }
309
333
  }
310
334
  }
@@ -320,6 +344,7 @@ function extractNodeConfigFromObject(obj, checker) {
320
344
  let input = {};
321
345
  let retries = undefined;
322
346
  let retryDelay = undefined;
347
+ let notes = undefined;
323
348
  for (const prop of obj.properties) {
324
349
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
325
350
  continue;
@@ -346,8 +371,11 @@ function extractNodeConfigFromObject(obj, checker) {
346
371
  retryDelay = prop.initializer.text;
347
372
  }
348
373
  }
374
+ else if (propName === 'notes') {
375
+ notes = extractStringLiteral(prop.initializer, checker);
376
+ }
349
377
  }
350
- return { next, onError, input, retries, retryDelay };
378
+ return { next, onError, input, retries, retryDelay, notes };
351
379
  }
352
380
  /**
353
381
  * Inspector for pikkuWorkflowGraph() calls
@@ -402,6 +430,7 @@ export const addWorkflowGraph = (logger, node, checker, state) => {
402
430
  source: 'graph',
403
431
  description: graphConfig.description,
404
432
  tags: graphConfig.tags,
433
+ notes: graphConfig.notes,
405
434
  nodes: graphNodes,
406
435
  entryNodeIds,
407
436
  };
@@ -62,8 +62,8 @@ function hasInlineSteps(steps) {
62
62
  if (step.defaultSteps && hasInlineSteps(step.defaultSteps))
63
63
  return true;
64
64
  }
65
- else if (step.type === 'fanout' && step.child) {
66
- if (hasInlineSteps([step.child]))
65
+ else if (step.type === 'fanout' && step.body) {
66
+ if (hasInlineSteps(step.body))
67
67
  return true;
68
68
  }
69
69
  else if (step.type === 'parallel' && step.children) {
@@ -80,6 +80,11 @@ export function collectInvokedRPCs(steps, rpcs) {
80
80
  for (const step of steps) {
81
81
  if (step.type === 'rpc' && step.rpcName) {
82
82
  rpcs.add(step.rpcName);
83
+ // The compensation handler is invoked at runtime too, so it has to be
84
+ // wired like any other step.
85
+ if (step.options?.onError) {
86
+ rpcs.add(step.options.onError);
87
+ }
83
88
  }
84
89
  else if (step.type === 'branch') {
85
90
  for (const branch of step.branches) {
@@ -96,8 +101,8 @@ export function collectInvokedRPCs(steps, rpcs) {
96
101
  if (step.defaultSteps)
97
102
  collectInvokedRPCs(step.defaultSteps, rpcs);
98
103
  }
99
- else if (step.type === 'fanout' && step.child) {
100
- collectInvokedRPCs([step.child], rpcs);
104
+ else if (step.type === 'fanout' && step.body) {
105
+ collectInvokedRPCs(step.body, rpcs);
101
106
  }
102
107
  else if (step.type === 'parallel' && step.children) {
103
108
  collectInvokedRPCs(step.children, rpcs);
@@ -22,6 +22,9 @@ export declare enum ErrorCode {
22
22
  EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
23
23
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
24
24
  INVALID_DSL_WORKFLOW = "PKU641",
25
+ WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
26
+ REMOTE_ADDON_NOT_DEV_DEPENDENCY = "PKU338",
27
+ REMOTE_ADDON_AUTH_UNRESOLVED = "PKU339",
25
28
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
26
29
  DB_JSON_COLUMN_UNTYPED = "PKU481",
27
30
  DB_FORMAT_ON_NON_STRING = "PKU482",
@@ -24,6 +24,10 @@ export var ErrorCode;
24
24
  ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
25
25
  ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
26
26
  ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
27
+ ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
28
+ // Remote addon (wireRemoteAddon) validation
29
+ ErrorCode["REMOTE_ADDON_NOT_DEV_DEPENDENCY"] = "PKU338";
30
+ ErrorCode["REMOTE_ADDON_AUTH_UNRESOLVED"] = "PKU339";
27
31
  // Database schema codegen warnings
28
32
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
29
33
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";