@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,244 @@
1
+ import * as ts from 'typescript';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ /**
5
+ * Extract JSDoc comment information from a TypeScript node
6
+ */
7
+ function extractJSDoc(node) {
8
+ const jsDocTags = ts.getJSDocTags(node);
9
+ const jsDocComments = ts.getJSDocCommentsAndTags(node);
10
+ let summary = '';
11
+ let description = '';
12
+ const summaryTag = jsDocTags.find((tag) => tag.tagName.text === 'summary');
13
+ if (summaryTag && summaryTag.comment) {
14
+ summary =
15
+ typeof summaryTag.comment === 'string'
16
+ ? summaryTag.comment
17
+ : summaryTag.comment.map((c) => c.text).join('');
18
+ }
19
+ for (const comment of jsDocComments) {
20
+ if (ts.isJSDoc(comment) && comment.comment) {
21
+ const commentText = typeof comment.comment === 'string'
22
+ ? comment.comment
23
+ : comment.comment.map((c) => c.text).join('');
24
+ if (!summary && commentText) {
25
+ const lines = commentText
26
+ .split('\n')
27
+ .map((l) => l.trim())
28
+ .filter(Boolean);
29
+ if (lines.length > 0) {
30
+ summary = lines[0];
31
+ if (lines.length > 1) {
32
+ description = lines.slice(1).join('\n').trim();
33
+ }
34
+ }
35
+ }
36
+ else {
37
+ description = commentText;
38
+ }
39
+ break;
40
+ }
41
+ }
42
+ return { summary, description };
43
+ }
44
+ /**
45
+ * Serialize a TypeScript type to a string representation
46
+ */
47
+ function serializeTypeToString(node, sourceFile, checker) {
48
+ const nodeSourceFile = node.getSourceFile();
49
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
50
+ return node.getText(nodeSourceFile);
51
+ }
52
+ if (ts.isClassDeclaration(node)) {
53
+ return serializePublicClassMembers(node, nodeSourceFile, checker);
54
+ }
55
+ const type = checker.getTypeAtLocation(node);
56
+ return checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation);
57
+ }
58
+ /**
59
+ * Extract public members from a class and serialize them
60
+ */
61
+ function serializePublicClassMembers(classNode, sourceFile, checker) {
62
+ const className = classNode.name?.text || 'UnnamedClass';
63
+ const publicMembers = [];
64
+ for (const member of classNode.members) {
65
+ const modifiers = ts.canHaveModifiers(member)
66
+ ? ts.getModifiers(member)
67
+ : undefined;
68
+ const isPublic = !modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword ||
69
+ mod.kind === ts.SyntaxKind.ProtectedKeyword);
70
+ if (!isPublic)
71
+ continue;
72
+ if (ts.isMethodDeclaration(member) ||
73
+ ts.isPropertyDeclaration(member) ||
74
+ ts.isConstructorDeclaration(member)) {
75
+ const memberSignature = getMemberSignature(member, sourceFile, checker);
76
+ if (memberSignature) {
77
+ publicMembers.push(memberSignature);
78
+ }
79
+ }
80
+ }
81
+ return `class ${className} {\n ${publicMembers.join('\n ')}\n}`;
82
+ }
83
+ /**
84
+ * Extract a clean signature for a class member (without implementation)
85
+ */
86
+ function getMemberSignature(member, sourceFile, checker) {
87
+ if (ts.isPropertyDeclaration(member)) {
88
+ const name = member.name.getText(sourceFile);
89
+ const type = member.type ? member.type.getText(sourceFile) : 'any';
90
+ const optional = member.questionToken ? '?' : '';
91
+ return `${name}${optional}: ${type};`;
92
+ }
93
+ if (ts.isMethodDeclaration(member)) {
94
+ const name = member.name.getText(sourceFile);
95
+ const typeParams = member.typeParameters
96
+ ? `<${member.typeParameters.map((tp) => tp.getText(sourceFile)).join(', ')}>`
97
+ : '';
98
+ const params = member.parameters
99
+ .map((p) => p.getText(sourceFile))
100
+ .join(', ');
101
+ const returnType = member.type ? member.type.getText(sourceFile) : 'void';
102
+ const optional = member.questionToken ? '?' : '';
103
+ return `${name}${optional}${typeParams}(${params}): ${returnType};`;
104
+ }
105
+ if (ts.isConstructorDeclaration(member)) {
106
+ const params = member.parameters
107
+ .map((p) => p.getText(sourceFile))
108
+ .join(', ');
109
+ return `constructor(${params});`;
110
+ }
111
+ return null;
112
+ }
113
+ /**
114
+ * Find the nearest package.json and extract package name and version
115
+ */
116
+ function getPackageInfo(filePath) {
117
+ let currentDir = path.dirname(filePath);
118
+ const root = path.parse(currentDir).root;
119
+ while (currentDir !== root) {
120
+ const packageJsonPath = path.join(currentDir, 'package.json');
121
+ if (fs.existsSync(packageJsonPath)) {
122
+ try {
123
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
124
+ return {
125
+ packageName: packageJson.name || 'unknown',
126
+ version: packageJson.version || '0.0.0',
127
+ };
128
+ }
129
+ catch (err) {
130
+ // If we can't parse the package.json, continue searching
131
+ }
132
+ }
133
+ currentDir = path.dirname(currentDir);
134
+ }
135
+ return { packageName: 'unknown', version: '0.0.0' };
136
+ }
137
+ /**
138
+ * Expand a type to show all its properties including inherited ones
139
+ * Returns a Record mapping property names to their type strings
140
+ */
141
+ function expandInterfaceProperties(type, checker, maxDepth = 2, currentDepth = 0, visited = new Set()) {
142
+ const result = {};
143
+ if (visited.has(type) || currentDepth >= maxDepth) {
144
+ return result;
145
+ }
146
+ visited.add(type);
147
+ const properties = type.getProperties();
148
+ for (const prop of properties) {
149
+ const propName = prop.getName();
150
+ const propDecl = prop.valueDeclaration || prop.declarations?.[0];
151
+ if (!propDecl)
152
+ continue;
153
+ try {
154
+ const propType = checker.getTypeOfSymbolAtLocation(prop, propDecl);
155
+ const isOptional = !!(prop.flags & ts.SymbolFlags.Optional);
156
+ let typeString = checker.typeToString(propType, propDecl, ts.TypeFormatFlags.NoTruncation |
157
+ ts.TypeFormatFlags.UseFullyQualifiedType);
158
+ if (isOptional && !typeString.includes('undefined')) {
159
+ typeString = `${typeString} | undefined`;
160
+ }
161
+ result[propName] = typeString;
162
+ }
163
+ catch (err) {
164
+ result[propName] = 'any';
165
+ }
166
+ }
167
+ return result;
168
+ }
169
+ /**
170
+ * Extract metadata for a service from its TypeScript declaration
171
+ */
172
+ export function extractServiceMetadata(serviceName, type, checker, rootDir) {
173
+ const property = type.getProperty(serviceName);
174
+ if (!property) {
175
+ return null;
176
+ }
177
+ const declaration = property.valueDeclaration || property.declarations?.[0];
178
+ if (!declaration) {
179
+ return null;
180
+ }
181
+ const sourceFile = declaration.getSourceFile();
182
+ const filePath = sourceFile.fileName;
183
+ const serviceType = checker.getTypeOfSymbolAtLocation(property, declaration);
184
+ let typeDeclaration = null;
185
+ if (serviceType.symbol) {
186
+ const typeDecl = serviceType.symbol.valueDeclaration ||
187
+ serviceType.symbol.declarations?.[0];
188
+ if (typeDecl &&
189
+ (ts.isInterfaceDeclaration(typeDecl) ||
190
+ ts.isClassDeclaration(typeDecl) ||
191
+ ts.isTypeAliasDeclaration(typeDecl))) {
192
+ typeDeclaration = typeDecl;
193
+ }
194
+ }
195
+ let summary = '';
196
+ let description = '';
197
+ if (typeDeclaration) {
198
+ const jsDoc = extractJSDoc(typeDeclaration);
199
+ summary = jsDoc.summary;
200
+ description = jsDoc.description;
201
+ }
202
+ else if (ts.isPropertySignature(declaration) ||
203
+ ts.isPropertyDeclaration(declaration)) {
204
+ const jsDoc = extractJSDoc(declaration);
205
+ summary = jsDoc.summary;
206
+ description = jsDoc.description;
207
+ }
208
+ let interfaceString = '';
209
+ if (typeDeclaration) {
210
+ interfaceString = serializeTypeToString(typeDeclaration, sourceFile, checker);
211
+ }
212
+ else {
213
+ interfaceString = checker.typeToString(serviceType, declaration, ts.TypeFormatFlags.NoTruncation);
214
+ }
215
+ const { packageName, version } = getPackageInfo(filePath);
216
+ const relativePath = path.relative(rootDir, filePath);
217
+ const expandedProperties = expandInterfaceProperties(serviceType, checker);
218
+ return {
219
+ name: serviceName,
220
+ summary,
221
+ description,
222
+ package: packageName,
223
+ path: relativePath,
224
+ version,
225
+ interface: interfaceString,
226
+ expandedProperties,
227
+ };
228
+ }
229
+ /**
230
+ * Extract metadata for all services in a type
231
+ */
232
+ export function extractAllServiceMetadata(servicesType, checker, rootDir) {
233
+ const metadata = [];
234
+ const serviceNames = servicesType
235
+ .getProperties()
236
+ .map((prop) => prop.getName());
237
+ for (const serviceName of serviceNames) {
238
+ const serviceMeta = extractServiceMetadata(serviceName, servicesType, checker, rootDir);
239
+ if (serviceMeta) {
240
+ metadata.push(serviceMeta);
241
+ }
242
+ }
243
+ return metadata;
244
+ }
@@ -7,15 +7,15 @@ interface Meta {
7
7
  }
