@pikku/inspector 0.11.0 → 0.11.1

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 (77) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/add/add-channel.js +11 -10
  3. package/dist/add/add-file-with-factory.js +10 -10
  4. package/dist/add/add-functions.js +57 -43
  5. package/dist/add/add-http-route.js +5 -4
  6. package/dist/add/add-mcp-prompt.js +6 -5
  7. package/dist/add/add-mcp-resource.js +6 -5
  8. package/dist/add/add-mcp-tool.js +6 -5
  9. package/dist/add/add-middleware.js +1 -1
  10. package/dist/add/add-permission.js +1 -1
  11. package/dist/add/add-queue-worker.js +6 -5
  12. package/dist/add/add-schedule.js +5 -4
  13. package/dist/add/add-workflow.d.ts +1 -1
  14. package/dist/add/add-workflow.js +92 -66
  15. package/dist/error-codes.d.ts +1 -0
  16. package/dist/error-codes.js +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +1 -0
  19. package/dist/inspector.js +10 -6
  20. package/dist/types.d.ts +21 -8
  21. package/dist/utils/extract-function-node.d.ts +10 -0
  22. package/dist/utils/extract-function-node.js +38 -0
  23. package/dist/utils/extract-node-value.d.ts +8 -0
  24. package/dist/utils/extract-node-value.js +24 -0
  25. package/dist/utils/extract-service-metadata.d.ts +19 -0
  26. package/dist/utils/extract-service-metadata.js +244 -0
  27. package/dist/utils/get-files-and-methods.d.ts +3 -3
  28. package/dist/utils/get-files-and-methods.js +3 -3
  29. package/dist/utils/get-property-value.d.ts +13 -6
  30. package/dist/utils/get-property-value.js +51 -43
  31. package/dist/utils/post-process.d.ts +9 -0
  32. package/dist/utils/post-process.js +30 -3
  33. package/dist/utils/serialize-inspector-state.d.ts +18 -5
  34. package/dist/utils/serialize-inspector-state.js +12 -10
  35. package/dist/utils/write-service-metadata.d.ts +13 -0
  36. package/dist/utils/write-service-metadata.js +37 -0
  37. package/dist/visit.js +2 -2
  38. package/dist/workflow/extract-simple-workflow.d.ts +15 -0
  39. package/dist/workflow/extract-simple-workflow.js +803 -0
  40. package/dist/workflow/patterns.d.ts +39 -0
  41. package/dist/workflow/patterns.js +138 -0
  42. package/dist/workflow/validation.d.ts +28 -0
  43. package/dist/workflow/validation.js +124 -0
  44. package/package.json +4 -4
  45. package/src/add/add-channel.ts +37 -17
  46. package/src/add/add-file-with-factory.ts +10 -10
  47. package/src/add/add-functions.ts +72 -56
  48. package/src/add/add-http-route.ts +10 -5
  49. package/src/add/add-mcp-prompt.ts +11 -7
  50. package/src/add/add-mcp-resource.ts +11 -7
  51. package/src/add/add-mcp-tool.ts +11 -7
  52. package/src/add/add-middleware.ts +1 -1
  53. package/src/add/add-permission.ts +1 -1
  54. package/src/add/add-queue-worker.ts +11 -12
  55. package/src/add/add-schedule.ts +10 -5
  56. package/src/add/add-workflow.ts +120 -110
  57. package/src/error-codes.ts +1 -0
  58. package/src/index.ts +2 -0
  59. package/src/inspector.ts +16 -6
  60. package/src/types.ts +18 -8
  61. package/src/utils/extract-function-node.ts +58 -0
  62. package/src/utils/extract-node-value.ts +31 -0
  63. package/src/utils/extract-service-metadata.ts +353 -0
  64. package/src/utils/filter-inspector-state.test.ts +3 -3
  65. package/src/utils/filter-utils.test.ts +45 -51
  66. package/src/utils/get-files-and-methods.ts +11 -11
  67. package/src/utils/get-property-value.ts +60 -53
  68. package/src/utils/permissions.test.ts +3 -3
  69. package/src/utils/post-process.ts +56 -3
  70. package/src/utils/serialize-inspector-state.ts +28 -18
  71. package/src/utils/test-data/inspector-state.json +9 -9
  72. package/src/utils/write-service-metadata.ts +51 -0
  73. package/src/visit.ts +3 -3
  74. package/src/workflow/extract-simple-workflow.ts +1035 -0
  75. package/src/workflow/patterns.ts +182 -0
  76. package/src/workflow/validation.ts +153 -0
  77. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,803 @@
