@pikku/inspector 0.10.1 → 0.11.0

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 (38) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/add/add-channel.js +68 -14
  3. package/dist/add/add-functions.js +9 -2
  4. package/dist/add/add-workflow.d.ts +6 -0
  5. package/dist/add/add-workflow.js +152 -0
  6. package/dist/error-codes.d.ts +4 -1
  7. package/dist/error-codes.js +4 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/inspector.d.ts +6 -0
  11. package/dist/inspector.js +53 -15
  12. package/dist/types.d.ts +10 -2
  13. package/dist/utils/extract-node-value.d.ts +24 -0
  14. package/dist/utils/extract-node-value.js +79 -0
  15. package/dist/utils/post-process.d.ts +1 -1
  16. package/dist/utils/post-process.js +30 -0
  17. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  18. package/dist/utils/serialize-inspector-state.js +12 -0
  19. package/dist/utils/type-utils.d.ts +4 -0
  20. package/dist/utils/type-utils.js +60 -3
  21. package/dist/visit.js +2 -0
  22. package/package.json +2 -2
  23. package/src/add/add-channel.ts +94 -19
  24. package/src/add/add-functions.ts +10 -2
  25. package/src/add/add-workflow.ts +231 -0
  26. package/src/error-codes.ts +5 -0
  27. package/src/index.ts +1 -1
  28. package/src/inspector.ts +77 -22
  29. package/src/types.ts +10 -2
  30. package/src/utils/extract-node-value.ts +101 -0
  31. package/src/utils/post-process.ts +40 -2
  32. package/src/utils/serialize-inspector-state.ts +18 -0
  33. package/src/utils/test-data/inspector-state.json +4 -0
  34. package/src/utils/type-utils.ts +74 -3
  35. package/src/visit.ts +3 -1
  36. package/tsconfig.tsbuildinfo +1 -1
  37. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  38. package/src/add/add-mcp-resource.ts.tmp +0 -0