8
8
  export type FilesAndMethods = {
9
9
  userSessionType: Meta;
10
- sessionServicesType: Meta;
10
+ wireServicesType: Meta;
11
11
  singletonServicesType: Meta;
12
12
  pikkuConfigType: Meta;
13
13
  pikkuConfigFactory: Meta;
14
14
  singletonServicesFactory: Meta;
15
- sessionServicesFactory: Meta;
15
+ wireServicesFactory: Meta;
16
16
  };
17
17
  export type FilesAndMethodsErrors = Map<string, PathToNameAndType>;
18
- export declare const getFilesAndMethods: ({ singletonServicesTypeImportMap, sessionServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }: InspectorState, { configFileType, userSessionType, singletonServicesFactoryType, sessionServicesFactoryType, }?: InspectorOptions["types"]) => {
18
+ export declare const getFilesAndMethods: ({ singletonServicesTypeImportMap, wireServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, wireServicesFactories, singletonServicesFactories, configFactories, }: InspectorState, { configFileType, userSessionType, singletonServicesFactoryType, wireServicesFactoryType, }?: InspectorOptions["types"]) => {
19
19
  result: Partial<FilesAndMethods>;
20
20
  errors: FilesAndMethodsErrors;
21
21
  };
@@ -46,16 +46,16 @@ const getMetaTypes = (type, map, desiredType, errors) => {
46
46
  }
47
47
  return;
48
48
  };