1
+ import * as ts from 'typescript';
2
+ import { extractStringLiteral, extractNumberLiteral, } from '../utils/extract-node-value.js';
3
+ import { isWorkflowDoCall, isWorkflowSleepCall, isParallelFanout, isParallelGroup, isSequentialFanout, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
4
+ import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
5
+ /**
6
+ * Extract simple workflow metadata from a function declaration
7
+ */
8
+ export function extractSimpleWorkflow(funcNode, checker) {
9
+ try {
10
+ // Find the async arrow function
11
+ const arrowFunc = findWorkflowFunction(funcNode);
12
+ if (!arrowFunc) {
13
+ return {
14
+ status: 'error',
15
+ reason: 'Could not find async arrow function in workflow definition',
16
+ simple: false,
17
+ };
18
+ }
19
+ // Extract input parameter name (second parameter)
20
+ const inputParamName = extractInputParamName(arrowFunc);
21
+ if (!inputParamName) {
22
+ return {
23
+ status: 'error',
24
+ reason: 'Could not determine input parameter name',
25
+ simple: false,
26
+ };
27
+ }
28
+ // Initialize extraction context
29
+ const context = {
30
+ checker,
31
+ outputVars: new Map(),
32
+ arrayVars: new Set(),
33
+ conditionalVars: new Set(),
34
+ inputParamName,
35
+ errors: [],
36
+ };
37
+ // Validate no disallowed patterns
38
+ const patternErrors = validateNoDisallowedPatterns(arrowFunc.body);
39
+ if (patternErrors.length > 0) {
40
+ return {
41
+ status: 'error',
42
+ reason: formatValidationErrors(patternErrors),
43
+ simple: false,
44
+ };
45
+ }
46
+ // Validate all workflow calls are awaited
47
+ const awaitErrors = validateAwaitedCalls(arrowFunc.body);
48
+ if (awaitErrors.length > 0) {
49
+ return {
50
+ status: 'error',
51
+ reason: formatValidationErrors(awaitErrors),
52
+ simple: false,
53
+ };
54
+ }
55
+ // Extract steps from function body
56
+ const steps = extractSteps(arrowFunc.body, context);
57
+ // Check for any accumulated errors
58
+ if (context.errors.length > 0) {
59
+ return {
60
+ status: 'error',
61
+ reason: formatValidationErrors(context.errors),
62
+ simple: false,
63
+ };
64
+ }
65
+ return {
66
+ status: 'ok',
67
+ steps,
68
+ simple: true,
69
+ };
70
+ }
71
+ catch (error) {
72
+ return {
73
+ status: 'error',
74
+ reason: error instanceof Error ? error.message : String(error),
75
+ simple: false,
76
+ };
77
+ }
78
+ }
79
+ /**
80
+ * Find the workflow function (async arrow function)
81
+ */
82
+ function findWorkflowFunction(node) {
83
+ // Handle pikkuSimpleWorkflowFunc(async () => {}) or pikkuWorkflowFunc(async () => {})
84
+ if (ts.isCallExpression(node)) {
85
+ const arg = node.arguments[0];
86
+ if (arg && ts.isArrowFunction(arg)) {
87
+ return arg;
88
+ }
89
+ // Also check if first argument is an object with func property
90
+ if (arg && ts.isObjectLiteralExpression(arg)) {
91
+ for (const prop of arg.properties) {
92
+ if (ts.isPropertyAssignment(prop) &&
93
+ ts.isIdentifier(prop.name) &&
94
+ prop.name.text === 'func') {
95
+ if (ts.isArrowFunction(prop.initializer)) {
96
+ return prop.initializer;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ // Handle pikkuSimpleWorkflowFunc({ func: async () => {} })
103
+ if (ts.isObjectLiteralExpression(node)) {
104
+ for (const prop of node.properties) {
105
+ if (ts.isPropertyAssignment(prop) &&
106
+ ts.isIdentifier(prop.name) &&
107
+ prop.name.text === 'func') {
108
+ if (ts.isArrowFunction(prop.initializer)) {
109
+ return prop.initializer;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Extract the input parameter name from the arrow function
118
+ */
119
+ function extractInputParamName(arrowFunc) {
120
+ if (arrowFunc.parameters.length < 2) {
121
+ return null;
122
+ }
123
+ const secondParam = arrowFunc.parameters[1];
124
+ if (ts.isIdentifier(secondParam.name)) {
125
+ return secondParam.name.text;
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Extract steps from the function body
131
+ */
132
+ function extractSteps(body, context) {
133
+ const steps = [];
134
+ if (!ts.isBlock(body)) {
135
+ return steps;
136
+ }
137
+ for (const statement of body.statements) {
138
+ const extracted = extractStep(statement, context);
139
+ if (extracted) {
140
+ steps.push(extracted);
141
+ }
142
+ }
143
+ return steps;
144
+ }
145
+ /**
146
+ * Extract a single step from a statement
147
+ */
148
+ function extractStep(statement, context) {
149
+ // Variable declaration with workflow.do assignment
150
+ if (ts.isVariableStatement(statement)) {
151
+ return extractVariableDeclaration(statement, context);
152
+ }
153
+ // Expression statement (await workflow.do without assignment)
154
+ if (ts.isExpressionStatement(statement)) {
155
+ return extractExpressionStatement(statement, context);
156
+ }
157
+ // If statement (branch)
158
+ if (ts.isIfStatement(statement)) {
159
+ return extractBranch(statement, context);
160
+ }
161
+ // For-of statement (sequential fanout)
162
+ if (ts.isForOfStatement(statement)) {
163
+ return extractSequentialFanout(statement, context);
164
+ }
165
+ // Return statement
166
+ if (ts.isReturnStatement(statement)) {
167
+ return extractReturn(statement, context);
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * Extract variable declaration (const x = await workflow.do(...))
173
+ */
174
+ function extractVariableDeclaration(statement, context) {
175
+ const declList = statement.declarationList;
176
+ if (declList.declarations.length !== 1) {
177
+ return null;
178
+ }
179
+ const decl = declList.declarations[0];
180
+ if (!ts.isIdentifier(decl.name)) {
181
+ return null;
182
+ }
183
+ const varName = decl.name.text;
184
+ const init = decl.initializer;
185
+ if (!init) {
186
+ return null;
187
+ }
188
+ // Check for await workflow.do(...)
189
+ if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
190
+ const call = init.expression;
191
+ if (isWorkflowDoCall(call, context.checker)) {
192
+ const step = extractRpcStep(call, context, varName);
193
+ if (step) {
194
+ // Track output variable
195
+ const type = context.checker.getTypeAtLocation(decl);
196
+ context.outputVars.set(varName, { type, node: decl });
197
+ // Check if it's an array type
198
+ if (isArrayType(type, context.checker)) {
199
+ context.arrayVars.add(varName);
200
+ }
201
+ // Check if it's a conditional variable (let x: T | undefined)
202
+ if (declList.flags & ts.NodeFlags.Let) {
203
+ const typeNode = decl.type;
204
+ if (typeNode && ts.isUnionTypeNode(typeNode)) {
205
+ // Check if union includes undefined
206
+ const hasUndefined = typeNode.types.some((t) => (ts.isLiteralTypeNode(t) &&
207
+ t.literal.kind === ts.SyntaxKind.UndefinedKeyword) ||
208
+ t.kind === ts.SyntaxKind.UndefinedKeyword);
209
+ if (hasUndefined) {
210
+ context.conditionalVars.add(varName);
211
+ }
212
+ }
213
+ }
214
+ return step;
215
+ }
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ /**
221
+ * Extract expression statement (await workflow.do(...) without assignment)
222
+ */
223
+ function extractExpressionStatement(statement, context) {
224
+ let expr = statement.expression;
225
+ // Handle assignment: owner = await workflow.do(...)
226
+ let outputVar;
227
+ if (ts.isBinaryExpression(expr) &&
228
+ expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
229
+ // Extract variable name from left side
230
+ if (ts.isIdentifier(expr.left)) {
231
+ outputVar = expr.left.text;
232
+ }
233
+ // Use right side as the expression to extract from
234
+ expr = expr.right;
235
+ }
236
+ // await workflow.do(...)
237
+ if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
238
+ const call = expr.expression;
239
+ if (isWorkflowDoCall(call, context.checker)) {
240
+ const step = extractRpcStep(call, context, outputVar);
241
+ // Track output variable if this is an assignment
242
+ if (outputVar && step) {
243
+ const type = context.checker.getTypeAtLocation(expr);
244
+ context.outputVars.set(outputVar, { type, node: expr });
245
+ // Check if it's an array type
246
+ if (isArrayType(type, context.checker)) {
247
+ context.arrayVars.add(outputVar);
248
+ }
249
+ }
250
+ return step;
251
+ }
252
+ if (isWorkflowSleepCall(call, context.checker)) {
253
+ return extractSleepStep(call, context);
254
+ }
255
+ // Check for parallel group or fanout
256
+ if (isParallelFanout(call)) {
257
+ return extractParallelFanout(call, context);
258
+ }
259
+ if (isParallelGroup(call)) {
260
+ return extractParallelGroup(call, context);
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+ /**
266
+ * Extract RPC step from workflow.do() call
267
+ */
268
+ function extractRpcStep(call, context, outputVar) {
269
+ const args = call.arguments;
270
+ if (args.length < 2) {
271
+ return null;
272
+ }
273
+ try {
274
+ const stepName = extractStringLiteral(args[0], context.checker);
275
+ const rpcName = extractStringLiteral(args[1], context.checker);
276
+ // Extract inputs from third argument
277
+ const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
278
+ // Extract options from fourth argument
279
+ const options = args.length >= 4 && ts.isObjectLiteralExpression(args[3])
280
+ ? extractStepOptions(args[3], context)
281
+ : undefined;
282
+ return {
283
+ type: 'rpc',
284
+ stepName,
285
+ rpcName,
286
+ outputVar,
287
+ inputs,
288
+ options,
289
+ };
290
+ }
291
+ catch (error) {
292
+ context.errors.push({
293
+ message: `Failed to extract RPC step: ${error instanceof Error ? error.message : String(error)}`,
294
+ node: call,
295
+ });
296
+ return null;
297
+ }
298
+ }
299
+ /**
300
+ * Extract step options from options object
301
+ */
302
+ function extractStepOptions(optionsNode, context) {
303
+ const options = {};
304
+ for (const prop of optionsNode.properties) {
305
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
306
+ const propName = prop.name.text;
307
+ if (propName === 'retries') {
308
+ const retries = extractNumberLiteral(prop.initializer);
309
+ if (retries !== null) {
310
+ options.retries = retries;
311
+ }
312
+ }
313
+ else if (propName === 'retryDelay') {
314
+ try {
315
+ if (ts.isStringLiteral(prop.initializer)) {
316
+ options.retryDelay = prop.initializer.text;
317
+ }
318
+ else {
319
+ const delay = extractNumberLiteral(prop.initializer);
320
+ if (delay !== null) {
321
+ options.retryDelay = delay;
322
+ }
323
+ }
324
+ }
325
+ catch {
326
+ // Ignore extraction errors for retryDelay
327
+ }
328
+ }
329
+ else if (propName === 'description') {
330
+ try {
331
+ options.description = extractStringLiteral(prop.initializer, context.checker);
332
+ }
333
+ catch {
334
+ // Ignore extraction errors for description
335
+ }
336
+ }
337
+ }
338
+ }
339
+ return Object.keys(options).length > 0 ? options : undefined;
340
+ }
341
+ /**
342
+ * Extract sleep step from workflow.sleep() call
343
+ */
344
+ function extractSleepStep(call, context) {
345
+ const args = call.arguments;
346
+ if (args.length < 2) {
347
+ return null;
348
+ }
349
+ try {
350
+ const stepName = extractStringLiteral(args[0], context.checker);
351
+ let duration;
352
+ const numValue = extractNumberLiteral(args[1]);
353
+ if (numValue !== null) {
354
+ duration = numValue;
355
+ }
356
+ else {
357
+ duration = extractStringLiteral(args[1], context.checker);
358
+ }
359
+ return {
360
+ type: 'sleep',
361
+ stepName,
362
+ duration,
363
+ };
364
+ }
365
+ catch (error) {
366
+ context.errors.push({
367
+ message: `Failed to extract sleep step: ${error instanceof Error ? error.message : String(error)}`,
368
+ node: call,
369
+ });
370
+ return null;
371
+ }
372
+ }
373
+ /**
374
+ * Extract branch step from if statement
375
+ */
376
+ function extractBranch(statement, context) {
377
+ const condition = getSourceText(statement.expression);
378
+ // Handle both block statements and single statements
379
+ const thenSteps = ts.isBlock(statement.thenStatement)
380
+ ? extractSteps(statement.thenStatement, context)
381
+ : extractStepsFromStatement(statement.thenStatement, context);
382
+ const elseSteps = statement.elseStatement
383
+ ? ts.isBlock(statement.elseStatement)
384
+ ? extractSteps(statement.elseStatement, context)
385
+ : extractStepsFromStatement(statement.elseStatement, context)
386
+ : undefined;
387
+ return {
388
+ type: 'branch',
389
+ condition,
390
+ branches: {
391
+ then: thenSteps,
392
+ else: elseSteps,
393
+ },
394
+ };
395
+ }
396
+ /**
397
+ * Extract steps from a single statement (non-block)
398
+ */
399
+ function extractStepsFromStatement(statement, context) {
400
+ const step = extractStep(statement, context);
401
+ return step ? [step] : [];
402
+ }
403
+ /**
404
+ * Extract parallel fanout from Promise.all(array.map(...))
405
+ */
406
+ function extractParallelFanout(call, context) {
407
+ const mapCall = call.arguments[0];
408
+ if (!ts.isCallExpression(mapCall)) {
409
+ return null;
410
+ }
411
+ if (!ts.isPropertyAccessExpression(mapCall.expression)) {
412
+ return null;
413
+ }
414
+ // Extract source array
415
+ const sourceExpr = mapCall.expression.expression;
416
+ let sourceVar = null;
417
+ if (ts.isIdentifier(sourceExpr)) {
418
+ sourceVar = sourceExpr.text;
419
+ }
420
+ else if (ts.isPropertyAccessExpression(sourceExpr) &&
421
+ ts.isIdentifier(sourceExpr.expression)) {
422
+ sourceVar = sourceExpr.expression.text;
423
+ }
424
+ if (!sourceVar) {
425
+ return null;
426
+ }
427
+ // Extract map function
428
+ const mapFn = mapCall.arguments[0];
429
+ if (!ts.isArrowFunction(mapFn)) {
430
+ return null;
431
+ }
432
+ // Extract item variable
433
+ const itemParam = mapFn.parameters[0];
434
+ if (!itemParam || !ts.isIdentifier(itemParam.name)) {
435
+ return null;
436
+ }
437
+ const itemVar = itemParam.name.text;
438
+ // Extract workflow.do call from map body
439
+ let doCall = null;
440
+ if (ts.isCallExpression(mapFn.body)) {
441
+ doCall = mapFn.body;
442
+ }
443
+ else if (ts.isAwaitExpression(mapFn.body)) {
444
+ // Handle: async (email) => await workflow.do(...)
445
+ if (ts.isCallExpression(mapFn.body.expression)) {
446
+ doCall = mapFn.body.expression;
447
+ }
448
+ }
449
+ else if (ts.isBlock(mapFn.body)) {
450
+ // Look for workflow.do in block
451
+ for (const stmt of mapFn.body.statements) {
452
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
453
+ if (ts.isCallExpression(stmt.expression)) {
454
+ doCall = stmt.expression;
455
+ break;
456
+ }
457
+ else if (ts.isAwaitExpression(stmt.expression)) {
458
+ // Handle: return await workflow.do(...)
459
+ if (ts.isCallExpression(stmt.expression.expression)) {
460
+ doCall = stmt.expression.expression;
461
+ break;
462
+ }
463
+ }
464
+ }
465
+ }
466
+ }
467
+ if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
468
+ return null;
469
+ }
470
+ // Create a temporary context for the child step
471
+ const childContext = {
472
+ ...context,
473
+ outputVars: new Map(context.outputVars),
474
+ };
475
+ const childStep = extractRpcStep(doCall, childContext);
476
+ if (!childStep) {
477
+ return null;
478
+ }
479
+ return {
480
+ type: 'fanout',
481
+ stepName: childStep.stepName,
482
+ sourceVar,
483
+ itemVar,
484
+ mode: 'parallel',
485
+ child: childStep,
486
+ };
487
+ }
488
+ /**
489
+ * Extract parallel group from Promise.all([...])
490
+ */
491
+ function extractParallelGroup(call, context) {
492
+ const arrayArg = call.arguments[0];
493
+ if (!ts.isArrayLiteralExpression(arrayArg)) {
494
+ return null;
495
+ }
496
+ const children = [];
497
+ for (const elem of arrayArg.elements) {
498
+ if (ts.isCallExpression(elem) && isWorkflowDoCall(elem, context.checker)) {
499
+ const step = extractRpcStep(elem, context);
500
+ if (step) {
501
+ children.push(step);
502
+ }
503
+ }
504
+ }
505
+ if (children.length === 0) {
506
+ return null;
507
+ }
508
+ return {
509
+ type: 'parallel',
510
+ children,
511
+ };
512
+ }
513
+ /**
514
+ * Extract sequential fanout from for-of loop
515
+ */
516
+ function extractSequentialFanout(statement, context) {
517
+ if (!isSequentialFanout(statement)) {
518
+ return null;
519
+ }
520
+ const vars = extractForOfVariable(statement);
521
+ if (!vars) {
522
+ return null;
523
+ }
524
+ const { itemVar, sourceVar } = vars;
525
+ // Extract child step and optional sleep from loop body
526
+ if (!ts.isBlock(statement.statement)) {
527
+ return null;
528
+ }
529
+ let childStep = null;
530
+ let timeBetween = undefined;
531
+ for (const stmt of statement.statement.statements) {
532
+ // Look for workflow.do
533
+ if (ts.isExpressionStatement(stmt)) {
534
+ const expr = stmt.expression;
535
+ if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
536
+ const call = expr.expression;
537
+ if (isWorkflowDoCall(call, context.checker)) {
538
+ const step = extractRpcStep(call, context);
539
+ if (step) {
540
+ childStep = step;
541
+ }
542
+ }
543
+ if (isWorkflowSleepCall(call, context.checker)) {
544
+ // Extract duration for timeBetween
545
+ const args = call.arguments;
546
+ if (args.length >= 2) {
547
+ try {
548
+ const numValue = extractNumberLiteral(args[1]);
549
+ if (numValue !== null) {
550
+ timeBetween = `${numValue}ms`;
551
+ }
552
+ else {
553
+ timeBetween = extractStringLiteral(args[1], context.checker);
554
+ }
555
+ }
556
+ catch {
557
+ // Ignore extraction errors
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
563
+ // Look for if statement with sleep
564
+ if (ts.isIfStatement(stmt)) {
565
+ if (ts.isBlock(stmt.thenStatement)) {
566
+ for (const thenStmt of stmt.thenStatement.statements) {
567
+ if (ts.isExpressionStatement(thenStmt)) {
568
+ const expr = thenStmt.expression;
569
+ if (ts.isAwaitExpression(expr) &&
570
+ ts.isCallExpression(expr.expression)) {
571
+ const call = expr.expression;
572
+ if (isWorkflowSleepCall(call, context.checker)) {
573
+ const args = call.arguments;
574
+ if (args.length >= 2) {
575
+ try {
576
+ const numValue = extractNumberLiteral(args[1]);
577
+ if (numValue !== null) {
578
+ timeBetween = `${numValue}ms`;
579
+ }
580
+ else {
581
+ timeBetween = extractStringLiteral(args[1], context.checker);
582
+ }
583
+ }
584
+ catch {
585
+ // Ignore extraction errors
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ }
594
+ }
595
+ if (!childStep) {
596
+ return null;
597
+ }
598
+ return {
599
+ type: 'fanout',
600
+ stepName: childStep.stepName,
601
+ sourceVar,
602
+ itemVar,
603
+ mode: 'sequential',
604
+ child: childStep,
605
+ timeBetween,
606
+ };
607
+ }
608
+ /**
609
+ * Extract return step
610
+ */
611
+ function extractReturn(statement, context) {
612
+ if (!statement.expression) {
613
+ return null;
614
+ }
615
+ if (!ts.isObjectLiteralExpression(statement.expression)) {
616
+ return null;
617
+ }
618
+ const outputs = {};
619
+ for (const prop of statement.expression.properties) {
620
+ if (ts.isPropertyAssignment(prop) ||
621
+ ts.isShorthandPropertyAssignment(prop)) {
622
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
623
+ if (!propName) {
624
+ continue;
625
+ }
626
+ let binding = null;
627
+ if (ts.isShorthandPropertyAssignment(prop)) {
628
+ // { orgId } - must be an output variable
629
+ const varName = prop.name.text;
630
+ if (context.outputVars.has(varName)) {
631
+ binding = { from: 'outputVar', name: varName };
632
+ }
633
+ }
634
+ else if (ts.isPropertyAssignment(prop)) {
635
+ const init = prop.initializer;
636
+ // Check for property access (e.g., org.id, owner?.id)
637
+ if (ts.isPropertyAccessExpression(init)) {
638
+ const objName = ts.isIdentifier(init.expression)
639
+ ? init.expression.text
640
+ : null;
641
+ const propPath = init.name.text;
642
+ if (objName && context.outputVars.has(objName)) {
643
+ binding = { from: 'outputVar', name: objName, path: propPath };
644
+ }
645
+ }
646
+ // Check for optional chaining (e.g., owner?.id)
647
+ if (init.kind === ts.SyntaxKind.PropertyAccessExpression ||
648
+ init.kind === ts.SyntaxKind.NonNullExpression) {
649
+ const text = init.getText();
650
+ const match = text.match(/^(\w+)\??\.(\w+)$/);
651
+ if (match) {
652
+ const [, objName, propPath] = match;
653
+ if (context.outputVars.has(objName)) {
654
+ binding = { from: 'outputVar', name: objName, path: propPath };
655
+ }
656
+ }
657
+ }
658
+ // Check for identifier (simple variable reference)
659
+ if (ts.isIdentifier(init)) {
660
+ const varName = init.text;
661
+ if (context.outputVars.has(varName)) {
662
+ binding = { from: 'outputVar', name: varName };
663
+ }
664
+ }
665
+ }
666
+ if (binding) {
667
+ outputs[propName] = binding;
668
+ }
669
+ }
670
+ }
671
+ if (Object.keys(outputs).length === 0) {
672
+ return null;
673
+ }
674
+ return {
675
+ type: 'return',
676
+ outputs,
677
+ };
678
+ }
679
+ /**
680
+ * Extract input sources from an argument node
681
+ */
682
+ function extractInputSources(node, context) {
683
+ if (!ts.isObjectLiteralExpression(node)) {
684
+ return undefined;
685
+ }
686
+ const inputs = {};
687
+ for (const prop of node.properties) {
688
+ if (ts.isPropertyAssignment(prop) ||
689
+ ts.isShorthandPropertyAssignment(prop)) {
690
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
691
+ if (!propName) {
692
+ continue;
693
+ }
694
+ let source = null;
695
+ if (ts.isShorthandPropertyAssignment(prop)) {
696
+ // { email } - could be from input or output var
697
+ const varName = prop.name.text;
698
+ if (context.outputVars.has(varName)) {
699
+ source = { from: 'outputVar', name: varName };
700
+ }
701
+ else {
702
+ source = { from: 'input', path: varName };
703
+ }
704
+ }
705
+ else if (ts.isPropertyAssignment(prop)) {
706
+ source = extractInputSource(prop.initializer, context);
707
+ }
708
+ if (source) {
709
+ inputs[propName] = source;
710
+ }
711
+ }
712
+ if (ts.isSpreadAssignment(prop)) {
713
+ // Handle spread: { ...data }
714
+ if (ts.isIdentifier(prop.expression)) {
715
+ const varName = prop.expression.text;
716
+ if (varName === context.inputParamName) {
717
+ // This is spreading the input data
718
+ // We can't fully model this in v1, so we'll skip it
719
+ continue;
720
+ }
721
+ }
722
+ }
723
+ }
724
+ return Object.keys(inputs).length > 0 ? inputs : undefined;
725
+ }
726
+ /**
727
+ * Extract a single input source
728
+ */
729
+ function extractInputSource(node, context) {
730
+ // Property access: data.email, org.id
731
+ if (ts.isPropertyAccessExpression(node)) {
732
+ const objExpr = node.expression;
733
+ const propName = node.name.text;
734
+ if (ts.isIdentifier(objExpr)) {
735
+ const objName = objExpr.text;
736
+ if (objName === context.inputParamName) {
737
+ return { from: 'input', path: propName };
738
+ }
739
+ if (context.outputVars.has(objName)) {
740
+ return { from: 'outputVar', name: objName, path: propName };
741
+ }
742
+ }
743
+ }
744
+ // Identifier: email, orgId
745
+ if (ts.isIdentifier(node)) {
746
+ const varName = node.text;
747
+ if (context.outputVars.has(varName)) {
748
+ return { from: 'outputVar', name: varName };
749
+ }
750
+ // Assume it's from input
751
+ return { from: 'input', path: varName };
752
+ }
753
+ // Literal: "string", 123, true, false, null
754
+ if (ts.isStringLiteral(node) ||
755
+ ts.isNumericLiteral(node) ||
756
+ node.kind === ts.SyntaxKind.TrueKeyword ||
757
+ node.kind === ts.SyntaxKind.FalseKeyword ||
758
+ node.kind === ts.SyntaxKind.NullKeyword) {
759
+ let value;
760
+ if (ts.isStringLiteral(node)) {
761
+ value = node.text;
762
+ }
763
+ else if (ts.isNumericLiteral(node)) {
764
+ value = Number(node.text);
765
+ }
766
+ else if (node.kind === ts.SyntaxKind.TrueKeyword) {
767
+ value = true;
768
+ }
769
+ else if (node.kind === ts.SyntaxKind.FalseKeyword) {
770
+ value = false;
771
+ }
772
+ else if (node.kind === ts.SyntaxKind.NullKeyword) {
773
+ value = null;
774
+ }
775
+ return { from: 'literal', value };
776
+ }
777
+ // Object literal
778
+ if (ts.isObjectLiteralExpression(node)) {
779
+ const obj = {};
780
+ for (const prop of node.properties) {
781
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
782
+ const propName = prop.name.text;
783
+ const propSource = extractInputSource(prop.initializer, context);
784
+ if (propSource && propSource.from === 'literal') {
785
+ obj[propName] = propSource.value;
786
+ }
787
+ }
788
+ }
789
+ return { from: 'literal', value: obj };
790
+ }
791
+ // Array literal
792
+ if (ts.isArrayLiteralExpression(node)) {
793
+ const arr = [];
794
+ for (const elem of node.elements) {
795
+ const elemSource = extractInputSource(elem, context);
796
+ if (elemSource && elemSource.from === 'literal') {
797
+ arr.push(elemSource.value);
798
+ }
799
+ }
800
+ return { from: 'literal', value: arr };
801
+ }
802
+ return null;
803
+ }