@pikku/inspector 0.10.2 → 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 (82) hide show
  1. package/CHANGELOG.md +23 -0
  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 +65 -44
  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 +6 -0
  14. package/dist/add/add-workflow.js +178 -0
  15. package/dist/error-codes.d.ts +5 -1
  16. package/dist/error-codes.js +5 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +1 -0
  19. package/dist/inspector.js +13 -5
  20. package/dist/types.d.ts +27 -9
  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 +32 -0
  24. package/dist/utils/extract-node-value.js +103 -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 +21 -4
  34. package/dist/utils/serialize-inspector-state.js +18 -8
  35. package/dist/utils/type-utils.d.ts +4 -0
  36. package/dist/utils/type-utils.js +55 -0
  37. package/dist/utils/write-service-metadata.d.ts +13 -0
  38. package/dist/utils/write-service-metadata.js +37 -0
  39. package/dist/visit.js +4 -2
  40. package/dist/workflow/extract-simple-workflow.d.ts +15 -0
  41. package/dist/workflow/extract-simple-workflow.js +803 -0
  42. package/dist/workflow/patterns.d.ts +39 -0
  43. package/dist/workflow/patterns.js +138 -0
  44. package/dist/workflow/validation.d.ts +28 -0
  45. package/dist/workflow/validation.js +124 -0
  46. package/package.json +4 -4
  47. package/src/add/add-channel.ts +37 -17
  48. package/src/add/add-file-with-factory.ts +10 -10
  49. package/src/add/add-functions.ts +81 -57
  50. package/src/add/add-http-route.ts +10 -5
  51. package/src/add/add-mcp-prompt.ts +11 -7
  52. package/src/add/add-mcp-resource.ts +11 -7
  53. package/src/add/add-mcp-tool.ts +11 -7
  54. package/src/add/add-middleware.ts +1 -1
  55. package/src/add/add-permission.ts +1 -1
  56. package/src/add/add-queue-worker.ts +11 -12
  57. package/src/add/add-schedule.ts +10 -5
  58. package/src/add/add-workflow.ts +241 -0
  59. package/src/error-codes.ts +6 -0
  60. package/src/index.ts +2 -0
  61. package/src/inspector.ts +19 -5
  62. package/src/types.ts +24 -9
  63. package/src/utils/extract-function-node.ts +58 -0
  64. package/src/utils/extract-node-value.ts +132 -0
  65. package/src/utils/extract-service-metadata.ts +353 -0
  66. package/src/utils/filter-inspector-state.test.ts +3 -3
  67. package/src/utils/filter-utils.test.ts +45 -51
  68. package/src/utils/get-files-and-methods.ts +11 -11
  69. package/src/utils/get-property-value.ts +60 -53
  70. package/src/utils/permissions.test.ts +3 -3
  71. package/src/utils/post-process.ts +56 -3
  72. package/src/utils/serialize-inspector-state.ts +37 -15
  73. package/src/utils/test-data/inspector-state.json +13 -9
  74. package/src/utils/type-utils.ts +69 -0
  75. package/src/utils/write-service-metadata.ts +51 -0
  76. package/src/visit.ts +5 -3
  77. package/src/workflow/extract-simple-workflow.ts +1035 -0
  78. package/src/workflow/patterns.ts +182 -0
  79. package/src/workflow/validation.ts +153 -0
  80. package/tsconfig.tsbuildinfo +1 -1
  81. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  82. package/src/add/add-mcp-resource.ts.tmp +0 -0