49
- export const getFilesAndMethods = ({ singletonServicesTypeImportMap, sessionServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, sessionServicesFactories, singletonServicesFactories, configFactories, }, { configFileType, userSessionType, singletonServicesFactoryType, sessionServicesFactoryType, } = {}) => {
49
+ export const getFilesAndMethods = ({ singletonServicesTypeImportMap, wireServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, wireServicesFactories, singletonServicesFactories, configFactories, }, { configFileType, userSessionType, singletonServicesFactoryType, wireServicesFactoryType, } = {}) => {
50
50
  const errors = new Map();
51
51
  const result = {
52
52
  userSessionType: getMetaTypes('CoreUserSession', userSessionTypeImportMap, userSessionType, errors),
53
53
  singletonServicesType: getMetaTypes('CoreSingletonServices', singletonServicesTypeImportMap, undefined, errors),
54
- sessionServicesType: getMetaTypes('CoreServices', sessionServicesTypeImportMap, undefined, errors),
54
+ wireServicesType: getMetaTypes('CoreServices', wireServicesTypeImportMap, undefined, errors),
55
55
  pikkuConfigType: getMetaTypes('CoreConfig', configTypeImportMap, undefined, errors),
56
56
  pikkuConfigFactory: getMetaTypes('CoreConfig', configFactories, configFileType, errors),
57
57
  singletonServicesFactory: getMetaTypes('CreateSingletonServices', singletonServicesFactories, singletonServicesFactoryType, errors),
58
- sessionServicesFactory: getMetaTypes('CreateSessionServices', sessionServicesFactories, sessionServicesFactoryType, errors),
58
+ wireServicesFactory: getMetaTypes('CreateWireServices', wireServicesFactories, wireServicesFactoryType, errors),
59
59
  };
60
60
  return { result, errors };
61
61
  };
@@ -1,12 +1,19 @@
1
- import { PikkuDocs } from '@pikku/core';
2
1
  import * as ts from 'typescript';
3
2
  import { ErrorCode } from '../error-codes.js';
4
- export declare const getPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string | string[] | null | PikkuDocs | boolean;
3
+ export declare const getPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string | string[] | null | boolean;
5
4
  /**
6
- * Gets the 'tags' property from an object and validates it's an array.
7
- * Logs a critical error if tags is not an array but still returns the value.
5
+ * Extracts common wire metadata (tags, summary, description, errors) directly from an object
6
+ * @param obj - The TypeScript object literal expression to extract metadata from
7
+ * @param wiringType - The type of wiring (e.g., 'HTTP route', 'Channel', 'Queue worker')
8
+ * @param wiringName - The name/identifier of the wiring (e.g., route path, channel name)
8
9
  * @param logger - Optional logger instance; if not provided, uses console.error
10
+ * @returns Object containing the common wire metadata fields
9
11
  */
10
- export declare const getPropertyTags: (obj: ts.ObjectLiteralExpression, wiringType: string, wiringName: string | null, logger?: {
12
+ export declare const getCommonWireMetaData: (obj: ts.ObjectLiteralExpression, wiringType: string, wiringName: string | null, logger?: {
11
13
  critical: (code: ErrorCode, message: string) => void;
12
- }) => string[] | undefined;
14
+ }) => {
15
+ tags?: string[];
16
+ summary?: string;
17
+ description?: string;
18
+ errors?: string[];
19
+ };
@@ -19,35 +19,6 @@ export const getPropertyValue = (obj, propertyName) => {
19
19
  .filter((item) => item !== null); // Filter non-null and assert type
20
20
  return stringArray.length > 0 ? stringArray : null;
21
21
  }
22
- // Special handling for 'docs' -> expect RouteDocs
23
- if (propertyName === 'docs' && ts.isObjectLiteralExpression(initializer)) {
24
- const docs = {};
25
- initializer.properties.forEach((prop) => {
26
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
27
- const propName = prop.name.text;
28
- if (propName === 'summary' && ts.isStringLiteral(prop.initializer)) {
29
- docs.summary = prop.initializer.text;
30
- }
31
- else if (propName === 'description' &&
32
- ts.isStringLiteral(prop.initializer)) {
33
- docs.description = prop.initializer.text;
34
- }
35
- else if (propName === 'tags' &&
36
- ts.isArrayLiteralExpression(prop.initializer)) {
37
- docs.tags = prop.initializer.elements
38
- .filter(ts.isStringLiteral)
39
- .map((element) => element.text);
40
- }
41
- else if (propName === 'errors' &&
42
- ts.isArrayLiteralExpression(prop.initializer)) {
43
- docs.errors = prop.initializer.elements
44
- .filter(ts.isIdentifier)
45
- .map((element) => element.text);
46
- }
47
- }
48
- });
49
- return docs;
50
- }
51
22
  // booleans -> true/false
