@pikku/inspector 0.12.7 → 0.12.9
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.
- package/CHANGELOG.md +29 -0
- package/dist/add/add-ai-agent.js +24 -7
- package/dist/add/add-channel.js +2 -2
- package/dist/add/add-cli.js +13 -10
- package/dist/add/add-file-with-factory.js +22 -5
- package/dist/add/add-functions.js +4 -3
- package/dist/add/add-http-route.js +1 -0
- package/dist/add/add-mcp-prompt.js +4 -0
- package/dist/add/add-mcp-resource.js +4 -0
- package/dist/add/add-rpc-invocations.js +2 -2
- package/dist/add/add-workflow.d.ts +5 -0
- package/dist/add/add-workflow.js +20 -2
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/extract-function-name.d.ts +1 -0
- package/dist/utils/extract-function-name.js +27 -32
- package/dist/utils/extract-node-value.js +6 -1
- package/dist/utils/filter-inspector-state.js +211 -8
- package/dist/utils/load-addon-functions-meta.js +47 -0
- package/dist/utils/post-process.js +63 -0
- package/dist/utils/resolve-versions.js +30 -0
- package/dist/utils/schema-generator.js +124 -33
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/visit.js +1 -1
- package/package.json +2 -2
- package/src/add/add-ai-agent.ts +25 -10
- package/src/add/add-channel.ts +2 -2
- package/src/add/add-cli.ts +17 -16
- package/src/add/add-file-with-factory.ts +26 -7
- package/src/add/add-functions.ts +4 -4
- package/src/add/add-http-route.ts +6 -1
- package/src/add/add-mcp-prompt.ts +5 -0
- package/src/add/add-mcp-resource.ts +5 -0
- package/src/add/add-queue-worker.ts +5 -1
- package/src/add/add-rpc-invocations.ts +2 -2
- package/src/add/add-workflow.ts +22 -2
- package/src/inspector.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/extract-function-name.ts +36 -37
- package/src/utils/extract-node-value.test.ts +67 -0
- package/src/utils/extract-node-value.ts +5 -1
- package/src/utils/filter-inspector-state.ts +246 -11
- package/src/utils/load-addon-functions-meta.ts +59 -0
- package/src/utils/post-process.ts +74 -0
- package/src/utils/resolve-versions.test.ts +141 -0
- package/src/utils/resolve-versions.ts +37 -0
- package/src/utils/schema-generator.ts +191 -41
- package/src/utils/serialize-inspector-state.ts +3 -0
- package/src/visit.ts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
2
|
import { randomUUID } from 'crypto'
|
|
3
|
+
import { formatVersionedId } from '@pikku/core'
|
|
3
4
|
|
|
4
5
|
export type ExtractedFunctionName = {
|
|
5
6
|
pikkuFuncId: string
|
|
@@ -8,6 +9,7 @@ export type ExtractedFunctionName = {
|
|
|
8
9
|
exportedName: string | null
|
|
9
10
|
propertyName: string | null
|
|
10
11
|
isHelper: boolean
|
|
12
|
+
version: number | null
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function makeContextBasedId(
|
|
@@ -40,6 +42,7 @@ export function extractFunctionName(
|
|
|
40
42
|
propertyName: null,
|
|
41
43
|
explicitName: null,
|
|
42
44
|
isHelper: false,
|
|
45
|
+
version: null,
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const workflowHelpers = new Set([
|
|
@@ -143,18 +146,7 @@ export function extractFunctionName(
|
|
|
143
146
|
// Check for object with 'name' property in first argument
|
|
144
147
|
const firstArg = args[0]
|
|
145
148
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
ts.isPropertyAssignment(prop) &&
|
|
149
|
-
ts.isIdentifier(prop.name) &&
|
|
150
|
-
prop.name.text === 'override' &&
|
|
151
|
-
ts.isStringLiteral(prop.initializer)
|
|
152
|
-
) {
|
|
153
|
-
// Priority 1: Object with override property
|
|
154
|
-
result.explicitName = prop.initializer.text
|
|
155
|
-
break
|
|
156
|
-
}
|
|
157
|
-
}
|
|
149
|
+
extractOverrideAndVersion(firstArg, result)
|
|
158
150
|
}
|
|
159
151
|
|
|
160
152
|
// Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
|
|
@@ -367,21 +359,9 @@ export function extractFunctionName(
|
|
|
367
359
|
ts.isIdentifier(decl.initializer.expression) &&
|
|
368
360
|
decl.initializer.expression.text.startsWith('pikku')
|
|
369
361
|
) {
|
|
370
|
-
// Check for object with 'override' property in first argument
|
|
371
362
|
const firstArg = decl.initializer.arguments[0]
|
|
372
363
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
ts.isPropertyAssignment(prop) &&
|
|
376
|
-
ts.isIdentifier(prop.name) &&
|
|
377
|
-
prop.name.text === 'override' &&
|
|
378
|
-
ts.isStringLiteral(prop.initializer)
|
|
379
|
-
) {
|
|
380
|
-
// Priority 1: Object with override property
|
|
381
|
-
result.explicitName = prop.initializer.text
|
|
382
|
-
break
|
|
383
|
-
}
|
|
384
|
-
}
|
|
364
|
+
extractOverrideAndVersion(firstArg, result)
|
|
385
365
|
}
|
|
386
366
|
|
|
387
367
|
if (decl.initializer.expression.text.startsWith('pikku')) {
|
|
@@ -448,18 +428,7 @@ export function extractFunctionName(
|
|
|
448
428
|
else if (ts.isCallExpression(callExpr)) {
|
|
449
429
|
const firstArg = callExpr.arguments[0]
|
|
450
430
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
ts.isPropertyAssignment(prop) &&
|
|
454
|
-
ts.isIdentifier(prop.name) &&
|
|
455
|
-
prop.name.text === 'override' &&
|
|
456
|
-
ts.isStringLiteral(prop.initializer) &&
|
|
457
|
-
!result.explicitName // Only set if not already set
|
|
458
|
-
) {
|
|
459
|
-
result.explicitName = prop.initializer.text
|
|
460
|
-
break
|
|
461
|
-
}
|
|
462
|
-
}
|
|
431
|
+
extractOverrideAndVersion(firstArg, result)
|
|
463
432
|
}
|
|
464
433
|
}
|
|
465
434
|
|
|
@@ -474,6 +443,10 @@ export function extractFunctionName(
|
|
|
474
443
|
result.pikkuFuncId = `__temp_${randomUUID()}`
|
|
475
444
|
}
|
|
476
445
|
|
|
446
|
+
if (result.version !== null) {
|
|
447
|
+
result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version)
|
|
448
|
+
}
|
|
449
|
+
|
|
477
450
|
return result
|
|
478
451
|
}
|
|
479
452
|
|
|
@@ -539,3 +512,29 @@ export function isNamedExport(
|
|
|
539
512
|
|
|
540
513
|
return false
|
|
541
514
|
}
|
|
515
|
+
|
|
516
|
+
function extractOverrideAndVersion(
|
|
517
|
+
objLiteral: ts.ObjectLiteralExpression,
|
|
518
|
+
result: ExtractedFunctionName
|
|
519
|
+
): void {
|
|
520
|
+
for (const prop of objLiteral.properties) {
|
|
521
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
522
|
+
if (
|
|
523
|
+
prop.name.text === 'override' &&
|
|
524
|
+
ts.isStringLiteral(prop.initializer) &&
|
|
525
|
+
!result.explicitName
|
|
526
|
+
) {
|
|
527
|
+
result.explicitName = prop.initializer.text
|
|
528
|
+
} else if (
|
|
529
|
+
prop.name.text === 'version' &&
|
|
530
|
+
ts.isNumericLiteral(prop.initializer) &&
|
|
531
|
+
result.version === null
|
|
532
|
+
) {
|
|
533
|
+
const parsed = Number(prop.initializer.text)
|
|
534
|
+
if (Number.isInteger(parsed) && parsed >= 1) {
|
|
535
|
+
result.version = parsed
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import * as ts from 'typescript'
|
|
4
|
+
import { extractDescription } from './extract-node-value'
|
|
5
|
+
|
|
6
|
+
const createChecker = (source: string) => {
|
|
7
|
+
const sourceFile = ts.createSourceFile(
|
|
8
|
+
'test.ts',
|
|
9
|
+
source,
|
|
10
|
+
ts.ScriptTarget.Latest,
|
|
11
|
+
true,
|
|
12
|
+
ts.ScriptKind.TS
|
|
13
|
+
)
|
|
14
|
+
const host = ts.createCompilerHost({})
|
|
15
|
+
const originalGetSourceFile = host.getSourceFile
|
|
16
|
+
host.getSourceFile = (fileName, target) => {
|
|
17
|
+
if (fileName === 'test.ts') return sourceFile
|
|
18
|
+
return originalGetSourceFile.call(host, fileName, target)
|
|
19
|
+
}
|
|
20
|
+
const program = ts.createProgram(['test.ts'], {}, host)
|
|
21
|
+
return { checker: program.getTypeChecker(), sourceFile }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const findObjectLiteral = (
|
|
25
|
+
node: ts.Node
|
|
26
|
+
): ts.ObjectLiteralExpression | undefined => {
|
|
27
|
+
if (ts.isObjectLiteralExpression(node)) return node
|
|
28
|
+
let result: ts.ObjectLiteralExpression | undefined
|
|
29
|
+
ts.forEachChild(node, (child) => {
|
|
30
|
+
if (!result) result = findObjectLiteral(child)
|
|
31
|
+
})
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('extractDescription', () => {
|
|
36
|
+
test('returns null when node is undefined', () => {
|
|
37
|
+
const { checker } = createChecker('')
|
|
38
|
+
assert.equal(extractDescription(undefined, checker), null)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('extracts string literal description', () => {
|
|
42
|
+
const { checker, sourceFile } = createChecker(
|
|
43
|
+
`const opts = { description: 'my step' }`
|
|
44
|
+
)
|
|
45
|
+
const obj = findObjectLiteral(sourceFile)!
|
|
46
|
+
assert.equal(extractDescription(obj, checker), 'my step')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('returns null for non-literal description value without crashing', () => {
|
|
50
|
+
const { checker, sourceFile } = createChecker(
|
|
51
|
+
`const name = 'test'; const data = { description: name + ' addon' }`
|
|
52
|
+
)
|
|
53
|
+
const objs: ts.ObjectLiteralExpression[] = []
|
|
54
|
+
const visit = (node: ts.Node) => {
|
|
55
|
+
if (ts.isObjectLiteralExpression(node)) objs.push(node)
|
|
56
|
+
ts.forEachChild(node, visit)
|
|
57
|
+
}
|
|
58
|
+
ts.forEachChild(sourceFile, visit)
|
|
59
|
+
const dataObj = objs[objs.length - 1]!
|
|
60
|
+
assert.equal(extractDescription(dataObj, checker), null)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('returns null for non-object node', () => {
|
|
64
|
+
const { checker, sourceFile } = createChecker(`const x = 42`)
|
|
65
|
+
assert.equal(extractDescription(sourceFile, checker), null)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -110,7 +110,11 @@ export function extractDescription(
|
|
|
110
110
|
if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
|
|
111
111
|
return null
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
try {
|
|
114
|
+
return extractPropertyString(optionsNode, 'description', checker)
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
/**
|
|
@@ -191,6 +191,11 @@ export function filterInspectorState(
|
|
|
191
191
|
return state
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Snapshot the original workflow graph meta before filtering prunes it
|
|
195
|
+
const originalGraphMeta = {
|
|
196
|
+
...((state as InspectorState).workflows?.graphMeta ?? {}),
|
|
197
|
+
}
|
|
198
|
+
|
|
194
199
|
// Create a shallow copy with new Maps/Sets to avoid mutating the original
|
|
195
200
|
const filteredState = {
|
|
196
201
|
...state,
|
|
@@ -201,11 +206,21 @@ export function filterInspectorState(
|
|
|
201
206
|
usedMiddleware: new Set<string>(),
|
|
202
207
|
usedPermissions: new Set<string>(),
|
|
203
208
|
},
|
|
209
|
+
functions: {
|
|
210
|
+
...state.functions,
|
|
211
|
+
meta: JSON.parse(JSON.stringify(state.functions.meta)), // Deep clone to avoid mutating original
|
|
212
|
+
files: new Map(state.functions.files),
|
|
213
|
+
},
|
|
204
214
|
http: {
|
|
205
215
|
...state.http,
|
|
206
216
|
meta: JSON.parse(JSON.stringify(state.http.meta)), // Deep clone metadata
|
|
207
217
|
files: new Set<string>(), // Will be repopulated with filtered files
|
|
208
218
|
},
|
|
219
|
+
workflows: {
|
|
220
|
+
...state.workflows,
|
|
221
|
+
graphMeta: JSON.parse(JSON.stringify(state.workflows?.graphMeta ?? {})),
|
|
222
|
+
meta: JSON.parse(JSON.stringify(state.workflows?.meta ?? {})),
|
|
223
|
+
},
|
|
209
224
|
channels: {
|
|
210
225
|
...state.channels,
|
|
211
226
|
meta: JSON.parse(JSON.stringify(state.channels.meta)),
|
|
@@ -240,6 +255,14 @@ export function filterInspectorState(
|
|
|
240
255
|
agentsMeta: JSON.parse(JSON.stringify(state.agents?.agentsMeta ?? {})),
|
|
241
256
|
files: new Map(),
|
|
242
257
|
},
|
|
258
|
+
rpc: {
|
|
259
|
+
...state.rpc,
|
|
260
|
+
internalMeta: { ...state.rpc.internalMeta }, // Clone to avoid mutating original
|
|
261
|
+
internalFiles: new Map(state.rpc.internalFiles),
|
|
262
|
+
exposedMeta: { ...state.rpc.exposedMeta },
|
|
263
|
+
exposedFiles: new Map(state.rpc.exposedFiles),
|
|
264
|
+
invokedFunctions: new Set(state.rpc.invokedFunctions),
|
|
265
|
+
},
|
|
243
266
|
cli: {
|
|
244
267
|
...state.cli,
|
|
245
268
|
meta: JSON.parse(JSON.stringify(state.cli.meta)),
|
|
@@ -280,6 +303,14 @@ export function filterInspectorState(
|
|
|
280
303
|
filteredState.serviceAggregation.usedFunctions.add(
|
|
281
304
|
routeMeta.pikkuFuncId
|
|
282
305
|
)
|
|
306
|
+
// For workflow/agent routes, also add the base name
|
|
307
|
+
// so the workflow/agent definition survives pruning
|
|
308
|
+
const colonIdx = routeMeta.pikkuFuncId.indexOf(':')
|
|
309
|
+
if (colonIdx !== -1) {
|
|
310
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
311
|
+
routeMeta.pikkuFuncId.slice(colonIdx + 1)
|
|
312
|
+
)
|
|
313
|
+
}
|
|
283
314
|
}
|
|
284
315
|
extractWireNames(routeMeta.middleware).forEach((name: string) =>
|
|
285
316
|
filteredState.serviceAggregation.usedMiddleware.add(name)
|
|
@@ -291,12 +322,28 @@ export function filterInspectorState(
|
|
|
291
322
|
}
|
|
292
323
|
}
|
|
293
324
|
|
|
294
|
-
// Repopulate http.files
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
325
|
+
// Repopulate http.files with only files that have surviving routes
|
|
326
|
+
for (const method of Object.keys(filteredState.http.meta)) {
|
|
327
|
+
const routes = (
|
|
328
|
+
filteredState.http.meta as Record<
|
|
329
|
+
string,
|
|
330
|
+
Record<string, { sourceFile?: string }>
|
|
331
|
+
>
|
|
332
|
+
)[method]
|
|
333
|
+
for (const routeMeta of Object.values(routes)) {
|
|
334
|
+
if (routeMeta.sourceFile) {
|
|
335
|
+
filteredState.http.files.add(routeMeta.sourceFile)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Fallback: if no sourceFile info available but routes exist, include all files
|
|
340
|
+
if (filteredState.http.files.size === 0) {
|
|
341
|
+
const hasHttpRoutes = Object.values(
|
|
342
|
+
filteredState.http.meta as Record<string, Record<string, unknown>>
|
|
343
|
+
).some((routes) => Object.keys(routes).length > 0)
|
|
344
|
+
if (hasHttpRoutes) {
|
|
345
|
+
filteredState.http.files = new Set(state.http.files)
|
|
346
|
+
}
|
|
300
347
|
}
|
|
301
348
|
|
|
302
349
|
// Filter channels
|
|
@@ -315,11 +362,40 @@ export function filterInspectorState(
|
|
|
315
362
|
if (!matches) {
|
|
316
363
|
delete filteredState.channels.meta[name]
|
|
317
364
|
} else {
|
|
318
|
-
|
|
365
|
+
// Add all functions referenced by this channel
|
|
366
|
+
if ('pikkuFuncId' in channelMeta && channelMeta.pikkuFuncId) {
|
|
367
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
368
|
+
channelMeta.pikkuFuncId as string
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
if (channelMeta.connect?.pikkuFuncId) {
|
|
372
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
373
|
+
channelMeta.connect.pikkuFuncId
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
if (channelMeta.disconnect?.pikkuFuncId) {
|
|
319
377
|
filteredState.serviceAggregation.usedFunctions.add(
|
|
320
|
-
channelMeta.pikkuFuncId
|
|
378
|
+
channelMeta.disconnect.pikkuFuncId
|
|
321
379
|
)
|
|
322
380
|
}
|
|
381
|
+
if (channelMeta.message?.pikkuFuncId) {
|
|
382
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
383
|
+
channelMeta.message.pikkuFuncId
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
if (channelMeta.messageWirings) {
|
|
387
|
+
for (const groupKey of Object.keys(channelMeta.messageWirings)) {
|
|
388
|
+
const commands = channelMeta.messageWirings[groupKey]
|
|
389
|
+
for (const cmdKey of Object.keys(commands)) {
|
|
390
|
+
const wiring = commands[cmdKey]
|
|
391
|
+
if (wiring.pikkuFuncId) {
|
|
392
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
393
|
+
wiring.pikkuFuncId
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
323
399
|
extractWireNames(channelMeta.middleware).forEach((name: string) =>
|
|
324
400
|
filteredState.serviceAggregation.usedMiddleware.add(name)
|
|
325
401
|
)
|
|
@@ -413,6 +489,12 @@ export function filterInspectorState(
|
|
|
413
489
|
filteredState.serviceAggregation.usedFunctions.add(
|
|
414
490
|
workerMeta.pikkuFuncId
|
|
415
491
|
)
|
|
492
|
+
const colonIdx = workerMeta.pikkuFuncId.indexOf(':')
|
|
493
|
+
if (colonIdx !== -1) {
|
|
494
|
+
filteredState.serviceAggregation.usedFunctions.add(
|
|
495
|
+
workerMeta.pikkuFuncId.slice(colonIdx + 1)
|
|
496
|
+
)
|
|
497
|
+
}
|
|
416
498
|
}
|
|
417
499
|
extractWireNames(workerMeta.middleware).forEach((name: string) =>
|
|
418
500
|
filteredState.serviceAggregation.usedMiddleware.add(name)
|
|
@@ -616,11 +698,47 @@ export function filterInspectorState(
|
|
|
616
698
|
filteredState.cli.files = new Set(state.cli.files)
|
|
617
699
|
}
|
|
618
700
|
|
|
619
|
-
//
|
|
701
|
+
// Direct function filtering: functions that match the names/tags/directories
|
|
702
|
+
// filters should be included even if no wiring (HTTP, scheduler, etc.) references them.
|
|
703
|
+
// This ensures standalone RPC-callable functions survive filtering.
|
|
704
|
+
// Only run when function-level filters are active — httpRoutes/httpMethods work
|
|
705
|
+
// through the HTTP wiring pass which already adds the right functions.
|
|
706
|
+
const hasFunctionLevelFilters =
|
|
707
|
+
(filters.names && filters.names.length > 0) ||
|
|
708
|
+
(filters.tags && filters.tags.length > 0) ||
|
|
709
|
+
(filters.directories && filters.directories.length > 0)
|
|
710
|
+
|
|
711
|
+
for (const funcId of Object.keys(filteredState.functions.meta)) {
|
|
712
|
+
if (!hasFunctionLevelFilters) break
|
|
713
|
+
const funcMeta = filteredState.functions.meta[funcId]
|
|
714
|
+
const funcFile = filteredState.functions.files.get(funcId)
|
|
715
|
+
const filePath = funcFile?.path
|
|
716
|
+
|
|
717
|
+
const matches = matchesFilters(
|
|
718
|
+
filters,
|
|
719
|
+
{
|
|
720
|
+
type: 'rpc' as PikkuWiringTypes,
|
|
721
|
+
name: funcId,
|
|
722
|
+
tags: funcMeta.tags,
|
|
723
|
+
filePath,
|
|
724
|
+
},
|
|
725
|
+
logger
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
if (matches) {
|
|
729
|
+
filteredState.serviceAggregation.usedFunctions.add(funcId)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Post-filter version expansion: when an unversioned base name is matched,
|
|
734
|
+
// include all its versions. Specific version matches (e.g. analyzeData@v1)
|
|
735
|
+
// do NOT expand to include other versions.
|
|
620
736
|
const includedBaseNames = new Set<string>()
|
|
621
737
|
for (const funcId of filteredState.serviceAggregation.usedFunctions) {
|
|
622
|
-
const { baseName } = parseVersionedId(funcId)
|
|
623
|
-
|
|
738
|
+
const { baseName, version } = parseVersionedId(funcId)
|
|
739
|
+
if (version === null) {
|
|
740
|
+
includedBaseNames.add(baseName)
|
|
741
|
+
}
|
|
624
742
|
}
|
|
625
743
|
if (includedBaseNames.size > 0) {
|
|
626
744
|
for (const funcId of Object.keys(state.functions.meta)) {
|
|
@@ -631,6 +749,123 @@ export function filterInspectorState(
|
|
|
631
749
|
}
|
|
632
750
|
}
|
|
633
751
|
|
|
752
|
+
// Prune functions.meta and functions.files to only include used functions
|
|
753
|
+
if (filteredState.serviceAggregation.usedFunctions.size > 0) {
|
|
754
|
+
for (const funcId of Object.keys(filteredState.functions.meta)) {
|
|
755
|
+
if (!filteredState.serviceAggregation.usedFunctions.has(funcId)) {
|
|
756
|
+
delete filteredState.functions.meta[funcId]
|
|
757
|
+
filteredState.functions.files.delete(funcId)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Prune channels whose functions were filtered out
|
|
762
|
+
for (const name of Object.keys(filteredState.channels.meta)) {
|
|
763
|
+
const channelMeta = filteredState.channels.meta[name]
|
|
764
|
+
// Check if any of the channel's functions are in the used set
|
|
765
|
+
const channelFuncIds: string[] = []
|
|
766
|
+
if (channelMeta.connect?.pikkuFuncId)
|
|
767
|
+
channelFuncIds.push(channelMeta.connect.pikkuFuncId)
|
|
768
|
+
if (channelMeta.disconnect?.pikkuFuncId)
|
|
769
|
+
channelFuncIds.push(channelMeta.disconnect.pikkuFuncId)
|
|
770
|
+
if (channelMeta.message?.pikkuFuncId)
|
|
771
|
+
channelFuncIds.push(channelMeta.message.pikkuFuncId)
|
|
772
|
+
if (channelMeta.messageWirings) {
|
|
773
|
+
for (const groupKey of Object.keys(channelMeta.messageWirings)) {
|
|
774
|
+
const commands = channelMeta.messageWirings[groupKey]
|
|
775
|
+
for (const cmdKey of Object.keys(commands)) {
|
|
776
|
+
const wiring = commands[cmdKey]
|
|
777
|
+
if (wiring.pikkuFuncId) channelFuncIds.push(wiring.pikkuFuncId)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const hasUsedFunc = channelFuncIds.some((id) =>
|
|
782
|
+
filteredState.serviceAggregation.usedFunctions.has(id)
|
|
783
|
+
)
|
|
784
|
+
if (channelFuncIds.length > 0 && !hasUsedFunc) {
|
|
785
|
+
delete filteredState.channels.meta[name]
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Prune workflow graphs whose function was filtered out
|
|
790
|
+
const workflowKeys = new Set([
|
|
791
|
+
...Object.keys(filteredState.workflows.graphMeta),
|
|
792
|
+
...Object.keys(filteredState.workflows.meta),
|
|
793
|
+
])
|
|
794
|
+
for (const name of workflowKeys) {
|
|
795
|
+
const graphMeta = filteredState.workflows.graphMeta[name]
|
|
796
|
+
const workflowMeta = filteredState.workflows.meta[name]
|
|
797
|
+
// Check both graphMeta.pikkuFuncId and meta.pikkuFuncId
|
|
798
|
+
const pikkuFuncId = graphMeta?.pikkuFuncId ?? workflowMeta?.pikkuFuncId
|
|
799
|
+
if (
|
|
800
|
+
pikkuFuncId &&
|
|
801
|
+
!filteredState.serviceAggregation.usedFunctions.has(pikkuFuncId)
|
|
802
|
+
) {
|
|
803
|
+
delete filteredState.workflows.graphMeta[name]
|
|
804
|
+
delete filteredState.workflows.meta[name]
|
|
805
|
+
} else if (!pikkuFuncId) {
|
|
806
|
+
// No function ID found — prune it
|
|
807
|
+
delete filteredState.workflows.graphMeta[name]
|
|
808
|
+
delete filteredState.workflows.meta[name]
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Prune RPC meta to only include entries whose target function survived
|
|
813
|
+
const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta))
|
|
814
|
+
for (const key of Object.keys(filteredState.rpc.internalMeta)) {
|
|
815
|
+
const targetFuncId = filteredState.rpc.internalMeta[key]
|
|
816
|
+
if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
|
|
817
|
+
delete filteredState.rpc.internalMeta[key]
|
|
818
|
+
filteredState.rpc.internalFiles.delete(key)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const key of Object.keys(filteredState.rpc.exposedMeta)) {
|
|
822
|
+
const targetFuncId = filteredState.rpc.exposedMeta[key]
|
|
823
|
+
if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
|
|
824
|
+
delete filteredState.rpc.exposedMeta[key]
|
|
825
|
+
filteredState.rpc.exposedFiles.delete(key)
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Prune invokedFunctions to match surviving functions
|
|
829
|
+
for (const funcId of filteredState.rpc.invokedFunctions) {
|
|
830
|
+
if (!survivingFuncIds.has(funcId)) {
|
|
831
|
+
filteredState.rpc.invokedFunctions.delete(funcId)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Recompute requiredSchemas based on pruned functions.meta
|
|
837
|
+
if (filteredState.serviceAggregation.usedFunctions.size > 0) {
|
|
838
|
+
const prunedSchemas = new Set<string>()
|
|
839
|
+
for (const funcMeta of Object.values(
|
|
840
|
+
filteredState.functions.meta
|
|
841
|
+
) as Array<{ inputs?: string[]; outputs?: string[] }>) {
|
|
842
|
+
if (funcMeta.inputs?.[0]) prunedSchemas.add(funcMeta.inputs[0])
|
|
843
|
+
if (funcMeta.outputs?.[0]) prunedSchemas.add(funcMeta.outputs[0])
|
|
844
|
+
}
|
|
845
|
+
filteredState.requiredSchemas = prunedSchemas
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// If any surviving function is a non-inline workflow step, the unit needs
|
|
849
|
+
// workflowService + queueService even though the function doesn't use them.
|
|
850
|
+
// Check the ORIGINAL graph meta (before filtering pruned it).
|
|
851
|
+
const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta))
|
|
852
|
+
// Use the snapshot taken before filtering
|
|
853
|
+
for (const graph of Object.values(originalGraphMeta)) {
|
|
854
|
+
if (!graph.nodes) continue
|
|
855
|
+
for (const node of Object.values(graph.nodes)) {
|
|
856
|
+
if (!('rpcName' in node) || !node.rpcName) continue
|
|
857
|
+
const rpcName = node.rpcName as string
|
|
858
|
+
if (!survivingFuncIds.has(rpcName)) continue
|
|
859
|
+
const isInline =
|
|
860
|
+
(node as { options?: { async?: boolean } }).options?.async !== true &&
|
|
861
|
+
graph.inline === true
|
|
862
|
+
if (!isInline) {
|
|
863
|
+
filteredState.serviceAggregation.requiredServices.add('workflowService')
|
|
864
|
+
filteredState.serviceAggregation.requiredServices.add('queueService')
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
634
869
|
// Recalculate requiredServices based on filtered functions/middleware/permissions
|
|
635
870
|
// Need to cast to InspectorState temporarily for aggregateRequiredServices
|
|
636
871
|
const stateForAggregation = filteredState as InspectorState
|
|
@@ -45,6 +45,65 @@ export async function loadAddonFunctionsMeta(
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
// Load addon secrets meta
|
|
49
|
+
try {
|
|
50
|
+
const secretsMetaPath = require.resolve(
|
|
51
|
+
`${decl.package}/.pikku/secrets/pikku-secrets-meta.gen.json`
|
|
52
|
+
)
|
|
53
|
+
const secretsRaw = await readFile(secretsMetaPath, 'utf-8')
|
|
54
|
+
const secretsMeta = JSON.parse(secretsRaw)
|
|
55
|
+
for (const [key, def] of Object.entries<any>(secretsMeta)) {
|
|
56
|
+
const existing = state.secrets.definitions.find(
|
|
57
|
+
(d: any) => d.name === key
|
|
58
|
+
)
|
|
59
|
+
if (!existing) {
|
|
60
|
+
state.secrets.definitions.push(def)
|
|
61
|
+
logger.debug(`Loaded addon secret '${key}' from ${decl.package}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// No secrets meta — that's fine
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load addon variables meta
|
|
69
|
+
try {
|
|
70
|
+
const variablesMetaPath = require.resolve(
|
|
71
|
+
`${decl.package}/.pikku/variables/pikku-variables-meta.gen.json`
|
|
72
|
+
)
|
|
73
|
+
const variablesRaw = await readFile(variablesMetaPath, 'utf-8')
|
|
74
|
+
const variablesMeta = JSON.parse(variablesRaw)
|
|
75
|
+
for (const [key, def] of Object.entries<any>(variablesMeta)) {
|
|
76
|
+
const existing = state.variables.definitions.find(
|
|
77
|
+
(d: any) => d.name === key
|
|
78
|
+
)
|
|
79
|
+
if (!existing) {
|
|
80
|
+
state.variables.definitions.push(def)
|
|
81
|
+
logger.debug(`Loaded addon variable '${key}' from ${decl.package}`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// No variables meta — that's fine
|
|
86
|
+
}
|
|
87
|
+
// Load addon required parent services from pikku-services.gen
|
|
88
|
+
try {
|
|
89
|
+
const servicesGenPath = require.resolve(
|
|
90
|
+
`${decl.package}/.pikku/pikku-services.gen.js`
|
|
91
|
+
)
|
|
92
|
+
const servicesModule = await import(servicesGenPath)
|
|
93
|
+
if (
|
|
94
|
+
servicesModule.requiredParentServices &&
|
|
95
|
+
Array.isArray(servicesModule.requiredParentServices)
|
|
96
|
+
) {
|
|
97
|
+
for (const service of servicesModule.requiredParentServices) {
|
|
98
|
+
state.addonRequiredParentServices.push(service)
|
|
99
|
+
}
|
|
100
|
+
logger.debug(
|
|
101
|
+
`Loaded ${servicesModule.requiredParentServices.length} required parent services for '${namespace}' from ${decl.package}`
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// No services gen — addon may not have requiredParentServices
|
|
106
|
+
}
|
|
48
107
|
} catch (error: any) {
|
|
49
108
|
logger.warn(
|
|
50
109
|
`Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`
|
|
@@ -221,6 +221,80 @@ export function aggregateRequiredServices(
|
|
|
221
221
|
}
|
|
222
222
|
})
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
// 6. Implicit platform services required by wiring types
|
|
226
|
+
// Workflows need workflowService + workflowRunService + schedulerService + queueService.
|
|
227
|
+
// Check workflow definitions, graph meta, AND helper functions (workflowStart:*, etc.)
|
|
228
|
+
// that wrap workflow operations but don't destructure the services.
|
|
229
|
+
const hasWorkflows =
|
|
230
|
+
Object.keys(state.workflows.graphMeta).length > 0 ||
|
|
231
|
+
Object.keys(state.workflows.meta).length > 0 ||
|
|
232
|
+
Object.keys(state.functions.meta).some(
|
|
233
|
+
(id) =>
|
|
234
|
+
id.startsWith('workflowStart:') ||
|
|
235
|
+
id.startsWith('workflowStatus:') ||
|
|
236
|
+
id.startsWith('workflow:')
|
|
237
|
+
)
|
|
238
|
+
if (hasWorkflows) {
|
|
239
|
+
requiredServices.add('workflowService')
|
|
240
|
+
requiredServices.add('workflowRunService')
|
|
241
|
+
requiredServices.add('schedulerService')
|
|
242
|
+
requiredServices.add('queueService')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 6b. Inject synthetic queue workers for workflow graph steps.
|
|
246
|
+
// Each workflow gets an orchestrator queue and per-step queues.
|
|
247
|
+
// Without these, the PikkuWorkflowService constructor can't find
|
|
248
|
+
// per-workflow queue entries and falls back to shared queue names.
|
|
249
|
+
for (const [, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
250
|
+
if (!graph.nodes || !graph.name) continue
|
|
251
|
+
|
|
252
|
+
const toKebab = (s: string) =>
|
|
253
|
+
s
|
|
254
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
255
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
256
|
+
.toLowerCase()
|
|
257
|
+
|
|
258
|
+
// Orchestrator queue
|
|
259
|
+
const orchQueueName = `wf-orchestrator-${toKebab(graph.name)}`
|
|
260
|
+
if (!state.queueWorkers.meta[orchQueueName]) {
|
|
261
|
+
state.queueWorkers.meta[orchQueueName] = {
|
|
262
|
+
name: orchQueueName,
|
|
263
|
+
pikkuFuncId: `pikkuWorkflowOrchestrator:${graph.name}`,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Per-step queues
|
|
268
|
+
for (const node of Object.values(graph.nodes)) {
|
|
269
|
+
if (!('rpcName' in node) || !node.rpcName) continue
|
|
270
|
+
const rpcName = node.rpcName as string
|
|
271
|
+
const stepQueueName = `wf-step-${toKebab(rpcName)}`
|
|
272
|
+
if (!state.queueWorkers.meta[stepQueueName]) {
|
|
273
|
+
state.queueWorkers.meta[stepQueueName] = {
|
|
274
|
+
name: stepQueueName,
|
|
275
|
+
pikkuFuncId: `pikkuWorkflowWorker:${rpcName}`,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// AI agents need aiStorage + aiRunState + agentRunService + aiAgentRunner
|
|
282
|
+
if (Object.keys(state.agents.agentsMeta).length > 0) {
|
|
283
|
+
requiredServices.add('aiStorage')
|
|
284
|
+
requiredServices.add('aiRunState')
|
|
285
|
+
requiredServices.add('agentRunService')
|
|
286
|
+
requiredServices.add('aiAgentRunner')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Channels need eventHub for pub/sub
|
|
290
|
+
if (Object.keys(state.channels.meta).length > 0) {
|
|
291
|
+
requiredServices.add('eventHub')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 7. Services that addons need from the parent project
|
|
295
|
+
for (const service of state.addonRequiredParentServices ?? []) {
|
|
296
|
+
requiredServices.add(service)
|
|
297
|
+
}
|
|
224
298
|
}
|
|
225
299
|
|
|
226
300
|
export function validateSecretOverrides(
|