@@ -0,0 +1,178 @@
1
+ import * as ts from 'typescript';
2
+ import { extractFunctionName } from '../utils/extract-function-name.js';
3
+ import { extractFunctionNode } from '../utils/extract-function-node.js';
4
+ import { ErrorCode } from '../error-codes.js';
5
+ import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
6
+ import { extractSimpleWorkflow } from '../workflow/extract-simple-workflow.js';
7
+ import { getCommonWireMetaData } from '../utils/get-property-value.js';
8
+ /**
9
+ * Scan for workflow.do(), workflow.sleep(), and workflow.cancel() calls to extract workflow steps
10
+ */
11
+ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
12
+ // Look for property access expressions: workflow.do or workflow.sleep
13
+ if (ts.isPropertyAccessExpression(node)) {
14
+ const { name } = node;
15
+ // Check if this is accessing 'do' or 'sleep' property
16
+ if (name.text === 'do' || name.text === 'sleep' || name.text === 'cancel') {
17
+ // Check if the parent is a call expression
18
+ const parent = node.parent;
19
+ if (ts.isCallExpression(parent) && parent.expression === node) {
20
+ const args = parent.arguments;
21
+ if (name.text === 'do' && args.length >= 2) {
22
+ // workflow.do(stepName, rpcName|fn, data?, options?)
23
+ const stepNameArg = args[0];
24
+ const secondArg = args[1];
25
+ const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
26
+ const stepName = extractStringLiteral(stepNameArg, checker);
27
+ const description = extractDescription(optionsArg, checker) ?? undefined;
28
+ // Determine form by checking 2nd argument type
29
+ if (isStringLike(secondArg, checker)) {
30
+ // RPC form: workflow.do(stepName, rpcName, data, options?)
31
+ const rpcName = extractStringLiteral(secondArg, checker);
32
+ steps.push({
33
+ type: 'rpc',
34
+ stepName,
35
+ rpcName,
36
+ });
37
+ state.rpc.invokedFunctions.add(rpcName);
38
+ }
39
+ else if (isFunctionLike(secondArg)) {
40
+ // Inline form: workflow.do(stepName, fn, options?)
41
+ steps.push({
42
+ type: 'inline',
43
+ stepName: stepName || '<dynamic>',
44
+ description: description || '<dynamic>',
45
+ });
46
+ }
47
+ }
48
+ else if (name.text === 'sleep' && args.length >= 2) {
49
+ // workflow.sleep(stepName, duration)
50
+ const stepNameArg = args[0];
51
+ const durationArg = args[1];
52
+ const stepName = extractStringLiteral(stepNameArg, checker);
53
+ const duration = extractDuration(durationArg, checker);
54
+ steps.push({
55
+ type: 'sleep',
56
+ stepName: stepName || '<dynamic>',
57
+ duration: duration || '<dynamic>',
58
+ });
59
+ }
60
+ else if (name.text === 'cancel') {
61
+ // workflow.cancel(reason?)
62
+ steps.push({
63
+ type: 'cancel',
64
+ });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ // Don't recurse into nested functions - only look at top-level workflow calls
70
+ ts.forEachChild(node, (child) => {
71
+ if (ts.isFunctionDeclaration(child) ||
72
+ ts.isFunctionExpression(child) ||
73
+ ts.isArrowFunction(child)) {
74
+ return;
75
+ }
76
+ getWorkflowInvocations(child, checker, state, workflowName, steps);
77
+ });
78
+ }
79
+ /**
80
+ * Inspector for pikkuWorkflow() and pikkuSimpleWorkflow() calls
81
+ * Detects workflow registration and extracts metadata
82
+ */
83
+ export const addWorkflow = (logger, node, checker, state) => {
84
+ if (!ts.isCallExpression(node)) {
85
+ return;
86
+ }
87
+ const args = node.arguments;
88
+ const firstArg = args[0];
89
+ const expression = node.expression;
90
+ if (!ts.isIdentifier(expression)) {
91
+ return;
92
+ }
93
+ let wrapperType = null;
94
+ if (expression.text === 'pikkuWorkflowFunc') {
95
+ wrapperType = 'regular';
96
+ }
97
+ else if (expression.text === 'pikkuSimpleWorkflowFunc') {
98
+ wrapperType = 'simple';
99
+ }
100
+ else {
101
+ return;
102
+ }
103
+ if (!firstArg) {
104
+ return;
105
+ }
106
+ // Extract workflow name and metadata using same logic as add-functions
107
+ const { pikkuFuncName, name, exportedName } = extractFunctionName(node, checker, state.rootDir);
108
+ const workflowName = exportedName || name;
109
+ if (!workflowName) {
110
+ logger.critical(ErrorCode.MISSING_NAME, `Could not determine workflow name from export.`);
111
+ return;
112
+ }
113
+ // Extract the function node (either direct function or from config.func)
114
+ const { funcNode, resolvedFunc } = extractFunctionNode(firstArg, checker);
115
+ // Extract metadata if using object form
116
+ let tags;
117
+ let summary;
118
+ let description;
119
+ let errors;
120
+ if (ts.isObjectLiteralExpression(firstArg)) {
121
+ const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger);
122
+ tags = metadata.tags;
123
+ summary = metadata.summary;
124
+ description = metadata.description;
125
+ errors = metadata.errors;
126
+ }
127
+ // Validate that we got a valid function
128
+ if (ts.isObjectLiteralExpression(firstArg) &&
129
+ (!funcNode || funcNode === firstArg)) {
130
+ logger.critical(ErrorCode.MISSING_FUNC, `No valid 'func' property for workflow '${workflowName}'.`);
131
+ return;
132
+ }
133
+ if (!resolvedFunc) {
134
+ logger.critical(ErrorCode.MISSING_FUNC, `Could not resolve workflow function for '${workflowName}'.`);
135
+ return;
136
+ }
137
+ // Track workflow file for wiring generation
138
+ if (exportedName) {
139
+ state.workflows.files.set(pikkuFuncName, {
140
+ path: node.getSourceFile().fileName,
141
+ exportedName,
142
+ });
143
+ }
144
+ let steps = [];
145
+ let simple = undefined;
146
+ // Try simple workflow extraction first
147
+ // Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
148
+ const result = extractSimpleWorkflow(node, checker);
149
+ if (result.status === 'ok' && result.steps) {
150
+ // Simple extraction succeeded
151
+ steps = result.steps;
152
+ simple = true;
153
+ }
154
+ else {
155
+ // Simple extraction failed
156
+ if (wrapperType === 'simple') {
157
+ // For pikkuSimpleWorkflowFunc, this is a critical error
158
+ logger.critical(ErrorCode.INVALID_SIMPLE_WORKFLOW, `Workflow '${workflowName}' uses pikkuSimpleWorkflowFunc but does not conform to simple workflow DSL:\n${result.reason || 'Unknown error'}`);
159
+ return;
160
+ }
161
+ else {
162
+ // For pikkuWorkflowFunc, fall back to basic extraction
163
+ logger.debug(`Workflow '${workflowName}' could not be extracted as simple workflow: ${result.reason || 'Unknown error'}. Falling back to basic extraction.`);
164
+ simple = false;
165
+ }
166
+ }
167
+ getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
168
+ state.workflows.meta[workflowName] = {
169
+ pikkuFuncName,
170
+ workflowName,
171
+ steps,
172
+ simple,
173
+ summary,
174
+ description,
175
+ errors,
176
+ tags,
177
+ };
178
+ };
@@ -17,6 +17,9 @@ export declare enum ErrorCode {
17
17
  MISSING_QUEUE_NAME = "PKU384",
18
18
  MISSING_CHANNEL_NAME = "PKU400",
19
19
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
20
+ DYNAMIC_STEP_NAME = "PKU529",
21
+ WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
22
+ INVALID_SIMPLE_WORKFLOW = "PKU641",
20
23
  CONFIG_TYPE_NOT_FOUND = "PKU426",
21
24
  CONFIG_TYPE_UNDEFINED = "PKU427",
22
25
  SCHEMA_NO_ROOT = "PKU431",
@@ -31,5 +34,6 @@ export declare enum ErrorCode {
31
34
  PERMISSION_HANDLER_INVALID = "PKU835",
32
35
  PERMISSION_TAG_INVALID = "PKU836",
33
36
  PERMISSION_EMPTY_ARRAY = "PKU937",
34
- PERMISSION_PATTERN_INVALID = "PKU975"
37
+ PERMISSION_PATTERN_INVALID = "PKU975",
38
+ WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901"
35
39
  }
@@ -19,6 +19,9 @@ export var ErrorCode;
19
19
  ErrorCode["MISSING_QUEUE_NAME"] = "PKU384";
20
20
  ErrorCode["MISSING_CHANNEL_NAME"] = "PKU400";
21
21
  ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
22
+ ErrorCode["DYNAMIC_STEP_NAME"] = "PKU529";
23
+ ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
24
+ ErrorCode["INVALID_SIMPLE_WORKFLOW"] = "PKU641";
22
25
  // Configuration errors
23
26
  ErrorCode["CONFIG_TYPE_NOT_FOUND"] = "PKU426";
24
27
  ErrorCode["CONFIG_TYPE_UNDEFINED"] = "PKU427";
@@ -37,4 +40,6 @@ export var ErrorCode;
37
40
  ErrorCode["PERMISSION_TAG_INVALID"] = "PKU836";
38
41
  ErrorCode["PERMISSION_EMPTY_ARRAY"] = "PKU937";
39
42
  ErrorCode["PERMISSION_PATTERN_INVALID"] = "PKU975";
43
+ // Feature Flag
44
+ ErrorCode["WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED"] = "PKU901";
40
45
  })(ErrorCode || (ErrorCode = {}));
package/dist/index.d.ts CHANGED
@@ -8,3 +8,5 @@ export { ErrorCode } from './error-codes.js';
8
8
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
9
9
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
10
10
  export { filterInspectorState } from './utils/filter-inspector-state.js';
11
+ export { writeAllServiceMetadata } from './utils/write-service-metadata.js';
12
+ export type { ServiceMetadata } from './utils/extract-service-metadata.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export { getFilesAndMethods } from './utils/get-files-and-methods.js';
3
3
  export { ErrorCode } from './error-codes.js';
4
4
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
5
5
  export { filterInspectorState } from './utils/filter-inspector-state.js';
6
+ export { writeAllServiceMetadata } from './utils/write-service-metadata.js';
package/dist/inspector.js CHANGED
@@ -4,7 +4,7 @@ import { visitSetup, visitRoutes } from './visit.js';
4
4
  import { TypesMap } from './types-map.js';
5
5
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
6
6
  import { findCommonAncestor } from './utils/find-root-dir.js';
7
- import { aggregateRequiredServices } from './utils/post-process.js';
7
+ import { aggregateRequiredServices, extractServiceInterfaceMetadata, } from './utils/post-process.js';
8
8
  /**
9
9
  * Creates an initial/empty inspector state with all required properties initialized
10
10
  * @param rootDir - The root directory for the project
@@ -14,12 +14,12 @@ export function getInitialInspectorState(rootDir) {
14
14
  return {
15
15
  rootDir,
16
16
  singletonServicesTypeImportMap: new Map(),
17
- sessionServicesTypeImportMap: new Map(),
17
+ wireServicesTypeImportMap: new Map(),
18
18
  userSessionTypeImportMap: new Map(),
19
19
  configTypeImportMap: new Map(),
20
20
  singletonServicesFactories: new Map(),
21
- sessionServicesFactories: new Map(),
22
- sessionServicesMeta: new Map(),
21
+ wireServicesFactories: new Map(),
22
+ wireServicesMeta: new Map(),
23
23
  configFactories: new Map(),
24
24
  filesAndMethods: {},
25
25
  filesAndMethodsErrors: new Map(),
@@ -56,6 +56,10 @@ export function getInitialInspectorState(rootDir) {
56
56
  meta: {},
57
57
  files: new Set(),
58
58
  },
59
+ workflows: {
60
+ meta: {},
61
+ files: new Map(),
62
+ },
59
63
  rpc: {
60
64
  internalMeta: {},
61
65
  internalFiles: new Map(),
@@ -90,8 +94,9 @@ export function getInitialInspectorState(rootDir) {
90
94
  usedMiddleware: new Set(),
91
95
  usedPermissions: new Set(),
92
96
  allSingletonServices: [],
93
- allSessionServices: [],
97
+ allWireServices: [],
94
98
  },
99
+ serviceMetadata: [],
95
100
  };
96
101
  }
97
102
  export const inspect = (logger, routeFiles, options = {}) => {
@@ -140,6 +145,9 @@ export const inspect = (logger, routeFiles, options = {}) => {
140
145
  const startAggregate = performance.now();
141
146
  aggregateRequiredServices(state);
142
147
  logger.debug(`Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`);
148
+ const startServiceMeta = performance.now();
149
+ extractServiceInterfaceMetadata(state, checker);
150
+ logger.debug(`Extract service metadata completed in ${(performance.now() - startServiceMeta).toFixed(2)}ms`);
143
151
  }
144
152
  return state;
145
153
  };
package/dist/types.d.ts CHANGED
@@ -2,7 +2,8 @@ import * as ts from 'typescript';
2
2
  import { ChannelsMeta } from '@pikku/core/channel';
3
3
  import { HTTPWiringsMeta } from '@pikku/core/http';
4
4
  import { ScheduledTasksMeta } from '@pikku/core/scheduler';
5
- import { queueWorkersMeta } from '@pikku/core/queue';
5
+ import { QueueWorkersMeta } from '@pikku/core/queue';
6
+ import { WorkflowsMeta } from '@pikku/core/workflow';
6
7
  import { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
7
8
  import { CLIMeta } from '@pikku/core/cli';
8
9
  import { TypesMap } from './types-map.js';
@@ -91,7 +92,7 @@ export type InspectorOptions = Partial<{
91
92
  configFileType: string;
92
93
  userSessionType: string;
93
94
  singletonServicesFactoryType: string;
94
- sessionServicesFactoryType: string;
95
+ wireServicesFactoryType: string;
95
96
  }>;
96
97
  }>;
97
98
  export interface InspectorLogger {
@@ -110,7 +111,7 @@ export interface InspectorFilesAndMethods {
110
111
  type: string;
111
112
  typePath: string;
112
113
  };
113
- sessionServicesType?: {
114
+ wireServicesType?: {
114
115
  file: string;
115
116
  variable: string;
116
117
  type: string;
@@ -140,7 +141,7 @@ export interface InspectorFilesAndMethods {
140
141
  type: string;
141
142
  typePath: string;
142
143
  };
143
- sessionServicesFactory?: {
144
+ wireServicesFactory?: {
144
145
  file: string;
145
146
  variable: string;
146
147
  type: string;
@@ -150,12 +151,12 @@ export interface InspectorFilesAndMethods {
150
151
  export interface InspectorState {
151
152
  rootDir: string;
152
153
  singletonServicesTypeImportMap: PathToNameAndType;
153
- sessionServicesTypeImportMap: PathToNameAndType;
154
+ wireServicesTypeImportMap: PathToNameAndType;
154
155
  userSessionTypeImportMap: PathToNameAndType;
155
156
  configTypeImportMap: PathToNameAndType;
156
157
  singletonServicesFactories: PathToNameAndType;
157
- sessionServicesFactories: PathToNameAndType;
158
- sessionServicesMeta: Map<string, string[]>;
158
+ wireServicesFactories: PathToNameAndType;
159
+ wireServicesMeta: Map<string, string[]>;
159
160
  configFactories: PathToNameAndType;
160
161
  filesAndMethods: InspectorFilesAndMethods;
161
162
  filesAndMethodsErrors: Map<string, PathToNameAndType>;
@@ -168,9 +169,16 @@ export interface InspectorState {
168
169
  files: Set<string>;
169
170
  };
170
171
  queueWorkers: {
171
- meta: queueWorkersMeta;
172
+ meta: QueueWorkersMeta;
172
173
  files: Set<string>;
173
174
  };
175
+ workflows: {
176
+ meta: WorkflowsMeta;
177
+ files: Map<string, {
178
+ path: string;
179
+ exportedName: string;
180
+ }>;
181
+ };
174
182
  rpc: {
175
183
  internalMeta: Record<string, string>;
176
184
  internalFiles: Map<string, {
@@ -202,6 +210,16 @@ export interface InspectorState {
202
210
  usedMiddleware: Set<string>;
203
211
  usedPermissions: Set<string>;
204
212
  allSingletonServices: string[];
205
- allSessionServices: string[];
213
+ allWireServices: string[];
206
214
  };
215
+ serviceMetadata: Array<{
216
+ name: string;
217
+ summary: string;
218
+ description: string;
219
+ package: string;
220
+ path: string;
221
+ version: string;
222
+ interface: string;
223
+ expandedProperties: Record<string, string>;
224
+ }>;
207
225
  }
@@ -0,0 +1,10 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Extracts the actual function node from a pikkuFunc/pikkuWorkflowFunc call
4
+ * Handles both direct function form and config object form { func: ... }
5
+ */
6
+ export declare function extractFunctionNode(firstArg: ts.Expression, checker: ts.TypeChecker): {
7
+ funcNode: ts.Node;
8
+ resolvedFunc: ts.Node | null;
9
+ isDirectFunction: boolean;
10
+ };
@@ -0,0 +1,38 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyAssignmentInitializer, resolveFunctionDeclaration, } from './type-utils.js';
3
+ /**
4
+ * Extracts the actual function node from a pikkuFunc/pikkuWorkflowFunc call
5
+ * Handles both direct function form and config object form { func: ... }
6
+ */
7
+ export function extractFunctionNode(firstArg, checker) {
8
+ let funcNode = firstArg;
9
+ let isDirectFunction = true;
10
+ // Check if first argument is a config object with 'func' property
11
+ if (ts.isObjectLiteralExpression(firstArg)) {
12
+ isDirectFunction = false;
13
+ const funcInitializer = getPropertyAssignmentInitializer(firstArg, 'func', true, checker);
14
+ if (funcInitializer) {
15
+ funcNode = funcInitializer;
16
+ }
17
+ else {
18
+ // Return the original node if no func property found
19
+ // Caller should handle validation
20
+ funcNode = firstArg;
21
+ }
22
+ }
23
+ // Resolve identifier to get the actual function node
24
+ if (ts.isIdentifier(funcNode)) {
25
+ const symbol = checker.getSymbolAtLocation(funcNode);
26
+ const decl = symbol?.valueDeclaration || symbol?.declarations?.[0];
27
+ if (decl && ts.isVariableDeclaration(decl) && decl.initializer) {
28
+ funcNode = decl.initializer;
29
+ }
30
+ }
31
+ // Resolve function declaration for deeper analysis
32
+ const resolvedFunc = resolveFunctionDeclaration(funcNode, checker);
33
+ return {
34
+ funcNode,
35
+ resolvedFunc,
36
+ isDirectFunction,
37
+ };
38
+ }
@@ -0,0 +1,32 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Extract string literal value from a TypeScript node.
4
+ * Handles string literals, template literals (including placeholders),
5
+ * and constant variable references.
6
+ */
7
+ export declare function extractStringLiteral(node: ts.Node, checker: ts.TypeChecker): string;
8
+ /**
9
+ * Check if node is string-like (string literal or template expression)
10
+ */
11
+ export declare function isStringLike(node: ts.Node, _checker: ts.TypeChecker): boolean;
12
+ /**
13
+ * Check if node is function-like (arrow, function expression, or function declaration)
14
+ */
15
+ export declare function isFunctionLike(node: ts.Node): boolean;
16
+ /**
17
+ * Extract number literal value from a node
18
+ */
19
+ export declare function extractNumberLiteral(node: ts.Node): number | null;
20
+ /**
21
+ * Extract a property value from an object literal expression
22
+ * Returns the extracted value or null if not found/cannot extract
23
+ */
24
+ export declare function extractPropertyString(objNode: ts.ObjectLiteralExpression, propertyName: string, checker: ts.TypeChecker): string | null;
25
+ /**
26
+ * Extract description from options object
27
+ */
28
+ export declare function extractDescription(optionsNode: ts.Node | undefined, checker: ts.TypeChecker): string | null;
29
+ /**
30
+ * Extract duration value (number or string)
31
+ */
32
+ export declare function extractDuration(node: ts.Node, checker: ts.TypeChecker): string | number | null;
@@ -0,0 +1,103 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Extract string literal value from a TypeScript node.
4
+ * Handles string literals, template literals (including placeholders),
5
+ * and constant variable references.
6
+ */
7
+ export function extractStringLiteral(node, checker) {
8
+ if (ts.isStringLiteral(node)) {
9
+ return node.text;
10
+ }
11
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
12
+ return node.text;
13
+ }
14
+ if (ts.isTemplateExpression(node)) {
15
+ // reconstruct: `head + ${expr} + middle + ${expr} + tail`
16
+ let result = node.head.text;
17
+ for (const span of node.templateSpans) {
18
+ const exprText = span.expression.getText();
19
+ result += '${' + exprText + '}' + span.literal.text;
20
+ }
21
+ return result;
22
+ }
23
+ // Try to evaluate constant identifiers
24
+ if (ts.isIdentifier(node)) {
25
+ const symbol = checker.getSymbolAtLocation(node);
26
+ if (symbol?.valueDeclaration &&
27
+ ts.isVariableDeclaration(symbol.valueDeclaration)) {
28
+ const init = symbol.valueDeclaration.initializer;
29
+ if (init) {
30
+ return extractStringLiteral(init, checker);
31
+ }
32
+ }
33
+ }
34
+ throw new Error('Unable to extract string literal from node');
35
+ }
36
+ /**
37
+ * Check if node is string-like (string literal or template expression)
38
+ */
39
+ export function isStringLike(node, _checker) {
40
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
41
+ return true;
42
+ }
43
+ // Check if it's a template string with substitutions
44
+ if (ts.isTemplateExpression(node)) {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ /**
50
+ * Check if node is function-like (arrow, function expression, or function declaration)
51
+ */
52
+ export function isFunctionLike(node) {
53
+ return (ts.isArrowFunction(node) ||
54
+ ts.isFunctionExpression(node) ||
55
+ ts.isFunctionDeclaration(node));
56
+ }
57
+ /**
58
+ * Extract number literal value from a node
59
+ */
60
+ export function extractNumberLiteral(node) {
61
+ if (ts.isNumericLiteral(node)) {
62
+ return Number(node.text);
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Extract a property value from an object literal expression
68
+ * Returns the extracted value or null if not found/cannot extract
69
+ */
70
+ export function extractPropertyString(objNode, propertyName, checker) {
71
+ for (const prop of objNode.properties) {
72
+ if (ts.isPropertyAssignment(prop) &&
73
+ ts.isIdentifier(prop.name) &&
74
+ prop.name.text === propertyName) {
75
+ return extractStringLiteral(prop.initializer, checker);
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Extract description from options object
82
+ */
83
+ export function extractDescription(optionsNode, checker) {
84
+ if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
85
+ return null;
86
+ }
87
+ return extractPropertyString(optionsNode, 'description', checker);
88
+ }
89
+ /**
90
+ * Extract duration value (number or string)
91
+ */
92
+ export function extractDuration(node, checker) {
93
+ const numValue = extractNumberLiteral(node);
94
+ if (numValue !== null) {
95
+ return numValue;
96
+ }
97
+ try {
98
+ return extractStringLiteral(node, checker);
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
@@ -0,0 +1,19 @@
1
+ import * as ts from 'typescript';
2
+ export interface ServiceMetadata {
3
+ name: string;
4
+ summary: string;
5
+ description: string;
6
+ package: string;
7
+ path: string;
8
+ version: string;
9
+ interface: string;
10
+ expandedProperties: Record<string, string>;
11
+ }
12
+ /**
13
+ * Extract metadata for a service from its TypeScript declaration
14
+ */
15
+ export declare function extractServiceMetadata(serviceName: string, type: ts.Type, checker: ts.TypeChecker, rootDir: string): ServiceMetadata | null;
16
+ /**
17
+ * Extract metadata for all services in a type
18
+ */
19
+ export declare function extractAllServiceMetadata(servicesType: ts.Type, checker: ts.TypeChecker, rootDir: string): ServiceMetadata[];