@@ -0,0 +1,231 @@
1
+ import * as ts from 'typescript'
2
+ import {
3
+ getPropertyValue,
4
+ getPropertyTags,
5
+ } from '../utils/get-property-value.js'
6
+ import { PikkuDocs } from '@pikku/core'
7
+ import { AddWiring, InspectorState } from '../types.js'
8
+ import { extractFunctionName } from '../utils/extract-function-name.js'
9
+ import {
10
+ getPropertyAssignmentInitializer,
11
+ resolveFunctionDeclaration,
12
+ } from '../utils/type-utils.js'
13
+ import { resolveMiddleware } from '../utils/middleware.js'
14
+ import { extractWireNames } from '../utils/post-process.js'
15
+ import { ErrorCode } from '../error-codes.js'
16
+ import { WorkflowStepMeta } from '@pikku/core/workflow'
17
+ import {
18
+ extractStringLiteral,
19
+ extractNumberLiteral,
20
+ extractPropertyString,
21
+ isStringLike,
22
+ isFunctionLike,
23
+ } from '../utils/extract-node-value.js'
24
+
25
+ /**
26
+ * Scan for workflow.do() and workflow.sleep() calls to extract workflow steps
27
+ */
28
+ function getWorkflowInvocations(
29
+ node: ts.Node,
30
+ checker: ts.TypeChecker,
31
+ state: InspectorState,
32
+ workflowName: string,
33
+ steps: WorkflowStepMeta[]
34
+ ) {
35
+ // Look for property access expressions: workflow.do or workflow.sleep
36
+ if (ts.isPropertyAccessExpression(node)) {
37
+ const { name } = node
38
+
39
+ // Check if this is accessing 'do' or 'sleep' property
40
+ if (name.text === 'do' || name.text === 'sleep') {
41
+ // Check if the parent is a call expression
42
+ const parent = node.parent
43
+ if (ts.isCallExpression(parent) && parent.expression === node) {
44
+ const args = parent.arguments
45
+
46
+ if (name.text === 'do' && args.length >= 2) {
47
+ // workflow.do(stepName, rpcName|fn, data?, options?)
48
+ const stepNameArg = args[0]
49
+ const secondArg = args[1]
50
+ const optionsArg =
51
+ args.length >= 3 ? args[args.length - 1] : undefined
52
+
53
+ const stepName = extractStringLiteral(stepNameArg, checker)
54
+ const description =
55
+ extractDescription(optionsArg, checker) ?? undefined
56
+
57
+ // Determine form by checking 2nd argument type
58
+ if (isStringLike(secondArg, checker)) {
59
+ // RPC form: workflow.do(stepName, rpcName, data, options?)
60
+ const rpcName = extractStringLiteral(secondArg, checker)
61
+ steps.push({
62
+ type: 'rpc',
63
+ stepName,
64
+ rpcName,
65
+ description,
66
+ })
67
+ state.rpc.invokedFunctions.add(rpcName)
68
+ } else if (isFunctionLike(secondArg)) {
69
+ // Inline form: workflow.do(stepName, fn, options?)
70
+ steps.push({
71
+ type: 'inline',
72
+ stepName: stepName || '<dynamic>',
73
+ description: description || '<dynamic>',
74
+ })
75
+ }
76
+ } else if (name.text === 'sleep' && args.length >= 2) {
77
+ // workflow.sleep(stepName, duration)
78
+ const stepNameArg = args[0]
79
+ const durationArg = args[1]
80
+
81
+ const stepName = extractStringLiteral(stepNameArg, checker)
82
+ const duration = extractDuration(durationArg, checker)
83
+
84
+ steps.push({
85
+ type: 'sleep',
86
+ stepName: stepName || '<dynamic>',
87
+ duration: duration || '<dynamic>',
88
+ })
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // Don't recurse into nested functions - only look at top-level workflow calls
95
+ ts.forEachChild(node, (child) => {
96
+ if (
97
+ ts.isFunctionDeclaration(child) ||
98
+ ts.isFunctionExpression(child) ||
99
+ ts.isArrowFunction(child)
100
+ ) {
101
+ return
102
+ }
103
+ getWorkflowInvocations(child, checker, state, workflowName, steps)
104
+ })
105
+ }
106
+
107
+ /**
108
+ * Extract description from options object
109
+ */
110
+ function extractDescription(
111
+ optionsNode: ts.Node | undefined,
112
+ checker: ts.TypeChecker
113
+ ): string | null {
114
+ if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
115
+ return null
116
+ }
117
+ return extractPropertyString(optionsNode, 'description', checker)
118
+ }
119
+
120
+ /**
121
+ * Extract duration value (number or string)
122
+ */
123
+ function extractDuration(
124
+ node: ts.Node,
125
+ checker: ts.TypeChecker
126
+ ): string | number | null {
127
+ const numValue = extractNumberLiteral(node)
128
+ if (numValue !== null) {
129
+ return numValue
130
+ }
131
+ return extractStringLiteral(node, checker)
132
+ }
133
+
134
+ /**
135
+ * Inspector for wireWorkflow() calls
136
+ * Detects workflow registration and extracts metadata
137
+ */
138
+ export const addWorkflow: AddWiring = (
139
+ logger,
140
+ node,
141
+ checker,
142
+ state,
143
+ options
144
+ ) => {
145
+ if (!ts.isCallExpression(node)) {
146
+ return
147
+ }
148
+
149
+ const args = node.arguments
150
+ const firstArg = args[0]
151
+ const expression = node.expression
152
+
153
+ // Check if the call is to wireWorkflow
154
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireWorkflow') {
155
+ return
156
+ }
157
+
158
+ if (!firstArg) {
159
+ return
160
+ }
161
+
162
+ if (ts.isObjectLiteralExpression(firstArg)) {
163
+ const obj = firstArg
164
+
165
+ const workflowName = getPropertyValue(obj, 'name') as string | null
166
+ const description = getPropertyValue(obj, 'description') as
167
+ | string
168
+ | undefined
169
+ const docs = (getPropertyValue(obj, 'docs') as PikkuDocs) || undefined
170
+ const tags = getPropertyTags(obj, 'Workflow', workflowName, logger)
171
+
172
+ // --- find the referenced function ---
173
+ const funcInitializer = getPropertyAssignmentInitializer(
174
+ obj,
175
+ 'func',
176
+ true,
177
+ checker
178
+ )
179
+
180
+ if (!workflowName) {
181
+ logger.critical(
182
+ ErrorCode.MISSING_NAME,
183
+ `Wasn't able to determine 'name' property for workflow wiring.`
184
+ )
185
+ return
186
+ }
187
+
188
+ if (!funcInitializer) {
189
+ logger.critical(
190
+ ErrorCode.MISSING_FUNC,
191
+ `No valid 'func' property for workflow '${workflowName}'.`
192
+ )
193
+ return
194
+ }
195
+
196
+ const pikkuFuncName = extractFunctionName(
197
+ funcInitializer,
198
+ checker,
199
+ state.rootDir
200
+ ).pikkuFuncName
201
+
202
+ // --- resolve middleware ---
203
+ const middleware = resolveMiddleware(state, obj, tags, checker)
204
+
205
+ // --- track used functions/middleware for service aggregation ---
206
+ state.serviceAggregation.usedFunctions.add(pikkuFuncName)
207
+ extractWireNames(middleware).forEach((name) =>
208
+ state.serviceAggregation.usedMiddleware.add(name)
209
+ )
210
+
211
+ state.workflows.files.add(node.getSourceFile().fileName)
212
+
213
+ // Extract workflow steps from function body
214
+ // Resolve the identifier to the actual function declaration
215
+ const resolvedFunc = resolveFunctionDeclaration(funcInitializer, checker)
216
+ const steps: WorkflowStepMeta[] = []
217
+ if (resolvedFunc) {
218
+ getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
219
+ }
220
+
221
+ state.workflows.meta[workflowName] = {
222
+ pikkuFuncName,
223
+ workflowName,
224
+ description,
225
+ docs,
226
+ tags,
227
+ middleware,
228
+ steps,
229
+ }
230
+ }
231
+ }
@@ -19,6 +19,8 @@ export enum ErrorCode {
19
19
  MISSING_QUEUE_NAME = 'PKU384',
20
20
  MISSING_CHANNEL_NAME = 'PKU400',
21
21
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = 'PKU672',
22
+ DYNAMIC_STEP_NAME = 'PKU529',
23
+ WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
22
24
 
23
25
  // Configuration errors
24
26
  CONFIG_TYPE_NOT_FOUND = 'PKU426',
@@ -40,4 +42,7 @@ export enum ErrorCode {
40
42
  PERMISSION_TAG_INVALID = 'PKU836',
41
43
  PERMISSION_EMPTY_ARRAY = 'PKU937',
42
44
  PERMISSION_PATTERN_INVALID = 'PKU975',
45
+
46
+ // Feature Flag
47
+ WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = 'PKU901',
43
48
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { inspect } from './inspector.js'
1
+ export { inspect, getInitialInspectorState } from './inspector.js'
2
2
  export { getFilesAndMethods } from './utils/get-files-and-methods.js'
3
3
  export type { TypesMap } from './types-map.js'
4
4
  export type * from './types.js'
package/src/inspector.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript'
2
+ import { performance } from 'perf_hooks'
2
3
  import { visitSetup, visitRoutes } from './visit.js'
3
4
  import { TypesMap } from './types-map.js'
4
5
  import { InspectorState, InspectorLogger, InspectorOptions } from './types.js'
@@ -6,22 +7,13 @@ import { getFilesAndMethods } from './utils/get-files-and-methods.js'
6
7
  import { findCommonAncestor } from './utils/find-root-dir.js'
7
8
  import { aggregateRequiredServices } from './utils/post-process.js'
8
9
 
9
- export const inspect = (
10
- logger: InspectorLogger,
11
- routeFiles: string[],
12
- options: InspectorOptions = {}
13
- ): InspectorState => {
14
- const program = ts.createProgram(routeFiles, {
15
- target: ts.ScriptTarget.ESNext,
16
- module: ts.ModuleKind.CommonJS,
17
- })
18
- const checker = program.getTypeChecker()
19
- const sourceFiles = program.getSourceFiles()
20
-
21
- // Infer root directory from source files
22
- const rootDir = findCommonAncestor(routeFiles)
23
-
24
- const state: InspectorState = {
10
+ /**
11
+ * Creates an initial/empty inspector state with all required properties initialized
12
+ * @param rootDir - The root directory for the project
13
+ * @returns A fresh InspectorState with empty collections
14
+ */
15
+ export function getInitialInspectorState(rootDir: string): InspectorState {
16
+ return {
25
17
  rootDir,
26
18
  singletonServicesTypeImportMap: new Map(),
27
19
  sessionServicesTypeImportMap: new Map(),
@@ -66,6 +58,10 @@ export const inspect = (
66
58
  meta: {},
67
59
  files: new Set(),
68
60
  },
61
+ workflows: {
62
+ meta: {},
63
+ files: new Set(),
64
+ },
69
65
  rpc: {
70
66
  internalMeta: {},
71
67
  internalFiles: new Map(),
@@ -99,30 +95,89 @@ export const inspect = (
99
95
  usedFunctions: new Set(),
100
96
  usedMiddleware: new Set(),
101
97
  usedPermissions: new Set(),
98
+ allSingletonServices: [],
99
+ allSessionServices: [],
102
100
  },
103
101
  }
102
+ }
103
+
104
+ export const inspect = (
105
+ logger: InspectorLogger,
106
+ routeFiles: string[],
107
+ options: InspectorOptions = {}
108
+ ): InspectorState => {
109
+ const startProgram = performance.now()
110
+ const program = ts.createProgram(routeFiles, {
111
+ target: ts.ScriptTarget.ESNext,
112
+ module: ts.ModuleKind.CommonJS,
113
+ skipLibCheck: true,
114
+ skipDefaultLibCheck: true,
115
+ moduleResolution: ts.ModuleResolutionKind.Node10,
116
+ types: [],
117
+ allowJs: false,
118
+ checkJs: false,
119
+ })
120
+ logger.debug(
121
+ `Created program in ${(performance.now() - startProgram).toFixed(2)}ms`
122
+ )
123
+
124
+ const startChecker = performance.now()
125
+ const checker = program.getTypeChecker()
126
+ logger.debug(
127
+ `Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`
128
+ )
129
+
130
+ const startSourceFiles = performance.now()
131
+ const sourceFiles = program.getSourceFiles()
132
+ logger.debug(
133
+ `Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`
134
+ )
135
+
136
+ // Infer root directory from source files
137
+ const rootDir = findCommonAncestor(routeFiles)
138
+
139
+ const state = getInitialInspectorState(rootDir)
104
140
 
105
141
  // First sweep: add all functions
142
+ const startSetup = performance.now()
106
143
  for (const sourceFile of sourceFiles) {
107
144
  ts.forEachChild(sourceFile, (child) =>
108
145
  visitSetup(logger, checker, child, state, options)
109
146
  )
110
147
  }
148
+ logger.debug(
149
+ `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`
150
+ )
111
151
 
112
- // Second sweep: add all transports
113
- for (const sourceFile of sourceFiles) {
114
- ts.forEachChild(sourceFile, (child) =>
115
- visitRoutes(logger, checker, child, state, options)
152
+ if (!options.setupOnly) {
153
+ // Second sweep: add all transports
154
+ const startRoutes = performance.now()
155
+ for (const sourceFile of sourceFiles) {
156
+ ts.forEachChild(sourceFile, (child) =>
157
+ visitRoutes(logger, checker, child, state, options)
158
+ )
159
+ }
160
+ logger.debug(
161
+ `Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`
116
162
  )
117
163
  }
118
164
 
119
165
  // Populate filesAndMethods
166
+ const startFilesAndMethods = performance.now()
120
167
  const { result, errors } = getFilesAndMethods(state, options.types)
121
168
  state.filesAndMethods = result
122
169
  state.filesAndMethodsErrors = errors
170
+ logger.debug(
171
+ `Get files and methods completed in ${(performance.now() - startFilesAndMethods).toFixed(2)}ms`
172
+ )
123
173
 
124
- // Post-processing: Aggregate required services from wired functions/middleware/permissions
125
- aggregateRequiredServices(state)
174
+ if (!options.setupOnly) {
175
+ const startAggregate = performance.now()
176
+ aggregateRequiredServices(state)
177
+ logger.debug(
178
+ `Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`
179
+ )
180
+ }
126
181
 
127
182
  return state
128
183
  }
package/src/types.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'
@@ -114,6 +115,7 @@ export type InspectorFilters = {
114
115
  }
115
116
 
116
117
  export type InspectorOptions = Partial<{
118
+ setupOnly: boolean
117
119
  types: Partial<{
118
120
  configFileType: string
119
121
  userSessionType: string
@@ -204,7 +206,11 @@ export interface InspectorState {
204
206
  files: Set<string>
205
207
  }
206
208
  queueWorkers: {
207
- meta: queueWorkersMeta
209
+ meta: QueueWorkersMeta
210
+ files: Set<string>
211
+ }
212
+ workflows: {
213
+ meta: WorkflowsMeta
208
214
  files: Set<string>
209
215
  }
210
216
  rpc: {
@@ -231,5 +237,7 @@ export interface InspectorState {
231
237
  usedFunctions: Set<string> // Function names actually wired/exposed
232
238
  usedMiddleware: Set<string> // Middleware names used by wired functions
233
239
  usedPermissions: Set<string> // Permission names used by wired functions
240
+ allSingletonServices: string[] // All services available in SingletonServices type
241
+ allSessionServices: string[] // All services available in Services type (excluding SingletonServices)
234
242
  }
235
243
  }
@@ -0,0 +1,101 @@
1
+ import * as ts from 'typescript'
2
+
3
+ /**
4
+ * Extract string literal value from a TypeScript node.
5
+ * Handles string literals, template literals (including placeholders),
6
+ * and constant variable references.
7
+ */
8
+ export function extractStringLiteral(
9
+ node: ts.Node,
10
+ checker: ts.TypeChecker
11
+ ): string {
12
+ if (ts.isStringLiteral(node)) {
13
+ return node.text
14
+ }
15
+
16
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
17
+ return node.text
18
+ }
19
+
20
+ if (ts.isTemplateExpression(node)) {
21
+ // reconstruct: `head + ${expr} + middle + ${expr} + tail`
22
+ let result = node.head.text
23
+ for (const span of node.templateSpans) {
24
+ const exprText = span.expression.getText()
25
+ result += '${' + exprText + '}' + span.literal.text
26
+ }
27
+ return result
28
+ }
29
+
30
+ // Try to evaluate constant identifiers
31
+ if (ts.isIdentifier(node)) {
32
+ const symbol = checker.getSymbolAtLocation(node)
33
+ if (
34
+ symbol?.valueDeclaration &&
35
+ ts.isVariableDeclaration(symbol.valueDeclaration)
36
+ ) {
37
+ const init = symbol.valueDeclaration.initializer
38
+ if (init) {
39
+ return extractStringLiteral(init, checker)
40
+ }
41
+ }
42
+ }
43
+
44
+ throw new Error('Unable to extract string literal from node')
45
+ }
46
+
47
+ /**
48
+ * Check if node is string-like (string literal or template expression)
49
+ */
50
+ export function isStringLike(node: ts.Node, _checker: ts.TypeChecker): boolean {
51
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
52
+ return true
53
+ }
54
+ // Check if it's a template string with substitutions
55
+ if (ts.isTemplateExpression(node)) {
56
+ return true
57
+ }
58
+ return false
59
+ }
60
+
61
+ /**
62
+ * Check if node is function-like (arrow, function expression, or function declaration)
63
+ */
64
+ export function isFunctionLike(node: ts.Node): boolean {
65
+ return (
66
+ ts.isArrowFunction(node) ||
67
+ ts.isFunctionExpression(node) ||
68
+ ts.isFunctionDeclaration(node)
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Extract number literal value from a node
74
+ */
75
+ export function extractNumberLiteral(node: ts.Node): number | null {
76
+ if (ts.isNumericLiteral(node)) {
77
+ return Number(node.text)
78
+ }
79
+ return null
80
+ }
81
+
82
+ /**
83
+ * Extract a property value from an object literal expression
84
+ * Returns the extracted value or null if not found/cannot extract
85
+ */
86
+ export function extractPropertyString(
87
+ objNode: ts.ObjectLiteralExpression,
88
+ propertyName: string,
89
+ checker: ts.TypeChecker
90
+ ): string | null {
91
+ for (const prop of objNode.properties) {
92
+ if (
93
+ ts.isPropertyAssignment(prop) &&
94
+ ts.isIdentifier(prop.name) &&
95
+ prop.name.text === propertyName
96
+ ) {
97
+ return extractStringLiteral(prop.initializer, checker)
98
+ }
99
+ }
100
+ return null
101
+ }
@@ -4,6 +4,7 @@ import {
4
4
  MiddlewareMetadata,
5
5
  PermissionMetadata,
6
6
  } from '@pikku/core'
7
+ import { extractTypeKeys } from './type-utils.js'
7
8
 
8
9
  /**
9
10
  * Helper to extract wire-level middleware/permission names from metadata.
@@ -28,7 +29,7 @@ export function extractWireNames(
28
29
  */
29
30
  function expandAndAddGroupServices(
30
31
  list: MiddlewareMetadata[] | PermissionMetadata[] | undefined,
31
- state: InspectorState,
32
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>,
32
33
  addServices: (services: FunctionServicesMeta | undefined) => void,
33
34
  isMiddleware: boolean
34
35
  ): void {
@@ -57,6 +58,38 @@ function expandAndAddGroupServices(
57
58
  }
58
59
  }
59
60
 
61
+ /**
62
+ * Extracts all service names from SingletonServices and Services types.
63
+ * This provides the complete list of available services for code generation.
64
+ * Only runs if typesLookup is available (omitted in deserialized states).
65
+ */
66
+ function extractAllServices(
67
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
68
+ ): void {
69
+ // Skip if typesLookup is not available (e.g., deserialized state)
70
+ if (!('typesLookup' in state)) {
71
+ return
72
+ }
73
+
74
+ // Extract all singleton services from the SingletonServices type
75
+ const singletonServicesTypes = state.typesLookup.get('SingletonServices')
76
+ if (singletonServicesTypes && singletonServicesTypes.length > 0) {
77
+ const singletonServiceNames = extractTypeKeys(singletonServicesTypes[0])
78
+ state.serviceAggregation.allSingletonServices = singletonServiceNames.sort()
79
+ }
80
+
81
+ // Extract all services from the Services type
82
+ const servicesTypes = state.typesLookup.get('Services')
83
+ if (servicesTypes && servicesTypes.length > 0) {
84
+ const allServiceNames = extractTypeKeys(servicesTypes[0])
85
+ // Session services are those in Services but not in SingletonServices
86
+ const singletonSet = new Set(state.serviceAggregation.allSingletonServices)
87
+ state.serviceAggregation.allSessionServices = allServiceNames
88
+ .filter((name) => !singletonSet.has(name))
89
+ .sort()
90
+ }
91
+ }
92
+
60
93
  /**
61
94
  * Aggregates all required services from wired functions, middleware, and permissions.
62
95
  * Must be called after AST traversal completes.
@@ -64,7 +97,12 @@ function expandAndAddGroupServices(
64
97
  * Note: usedFunctions, usedMiddleware, and usedPermissions are tracked directly
65
98
  * in the add-* methods during AST traversal for efficiency.
66
99
  */
67
- export function aggregateRequiredServices(state: InspectorState): void {
100
+ export function aggregateRequiredServices(
101
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
102
+ ): void {
103
+ // First, extract all available services from types
104
+ extractAllServices(state)
105
+
68
106
  const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } =
69
107
  state.serviceAggregation
70
108
 
@@ -112,6 +112,10 @@ export interface SerializableInspectorState {
112
112
  meta: InspectorState['queueWorkers']['meta']
113
113
  files: string[]
114
114
  }
115
+ workflows: {
116
+ meta: InspectorState['workflows']['meta']
117
+ files: string[]
118
+ }
115
119
  rpc: {
116
120
  internalMeta: InspectorState['rpc']['internalMeta']
117
121
  internalFiles: Array<[string, { path: string; exportedName: string }]>
@@ -162,6 +166,8 @@ export interface SerializableInspectorState {
162
166
  usedFunctions: string[]
163
167
  usedMiddleware: string[]
164
168
  usedPermissions: string[]
169
+ allSingletonServices: string[]
170
+ allSessionServices: string[]
165
171
  }
166
172
  }
167
173
 
@@ -241,6 +247,10 @@ export function serializeInspectorState(
241
247
  meta: state.queueWorkers.meta,
242
248
  files: Array.from(state.queueWorkers.files),
243
249
  },
250
+ workflows: {
251
+ meta: state.workflows.meta,
252
+ files: Array.from(state.workflows.files),
253
+ },
244
254
  rpc: {
245
255
  internalMeta: state.rpc.internalMeta,
246
256
  internalFiles: Array.from(state.rpc.internalFiles.entries()),
@@ -271,6 +281,8 @@ export function serializeInspectorState(
271
281
  usedFunctions: Array.from(state.serviceAggregation.usedFunctions),
272
282
  usedMiddleware: Array.from(state.serviceAggregation.usedMiddleware),
273
283
  usedPermissions: Array.from(state.serviceAggregation.usedPermissions),
284
+ allSingletonServices: state.serviceAggregation.allSingletonServices,
285
+ allSessionServices: state.serviceAggregation.allSessionServices,
274
286
  },
275
287
  }
276
288
  }
@@ -340,6 +352,10 @@ export function deserializeInspectorState(
340
352
  meta: data.queueWorkers.meta,
341
353
  files: new Set(data.queueWorkers.files),
342
354
  },
355
+ workflows: {
356
+ meta: data.workflows.meta,
357
+ files: new Set(data.workflows.files),
358
+ },
343
359
  rpc: {
344
360
  internalMeta: data.rpc.internalMeta,
345
361
  internalFiles: new Map(data.rpc.internalFiles),
@@ -370,6 +386,8 @@ export function deserializeInspectorState(
370
386
  usedFunctions: new Set(data.serviceAggregation.usedFunctions),
371
387
  usedMiddleware: new Set(data.serviceAggregation.usedMiddleware),
372
388
  usedPermissions: new Set(data.serviceAggregation.usedPermissions),
389
+ allSingletonServices: data.serviceAggregation.allSingletonServices,
390
+ allSessionServices: data.serviceAggregation.allSessionServices,
373
391
  },
374
392
  }
375
393
  }
@@ -1260,6 +1260,10 @@
1260
1260
  },
1261
1261
  "files": ["src/queue-worker.wiring.ts"]
1262
1262
  },
1263
+ "workflows": {
1264
+ "meta": {},
1265
+ "files": []
1266
+ },
1263
1267
  "rpc": {
1264
1268
  "internalMeta": {
1265
1269
  "onConnect": "onConnect",