52
23
  if (initializer.kind === ts.SyntaxKind.TrueKeyword) {
53
24
  return true;
@@ -66,21 +37,58 @@ export const getPropertyValue = (obj, propertyName) => {
66
37
  return null;
67
38
  };
68
39
  /**
69
- * Gets the 'tags' property from an object and validates it's an array.
70
- * Logs a critical error if tags is not an array but still returns the value.
40
+ * Extracts common wire metadata (tags, summary, description, errors) directly from an object
41
+ * @param obj - The TypeScript object literal expression to extract metadata from
42
+ * @param wiringType - The type of wiring (e.g., 'HTTP route', 'Channel', 'Queue worker')
43
+ * @param wiringName - The name/identifier of the wiring (e.g., route path, channel name)
71
44
  * @param logger - Optional logger instance; if not provided, uses console.error
45
+ * @returns Object containing the common wire metadata fields
72
46
  */
73
- export const getPropertyTags = (obj, wiringType, wiringName, logger) => {
74
- const tagsValue = getPropertyValue(obj, 'tags');
75
- if (tagsValue !== null && !Array.isArray(tagsValue)) {
76
- const errorMsg = `${wiringType} '${wiringName}' has invalid 'tags' property - must be an array of strings.`;
77
- if (logger) {
78
- logger.critical(ErrorCode.INVALID_TAGS_TYPE, errorMsg);
79
- }
80
- else {
81
- console.error(errorMsg);
47
+ export const getCommonWireMetaData = (obj, wiringType, wiringName, logger) => {
48
+ const metadata = {};
49
+ obj.properties.forEach((prop) => {
50
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
51
+ const propName = prop.name.text;
52
+ if (propName === 'summary' && ts.isStringLiteral(prop.initializer)) {
53
+ metadata.summary = prop.initializer.text;
54
+ }
55
+ else if (propName === 'description' &&
56
+ ts.isStringLiteral(prop.initializer)) {
57
+ metadata.description = prop.initializer.text;
58
+ }
59
+ else if (propName === 'tags') {
60
+ if (ts.isArrayLiteralExpression(prop.initializer)) {
61
+ metadata.tags = prop.initializer.elements
62
+ .filter(ts.isStringLiteral)
63
+ .map((element) => element.text);
64
+ }
65
+ else {
66
+ const errorMsg = `${wiringType} '${wiringName}' has invalid 'tags' property - must be an array of strings.`;
67
+ if (logger) {
68
+ logger.critical(ErrorCode.INVALID_TAGS_TYPE, errorMsg);
69
+ }
70
+ else {
71
+ console.error(errorMsg);
72
+ }
73
+ }
74
+ }
75
+ else if (propName === 'errors') {
76
+ if (ts.isArrayLiteralExpression(prop.initializer)) {
77
+ metadata.errors = prop.initializer.elements
78
+ .filter(ts.isIdentifier)
79
+ .map((element) => element.text);
80
+ }
81
+ else {
82
+ const errorMsg = `${wiringType} '${wiringName}' has invalid 'errors' property - must be an array of error identifiers.`;
83
+ if (logger) {
84
+ logger.critical(ErrorCode.INVALID_TAGS_TYPE, errorMsg);
85
+ }
86
+ else {
87
+ console.error(errorMsg);
88
+ }
89
+ }
90
+ }
82
91
  }
83
- // Return undefined but don't stop processing - error will be caught by the exit handler
84
- }
85
- return Array.isArray(tagsValue) ? tagsValue : undefined;
92
+ });
93
+ return metadata;
86
94
  };
@@ -1,3 +1,4 @@
1
+ import * as ts from 'typescript';
1
2
  import { InspectorState } from '../types.js';
2
3
  import { MiddlewareMetadata, PermissionMetadata } from '@pikku/core';
3
4
  /**
@@ -14,3 +15,11 @@ export declare function extractWireNames(list?: MiddlewareMetadata[] | Permissio
14
15
  * in the add-* methods during AST traversal for efficiency.
15
16
  */
16
17
  export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
18
+ /**
19
+ * Extract service interface metadata for all user-defined services.
20
+ * This extracts metadata for services in SingletonServices and Services types
21
+ * to generate documentation for AI consumption.
22
+ *
23
+ * Must be called after aggregateRequiredServices() to ensure types are loaded.
24
+ */
25
+ export declare function extractServiceInterfaceMetadata(state: InspectorState | Omit<InspectorState, 'typesLookup'>, checker: ts.TypeChecker): void;
@@ -1,4 +1,5 @@
1
1
  import { extractTypeKeys } from './type-utils.js';
2
+ import { extractAllServiceMetadata, } from './extract-service-metadata.js';
2
3
  /**
3
4
  * Helper to extract wire-level middleware/permission names from metadata.
4
5
  * Only extracts type:'wire' variants (individual middleware/permissions).
@@ -60,9 +61,9 @@ function extractAllServices(state) {
60
61
  const servicesTypes = state.typesLookup.get('Services');
61
62
  if (servicesTypes && servicesTypes.length > 0) {
62
63
  const allServiceNames = extractTypeKeys(servicesTypes[0]);
63
- // Session services are those in Services but not in SingletonServices
64
+ // Wire services are those in Services but not in SingletonServices
64
65
  const singletonSet = new Set(state.serviceAggregation.allSingletonServices);
65
- state.serviceAggregation.allSessionServices = allServiceNames
66
+ state.serviceAggregation.allWireServices = allServiceNames
66
67
  .filter((name) => !singletonSet.has(name))
67
68
  .sort();
68
69
  }
@@ -152,7 +153,7 @@ export function aggregateRequiredServices(state) {
152
153
  expandAndAddGroupServices(resourceMeta.permissions, state, addServices, false);
153
154
  }
154
155
  // 5. Services from session service factories
155
- for (const singletonServices of state.sessionServicesMeta.values()) {
156
+ for (const singletonServices of state.wireServicesMeta.values()) {
156
157
  singletonServices.forEach((service) => {
157
158
  if (!internalServices.has(service)) {
158
159
  requiredServices.add(service);
@@ -160,3 +161,29 @@ export function aggregateRequiredServices(state) {
160
161
  });
161
162
  }
162
163
  }
164
+ /**
165
+ * Extract service interface metadata for all user-defined services.
166
+ * This extracts metadata for services in SingletonServices and Services types
167
+ * to generate documentation for AI consumption.
168
+ *
169
+ * Must be called after aggregateRequiredServices() to ensure types are loaded.
170
+ */
171
+ export function extractServiceInterfaceMetadata(state, checker) {
172
+ if (!('typesLookup' in state)) {
173
+ return;
174
+ }
175
+ const allMetadata = [];
176
+ const singletonServicesTypes = state.typesLookup.get('SingletonServices');
177
+ if (singletonServicesTypes && singletonServicesTypes.length > 0) {
178
+ const singletonMeta = extractAllServiceMetadata(singletonServicesTypes[0], checker, state.rootDir);
179
+ allMetadata.push(...singletonMeta);
180
+ }
181
+ const servicesTypes = state.typesLookup.get('Services');
182
+ if (servicesTypes && servicesTypes.length > 0) {
183
+ const wireServicesMeta = extractAllServiceMetadata(servicesTypes[0], checker, state.rootDir);
184
+ const singletonNames = new Set(state.serviceAggregation.allSingletonServices);
185
+ const uniqueWireServices = wireServicesMeta.filter((meta) => !singletonNames.has(meta.name));
186
+ allMetadata.push(...uniqueWireServices);
187
+ }
188
+ state.serviceMetadata = allMetadata;
189
+ }
@@ -13,7 +13,7 @@ export interface SerializableInspectorState {
13
13
  typePath: string | null;
14
14
  }[]
15
15
  ]>;
16
- sessionServicesTypeImportMap: Array<[
16
+ wireServicesTypeImportMap: Array<[
17
17
  string,
18
18
  {
19
19
  variable: string;
@@ -45,7 +45,7 @@ export interface SerializableInspectorState {
45
45
  typePath: string | null;
46
46
  }[]
47
47
  ]>;
48
- sessionServicesFactories: Array<[
48
+ wireServicesFactories: Array<[
49
49
  string,
50
50
  {
51
51
  variable: string;
@@ -53,7 +53,7 @@ export interface SerializableInspectorState {
53
53
  typePath: string | null;
54
54
  }[]
55
55
  ]>;
56
- sessionServicesMeta: Array<[string, string[]]>;
56
+ wireServicesMeta: Array<[string, string[]]>;
57
57
  configFactories: Array<[
58
58
  string,
59
59
  {
@@ -123,6 +123,13 @@ export interface SerializableInspectorState {
123
123
  meta: InspectorState['queueWorkers']['meta'];
124
124
  files: string[];
125
125
  };
126
+ workflows: {
127
+ meta: InspectorState['workflows']['meta'];
128
+ files: Array<[string, {
129
+ path: string;
130
+ exportedName: string;
131
+ }]>;
132
+ };
126
133
  rpc: {
127
134
  internalMeta: InspectorState['rpc']['internalMeta'];
128
135
  internalFiles: Array<[string, {
@@ -166,8 +173,18 @@ export interface SerializableInspectorState {
166
173
  usedMiddleware: string[];
167
174
  usedPermissions: string[];
168
175
  allSingletonServices: string[];
169
- allSessionServices: string[];
176
+ allWireServices: string[];
170
177
  };
178
+ serviceMetadata: Array<{
179
+ name: string;
180
+ summary: string;
181
+ description: string;
182
+ package: string;
183
+ path: string;
184
+ version: string;
185
+ interface: string;
186
+ expandedProperties: Record<string, string>;
187
+ }>;
171
188
  }
172
189
  /**
173
190
  * Serializes InspectorState to a JSON-compatible format
@@ -17,12 +17,12 @@ export function serializeInspectorState(state) {
17
17
  return {
18
18
  rootDir: state.rootDir,
19
19
  singletonServicesTypeImportMap: Array.from(state.singletonServicesTypeImportMap.entries()),
20
- sessionServicesTypeImportMap: Array.from(state.sessionServicesTypeImportMap.entries()),
20
+ wireServicesTypeImportMap: Array.from(state.wireServicesTypeImportMap.entries()),
21
21
  userSessionTypeImportMap: Array.from(state.userSessionTypeImportMap.entries()),
22
22
  configTypeImportMap: Array.from(state.configTypeImportMap.entries()),
23
23
  singletonServicesFactories: Array.from(state.singletonServicesFactories.entries()),
24
- sessionServicesFactories: Array.from(state.sessionServicesFactories.entries()),
25
- sessionServicesMeta: Array.from(state.sessionServicesMeta.entries()),
24
+ wireServicesFactories: Array.from(state.wireServicesFactories.entries()),
25
+ wireServicesMeta: Array.from(state.wireServicesMeta.entries()),
26
26
  configFactories: Array.from(state.configFactories.entries()),
27
27
  filesAndMethods: state.filesAndMethods,
28
28
  filesAndMethodsErrors: Array.from(state.filesAndMethodsErrors.entries()).map(([key, mapValue]) => [key, Array.from(mapValue.entries())]),
@@ -50,6 +50,10 @@ export function serializeInspectorState(state) {
50
50
  meta: state.queueWorkers.meta,
51
51
  files: Array.from(state.queueWorkers.files),
52
52
  },
53
+ workflows: {
54
+ meta: state.workflows.meta,
55
+ files: Array.from(state.workflows.files.entries()),
56
+ },
53
57
  rpc: {
54
58
  internalMeta: state.rpc.internalMeta,
55
59
  internalFiles: Array.from(state.rpc.internalFiles.entries()),
@@ -81,8 +85,9 @@ export function serializeInspectorState(state) {
81
85
  usedMiddleware: Array.from(state.serviceAggregation.usedMiddleware),
82
86
  usedPermissions: Array.from(state.serviceAggregation.usedPermissions),
83
87
  allSingletonServices: state.serviceAggregation.allSingletonServices,
84
- allSessionServices: state.serviceAggregation.allSessionServices,
88
+ allWireServices: state.serviceAggregation.allWireServices,
85
89
  },
90
+ serviceMetadata: state.serviceMetadata,
86
91
  };
87
92
  }
88
93
  /**
@@ -101,12 +106,12 @@ export function deserializeInspectorState(data) {
101
106
  return {
102
107
  rootDir: data.rootDir,
103
108
  singletonServicesTypeImportMap: new Map(data.singletonServicesTypeImportMap),
104
- sessionServicesTypeImportMap: new Map(data.sessionServicesTypeImportMap),
109
+ wireServicesTypeImportMap: new Map(data.wireServicesTypeImportMap),
105
110
  userSessionTypeImportMap: new Map(data.userSessionTypeImportMap),
106
111
  configTypeImportMap: new Map(data.configTypeImportMap),
107
112
  singletonServicesFactories: new Map(data.singletonServicesFactories),
108
- sessionServicesFactories: new Map(data.sessionServicesFactories),
109
- sessionServicesMeta: new Map(data.sessionServicesMeta),
113
+ wireServicesFactories: new Map(data.wireServicesFactories),
114
+ wireServicesMeta: new Map(data.wireServicesMeta),
110
115
  configFactories: new Map(data.configFactories),
111
116
  filesAndMethods: data.filesAndMethods,
112
117
  filesAndMethodsErrors: new Map(data.filesAndMethodsErrors.map(([key, entries]) => [
@@ -137,6 +142,10 @@ export function deserializeInspectorState(data) {
137
142
  meta: data.queueWorkers.meta,
138
143
  files: new Set(data.queueWorkers.files),
139
144
  },
145
+ workflows: {
146
+ meta: data.workflows.meta,
147
+ files: new Map(data.workflows.files),
148
+ },
140
149
  rpc: {
141
150
  internalMeta: data.rpc.internalMeta,
142
151
  internalFiles: new Map(data.rpc.internalFiles),
@@ -168,7 +177,8 @@ export function deserializeInspectorState(data) {
168
177
  usedMiddleware: new Set(data.serviceAggregation.usedMiddleware),
169
178
  usedPermissions: new Set(data.serviceAggregation.usedPermissions),
170
179
  allSingletonServices: data.serviceAggregation.allSingletonServices,
171
- allSessionServices: data.serviceAggregation.allSessionServices,
180
+ allWireServices: data.serviceAggregation.allWireServices,
172
181
  },
182
+ serviceMetadata: data.serviceMetadata || [],
173
183
  };
174
184
  }
@@ -1,3 +1,7 @@
1
1
  import * as ts from 'typescript';
2
2
  export declare const extractTypeKeys: (type: ts.Type) => string[];
3
+ /**
4
+ * Resolve an identifier or call expression to the actual function declaration
5
+ */
6
+ export declare function resolveFunctionDeclaration(node: ts.Node, checker: ts.TypeChecker): ts.Node | null;
3
7
  export declare function getPropertyAssignmentInitializer(obj: ts.ObjectLiteralExpression, propName: string, followShorthand?: boolean, checker?: ts.TypeChecker): ts.Expression | undefined;