@pikku/inspector 0.12.74 → 0.12.81
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 +347 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +23 -13
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2207
- package/src/utils/filter-inspector-state.ts +0 -1302
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import {
|
|
3
|
-
getPropertyValue,
|
|
4
|
-
getCommonWireMetaData,
|
|
5
|
-
} from '../utils/get-property-value.js'
|
|
6
|
-
import type { AddWiring } from '../types.js'
|
|
7
|
-
import {
|
|
8
|
-
extractFunctionName,
|
|
9
|
-
makeContextBasedId,
|
|
10
|
-
} from '../utils/extract-function-name.js'
|
|
11
|
-
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
|
|
12
|
-
import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
|
|
13
|
-
import { resolveMiddleware } from '../utils/middleware.js'
|
|
14
|
-
import { extractWireNames } from '../utils/post-process.js'
|
|
15
|
-
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
16
|
-
import { ErrorCode } from '../error-codes.js'
|
|
17
|
-
|
|
18
|
-
export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
|
|
19
|
-
if (!ts.isCallExpression(node)) {
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const args = node.arguments
|
|
24
|
-
const firstArg = args[0]
|
|
25
|
-
const expression = node.expression
|
|
26
|
-
|
|
27
|
-
// Check if the call is to addQueueWorker
|
|
28
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'wireQueueWorker') {
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!firstArg) {
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
37
|
-
const obj = firstArg
|
|
38
|
-
|
|
39
|
-
const name = getPropertyValue(obj, 'name') as string | null
|
|
40
|
-
const { disabled, tags, summary, description, errors } =
|
|
41
|
-
getCommonWireMetaData(obj, 'Queue worker', name, logger, checker)
|
|
42
|
-
|
|
43
|
-
if (disabled) return
|
|
44
|
-
|
|
45
|
-
// --- find the referenced function ---
|
|
46
|
-
const funcInitializer = getPropertyAssignmentInitializer(
|
|
47
|
-
obj,
|
|
48
|
-
'func',
|
|
49
|
-
true,
|
|
50
|
-
checker
|
|
51
|
-
)
|
|
52
|
-
if (!funcInitializer) {
|
|
53
|
-
logger.critical(
|
|
54
|
-
ErrorCode.MISSING_FUNC,
|
|
55
|
-
`No valid 'func' property for queue processor '${name}'.`
|
|
56
|
-
)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const extracted = extractFunctionName(
|
|
61
|
-
funcInitializer,
|
|
62
|
-
checker,
|
|
63
|
-
state.rootDir
|
|
64
|
-
)
|
|
65
|
-
let pikkuFuncId = extracted.pikkuFuncId
|
|
66
|
-
if (pikkuFuncId.startsWith('__temp_') && name) {
|
|
67
|
-
pikkuFuncId = makeContextBasedId('queue', name)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const packageName = ts.isIdentifier(funcInitializer)
|
|
71
|
-
? resolveAddonName(
|
|
72
|
-
funcInitializer,
|
|
73
|
-
checker,
|
|
74
|
-
state.rpc.wireAddonDeclarations
|
|
75
|
-
)
|
|
76
|
-
: null
|
|
77
|
-
|
|
78
|
-
if (!name) {
|
|
79
|
-
logger.critical(
|
|
80
|
-
ErrorCode.MISSING_QUEUE_NAME,
|
|
81
|
-
`No 'name' provided for queue processor function '${pikkuFuncId}'.`
|
|
82
|
-
)
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Register metadata for a func inlined into the wiring (see helper).
|
|
87
|
-
ensureInlineWiringFunction(
|
|
88
|
-
state,
|
|
89
|
-
pikkuFuncId,
|
|
90
|
-
name,
|
|
91
|
-
funcInitializer,
|
|
92
|
-
checker,
|
|
93
|
-
extracted.isHelper
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
// --- resolve middleware ---
|
|
97
|
-
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
98
|
-
|
|
99
|
-
// --- track used functions/middleware for service aggregation ---
|
|
100
|
-
state.serviceAggregation.usedFunctions.add(pikkuFuncId)
|
|
101
|
-
extractWireNames(middleware).forEach((n) =>
|
|
102
|
-
state.serviceAggregation.usedMiddleware.add(n)
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
state.queueWorkers.files.add(node.getSourceFile().fileName)
|
|
106
|
-
state.queueWorkers.meta[name] = {
|
|
107
|
-
pikkuFuncId,
|
|
108
|
-
...(packageName && { packageName }),
|
|
109
|
-
name,
|
|
110
|
-
summary,
|
|
111
|
-
description,
|
|
112
|
-
errors,
|
|
113
|
-
tags,
|
|
114
|
-
middleware,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert'
|
|
2
|
-
import { describe, test } from 'node:test'
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { inspect } from '../inspector.js'
|
|
7
|
-
import type { InspectorLogger, InspectorState } from '../types.js'
|
|
8
|
-
|
|
9
|
-
function makeLogger(): InspectorLogger {
|
|
10
|
-
return {
|
|
11
|
-
debug: () => {},
|
|
12
|
-
info: () => {},
|
|
13
|
-
warn: () => {},
|
|
14
|
-
error: () => {},
|
|
15
|
-
diagnostic: () => {},
|
|
16
|
-
critical: () => {},
|
|
17
|
-
hasCriticalErrors: () => false,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function inspectFiles(
|
|
22
|
-
files: Record<string, string>
|
|
23
|
-
): Promise<{ state: InspectorState; dir: string }> {
|
|
24
|
-
const dir = await mkdtemp(join(tmpdir(), 'pikku-rpc-invocations-test-'))
|
|
25
|
-
const paths: string[] = []
|
|
26
|
-
for (const [name, source] of Object.entries(files)) {
|
|
27
|
-
const path = join(dir, name)
|
|
28
|
-
await writeFile(path, source)
|
|
29
|
-
paths.push(path)
|
|
30
|
-
}
|
|
31
|
-
const state = await inspect(makeLogger(), paths, { rootDir: dir })
|
|
32
|
-
return { state, dir }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('add-rpc-invocations — invokedFunctionsByFile', () => {
|
|
36
|
-
test('body-level rpc.invoke() is attributed to its source file', async () => {
|
|
37
|
-
const { state, dir } = await inspectFiles({
|
|
38
|
-
'caller.ts': `
|
|
39
|
-
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> }
|
|
40
|
-
export async function doWork() {
|
|
41
|
-
return rpc.invoke('console:getSchema')
|
|
42
|
-
}
|
|
43
|
-
`,
|
|
44
|
-
'other.ts': `
|
|
45
|
-
export const unrelated = () => 'no invocations here'
|
|
46
|
-
`,
|
|
47
|
-
})
|
|
48
|
-
try {
|
|
49
|
-
assert.ok(state.rpc.invokedFunctions.has('console:getSchema'))
|
|
50
|
-
const byFile = state.rpc.invokedFunctionsByFile
|
|
51
|
-
assert.strictEqual(byFile.size, 1)
|
|
52
|
-
const [file, invoked] = [...byFile.entries()][0]!
|
|
53
|
-
assert.ok(file.endsWith('caller.ts'))
|
|
54
|
-
assert.deepStrictEqual([...invoked], ['console:getSchema'])
|
|
55
|
-
} finally {
|
|
56
|
-
await rm(dir, { recursive: true, force: true })
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
test('namespaced body invoke joins serviceAggregation.usedFunctions', async () => {
|
|
61
|
-
const { state, dir } = await inspectFiles({
|
|
62
|
-
'caller.ts': `
|
|
63
|
-
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> }
|
|
64
|
-
export async function doWork() {
|
|
65
|
-
await rpc.invoke('ext:goodbye')
|
|
66
|
-
return rpc.invoke('localHelper')
|
|
67
|
-
}
|
|
68
|
-
`,
|
|
69
|
-
})
|
|
70
|
-
try {
|
|
71
|
-
assert.ok(state.serviceAggregation.usedFunctions.has('ext:goodbye'))
|
|
72
|
-
assert.ok(!state.serviceAggregation.usedFunctions.has('localHelper'))
|
|
73
|
-
} finally {
|
|
74
|
-
await rm(dir, { recursive: true, force: true })
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('multiple invocations in one file accumulate under that file', async () => {
|
|
79
|
-
const { state, dir } = await inspectFiles({
|
|
80
|
-
'caller.ts': `
|
|
81
|
-
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> }
|
|
82
|
-
export async function doWork() {
|
|
83
|
-
await rpc.invoke('console:getSchema')
|
|
84
|
-
return rpc.invoke('listTasks')
|
|
85
|
-
}
|
|
86
|
-
`,
|
|
87
|
-
})
|
|
88
|
-
try {
|
|
89
|
-
const invoked = [...state.rpc.invokedFunctionsByFile.values()][0]!
|
|
90
|
-
assert.deepStrictEqual([...invoked].sort(), [
|
|
91
|
-
'console:getSchema',
|
|
92
|
-
'listTasks',
|
|
93
|
-
])
|
|
94
|
-
} finally {
|
|
95
|
-
await rm(dir, { recursive: true, force: true })
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('wiring-level ref() lands in invokedFunctions but NOT in the by-file map', async () => {
|
|
100
|
-
const { state, dir } = await inspectFiles({
|
|
101
|
-
'wiring.ts': `
|
|
102
|
-
declare const ref: (name: string) => unknown
|
|
103
|
-
export const routes = { stream: { func: ref('console:streamWorkflowRun') } }
|
|
104
|
-
`,
|
|
105
|
-
})
|
|
106
|
-
try {
|
|
107
|
-
assert.ok(state.rpc.invokedFunctions.has('console:streamWorkflowRun'))
|
|
108
|
-
assert.strictEqual(state.rpc.invokedFunctionsByFile.size, 0)
|
|
109
|
-
} finally {
|
|
110
|
-
await rm(dir, { recursive: true, force: true })
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('a non-null assertion on the receiver is still an invocation', async () => {
|
|
115
|
-
const { state, dir } = await inspectFiles({
|
|
116
|
-
'caller.ts': `
|
|
117
|
-
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> } | undefined
|
|
118
|
-
export async function doWork() {
|
|
119
|
-
return rpc!.invoke('executeVirtualUserRun')
|
|
120
|
-
}
|
|
121
|
-
`,
|
|
122
|
-
})
|
|
123
|
-
try {
|
|
124
|
-
assert.ok(state.rpc.invokedFunctions.has('executeVirtualUserRun'))
|
|
125
|
-
const invoked = [...state.rpc.invokedFunctionsByFile.values()][0]!
|
|
126
|
-
assert.deepStrictEqual([...invoked], ['executeVirtualUserRun'])
|
|
127
|
-
} finally {
|
|
128
|
-
await rm(dir, { recursive: true, force: true })
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test('a parenthesised receiver is still an invocation', async () => {
|
|
133
|
-
const { state, dir } = await inspectFiles({
|
|
134
|
-
'caller.ts': `
|
|
135
|
-
declare const rpc: { invoke: (name: string, data?: unknown) => Promise<unknown> } | undefined
|
|
136
|
-
export async function doWork() {
|
|
137
|
-
return (rpc!).invoke('listTasks')
|
|
138
|
-
}
|
|
139
|
-
`,
|
|
140
|
-
})
|
|
141
|
-
try {
|
|
142
|
-
assert.ok(state.rpc.invokedFunctions.has('listTasks'))
|
|
143
|
-
} finally {
|
|
144
|
-
await rm(dir, { recursive: true, force: true })
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test('invoke on some other receiver is left alone', async () => {
|
|
149
|
-
const { state, dir } = await inspectFiles({
|
|
150
|
-
'caller.ts': `
|
|
151
|
-
declare const client: { invoke: (name: string, data?: unknown) => Promise<unknown> } | undefined
|
|
152
|
-
export async function doWork() {
|
|
153
|
-
return client!.invoke('notAnRpc')
|
|
154
|
-
}
|
|
155
|
-
`,
|
|
156
|
-
})
|
|
157
|
-
try {
|
|
158
|
-
assert.ok(!state.rpc.invokedFunctions.has('notAnRpc'))
|
|
159
|
-
} finally {
|
|
160
|
-
await rm(dir, { recursive: true, force: true })
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
describe('add-rpc-invocations — invokedAgentsByFile', () => {
|
|
166
|
-
test('a body-level runAgent() is attributed to its source file', async () => {
|
|
167
|
-
const { state, dir } = await inspectFiles({
|
|
168
|
-
'caller.ts': `
|
|
169
|
-
declare const runAgent: (name: string, input: unknown) => Promise<unknown>
|
|
170
|
-
export async function askTheHouse() {
|
|
171
|
-
return runAgent('houseAssistant', { message: 'hi' })
|
|
172
|
-
}
|
|
173
|
-
`,
|
|
174
|
-
'other.ts': `
|
|
175
|
-
export const unrelated = () => 'no agents here'
|
|
176
|
-
`,
|
|
177
|
-
})
|
|
178
|
-
try {
|
|
179
|
-
const byFile = state.agents.invokedAgentsByFile
|
|
180
|
-
assert.strictEqual(byFile.size, 1)
|
|
181
|
-
const [file, agents] = [...byFile.entries()][0]!
|
|
182
|
-
assert.ok(file.endsWith('caller.ts'))
|
|
183
|
-
assert.deepStrictEqual([...agents], ['houseAssistant'])
|
|
184
|
-
} finally {
|
|
185
|
-
await rm(dir, { recursive: true, force: true })
|
|
186
|
-
}
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test('rpc.agent.run() is an agent invocation too', async () => {
|
|
190
|
-
const { state, dir } = await inspectFiles({
|
|
191
|
-
'caller.ts': `
|
|
192
|
-
declare const rpc: { agent: { run: (name: string, input: unknown) => Promise<unknown> } }
|
|
193
|
-
export async function askTheHouse() {
|
|
194
|
-
return rpc.agent.run('houseAssistant', { message: 'hi' })
|
|
195
|
-
}
|
|
196
|
-
`,
|
|
197
|
-
})
|
|
198
|
-
try {
|
|
199
|
-
const agents = [...state.agents.invokedAgentsByFile.values()][0]!
|
|
200
|
-
assert.deepStrictEqual([...agents], ['houseAssistant'])
|
|
201
|
-
} finally {
|
|
202
|
-
await rm(dir, { recursive: true, force: true })
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
test('a non-null assertion on the rpc receiver is still an invocation', async () => {
|
|
207
|
-
const { state, dir } = await inspectFiles({
|
|
208
|
-
'caller.ts': `
|
|
209
|
-
declare const rpc: { agent: { stream: (name: string, input: unknown) => Promise<unknown> } } | undefined
|
|
210
|
-
export async function askTheHouse() {
|
|
211
|
-
return rpc!.agent.stream('houseAssistant', { message: 'hi' })
|
|
212
|
-
}
|
|
213
|
-
`,
|
|
214
|
-
})
|
|
215
|
-
try {
|
|
216
|
-
const agents = [...state.agents.invokedAgentsByFile.values()][0]!
|
|
217
|
-
assert.deepStrictEqual([...agents], ['houseAssistant'])
|
|
218
|
-
} finally {
|
|
219
|
-
await rm(dir, { recursive: true, force: true })
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
test('multiple agent calls in one file accumulate under that file', async () => {
|
|
224
|
-
const { state, dir } = await inspectFiles({
|
|
225
|
-
'caller.ts': `
|
|
226
|
-
declare const runAgent: (name: string, input: unknown) => Promise<unknown>
|
|
227
|
-
declare const rpc: { agent: { run: (name: string, input: unknown) => Promise<unknown> } }
|
|
228
|
-
export async function askEveryone() {
|
|
229
|
-
await runAgent('houseAssistant', {})
|
|
230
|
-
return rpc.agent.run('gardenAssistant', {})
|
|
231
|
-
}
|
|
232
|
-
`,
|
|
233
|
-
})
|
|
234
|
-
try {
|
|
235
|
-
const agents = [...state.agents.invokedAgentsByFile.values()][0]!
|
|
236
|
-
assert.deepStrictEqual([...agents].sort(), [
|
|
237
|
-
'gardenAssistant',
|
|
238
|
-
'houseAssistant',
|
|
239
|
-
])
|
|
240
|
-
} finally {
|
|
241
|
-
await rm(dir, { recursive: true, force: true })
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test('a dynamic agent name is warned about, not recorded', async () => {
|
|
246
|
-
const warnings: string[] = []
|
|
247
|
-
const dir = await mkdtemp(join(tmpdir(), 'pikku-agent-invocations-test-'))
|
|
248
|
-
const path = join(dir, 'caller.ts')
|
|
249
|
-
await writeFile(
|
|
250
|
-
path,
|
|
251
|
-
`
|
|
252
|
-
declare const runAgent: (name: string, input: unknown) => Promise<unknown>
|
|
253
|
-
declare const suffix: string
|
|
254
|
-
export async function askTheHouse() {
|
|
255
|
-
return runAgent(\`house-\${suffix}\`, {})
|
|
256
|
-
}
|
|
257
|
-
`
|
|
258
|
-
)
|
|
259
|
-
const logger = makeLogger()
|
|
260
|
-
logger.warn = (message: string) => {
|
|
261
|
-
warnings.push(message)
|
|
262
|
-
}
|
|
263
|
-
try {
|
|
264
|
-
const state = await inspect(logger, [path], { rootDir: dir })
|
|
265
|
-
assert.strictEqual(state.agents.invokedAgentsByFile.size, 0)
|
|
266
|
-
assert.ok(warnings.some((w) => w.includes('dynamic agent invocation')))
|
|
267
|
-
} finally {
|
|
268
|
-
await rm(dir, { recursive: true, force: true })
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
test('run() on some other receiver is left alone', async () => {
|
|
273
|
-
const { state, dir } = await inspectFiles({
|
|
274
|
-
'caller.ts': `
|
|
275
|
-
declare const client: { agent: { run: (name: string, input: unknown) => Promise<unknown> } }
|
|
276
|
-
export async function doWork() {
|
|
277
|
-
return client.agent.run('notAnAgentCall', {})
|
|
278
|
-
}
|
|
279
|
-
`,
|
|
280
|
-
})
|
|
281
|
-
try {
|
|
282
|
-
assert.strictEqual(state.agents.invokedAgentsByFile.size, 0)
|
|
283
|
-
} finally {
|
|
284
|
-
await rm(dir, { recursive: true, force: true })
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
})
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
3
|
-
import { ErrorCode } from '../error-codes.js'
|
|
4
|
-
|
|
5
|
-
function hasTypeCast(node: ts.Node): boolean {
|
|
6
|
-
return ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function outerParent(node: ts.Node): ts.Node {
|
|
10
|
-
let p = node.parent
|
|
11
|
-
while (p && (ts.isAwaitExpression(p) || ts.isParenthesizedExpression(p))) {
|
|
12
|
-
p = p.parent
|
|
13
|
-
}
|
|
14
|
-
return p
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function findCastArg(
|
|
18
|
-
args: ts.NodeArray<ts.Expression>
|
|
19
|
-
): ts.Expression | undefined {
|
|
20
|
-
return args.find(hasTypeCast)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The `rpc` in `rpc.invoke(...)`, seen through whatever the author wrote to
|
|
25
|
-
* satisfy the type checker.
|
|
26
|
-
*
|
|
27
|
-
* `rpc` is optional on the wire, so a caller that knows it is there reaches for
|
|
28
|
-
* `rpc!.invoke(...)` — and a non-null assertion is a node of its own, not the
|
|
29
|
-
* identifier underneath it. Matching only the bare identifier meant those calls
|
|
30
|
-
* were never recorded as invocations, which is silent rather than loud: the
|
|
31
|
-
* target is not `expose: true`, so nothing else puts it in the registry, and
|
|
32
|
-
* the dispatch fails with "Function not found" at run time on a call site that
|
|
33
|
-
* type-checked. Parentheses are unwrapped for the same reason.
|
|
34
|
-
*/
|
|
35
|
-
function invocationReceiver(node: ts.Expression): ts.Expression {
|
|
36
|
-
let inner = node
|
|
37
|
-
while (ts.isNonNullExpression(inner) || ts.isParenthesizedExpression(inner)) {
|
|
38
|
-
inner = inner.expression
|
|
39
|
-
}
|
|
40
|
-
return inner
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Helper to extract namespace from a namespaced function reference like 'ext:hello'
|
|
45
|
-
*/
|
|
46
|
-
function extractNamespace(functionRef: string): string | null {
|
|
47
|
-
const colonIndex = functionRef.indexOf(':')
|
|
48
|
-
if (colonIndex !== -1) {
|
|
49
|
-
return functionRef.substring(0, colonIndex)
|
|
50
|
-
}
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function agentInvocationName(
|
|
55
|
-
expression: ts.Expression
|
|
56
|
-
): 'runAgent' | 'streamAgent' | 'rpc.agent.run' | 'rpc.agent.stream' | null {
|
|
57
|
-
if (ts.isIdentifier(expression)) {
|
|
58
|
-
if (expression.text === 'runAgent') return 'runAgent'
|
|
59
|
-
if (expression.text === 'streamAgent') return 'streamAgent'
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
if (!ts.isPropertyAccessExpression(expression)) return null
|
|
63
|
-
const method = expression.name.text
|
|
64
|
-
if (method !== 'run' && method !== 'stream') return null
|
|
65
|
-
const agentReceiver = invocationReceiver(expression.expression)
|
|
66
|
-
if (
|
|
67
|
-
!ts.isPropertyAccessExpression(agentReceiver) ||
|
|
68
|
-
agentReceiver.name.text !== 'agent'
|
|
69
|
-
) {
|
|
70
|
-
return null
|
|
71
|
-
}
|
|
72
|
-
const rpcReceiver = invocationReceiver(agentReceiver.expression)
|
|
73
|
-
const isRpc = ts.isIdentifier(rpcReceiver)
|
|
74
|
-
? rpcReceiver.text === 'rpc'
|
|
75
|
-
: ts.isPropertyAccessExpression(rpcReceiver) &&
|
|
76
|
-
rpcReceiver.name.text === 'rpc'
|
|
77
|
-
if (!isRpc) return null
|
|
78
|
-
return method === 'run' ? 'rpc.agent.run' : 'rpc.agent.stream'
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Scan for rpc.invoke() calls to track which functions are actually being invoked
|
|
83
|
-
* Also detects addon usage via:
|
|
84
|
-
* - Namespaced calls: rpc.invoke('namespace:function')
|
|
85
|
-
* - Addon helper: addon('namespace:function')
|
|
86
|
-
*/
|
|
87
|
-
export function addRPCInvocations(
|
|
88
|
-
node: ts.Node,
|
|
89
|
-
state: InspectorState,
|
|
90
|
-
logger: InspectorLogger
|
|
91
|
-
) {
|
|
92
|
-
// Look for call expressions: addon('ext:hello') or rpc.invoke('...')
|
|
93
|
-
if (ts.isCallExpression(node)) {
|
|
94
|
-
const { expression, arguments: args } = node
|
|
95
|
-
|
|
96
|
-
// Check for ref('name') calls
|
|
97
|
-
if (ts.isIdentifier(expression) && expression.text === 'ref') {
|
|
98
|
-
const [firstArg] = args
|
|
99
|
-
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
100
|
-
const functionRef = firstArg.text
|
|
101
|
-
logger.debug(`• Found addon() call: ${functionRef}`)
|
|
102
|
-
state.rpc.invokedFunctions.add(functionRef)
|
|
103
|
-
|
|
104
|
-
const namespace = extractNamespace(functionRef)
|
|
105
|
-
if (namespace) {
|
|
106
|
-
logger.debug(` → Addon detected: ${namespace}`)
|
|
107
|
-
state.rpc.usedAddons.add(namespace)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check for workflow('...'), workflowStart('...'), workflowRun('...'), workflowStatus('...'), graphStart('...') calls
|
|
113
|
-
if (
|
|
114
|
-
ts.isIdentifier(expression) &&
|
|
115
|
-
(expression.text === 'workflow' ||
|
|
116
|
-
expression.text === 'workflowStart' ||
|
|
117
|
-
expression.text === 'workflowRun' ||
|
|
118
|
-
expression.text === 'workflowStatus' ||
|
|
119
|
-
expression.text === 'graphStart')
|
|
120
|
-
) {
|
|
121
|
-
const [firstArg] = args
|
|
122
|
-
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
123
|
-
const workflowName = firstArg.text
|
|
124
|
-
logger.debug(`• Found ${expression.text}() call: ${workflowName}`)
|
|
125
|
-
state.workflows.invokedWorkflows.add(workflowName)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const agentCall = agentInvocationName(expression)
|
|
130
|
-
if (agentCall) {
|
|
131
|
-
const [firstArg] = args
|
|
132
|
-
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
133
|
-
const agentName = firstArg.text
|
|
134
|
-
logger.debug(`• Found ${agentCall}() call: ${agentName}`)
|
|
135
|
-
const sourceFileName = node.getSourceFile().fileName
|
|
136
|
-
let byFile = state.agents.invokedAgentsByFile.get(sourceFileName)
|
|
137
|
-
if (!byFile) {
|
|
138
|
-
byFile = new Set()
|
|
139
|
-
state.agents.invokedAgentsByFile.set(sourceFileName, byFile)
|
|
140
|
-
}
|
|
141
|
-
byFile.add(agentName)
|
|
142
|
-
} else if (
|
|
143
|
-
firstArg &&
|
|
144
|
-
(ts.isTemplateExpression(firstArg) ||
|
|
145
|
-
ts.isNoSubstitutionTemplateLiteral(firstArg))
|
|
146
|
-
) {
|
|
147
|
-
logger.warn(`• Found dynamic agent invocation: ${firstArg.getText()}`)
|
|
148
|
-
logger.warn(
|
|
149
|
-
`\tYou can only use string literals for agent names, with ' or " and not \``
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check for rpc.invoke('...') calls
|
|
155
|
-
const invokeReceiver = ts.isPropertyAccessExpression(expression)
|
|
156
|
-
? invocationReceiver(expression.expression)
|
|
157
|
-
: undefined
|
|
158
|
-
if (
|
|
159
|
-
ts.isPropertyAccessExpression(expression) &&
|
|
160
|
-
expression.name.text === 'invoke' &&
|
|
161
|
-
invokeReceiver &&
|
|
162
|
-
ts.isIdentifier(invokeReceiver) &&
|
|
163
|
-
invokeReceiver.text === 'rpc'
|
|
164
|
-
) {
|
|
165
|
-
// Skip PKU940 for generated files — they may contain intentional casts
|
|
166
|
-
// (e.g. the paginated useInfiniteQuery hook in pikku-react-query.gen.ts).
|
|
167
|
-
const sourceFileName = node.getSourceFile().fileName
|
|
168
|
-
const isGenerated =
|
|
169
|
-
sourceFileName.endsWith('.gen.ts') || sourceFileName.endsWith('.gen.js')
|
|
170
|
-
if (!isGenerated) {
|
|
171
|
-
if (hasTypeCast(outerParent(node))) {
|
|
172
|
-
logger.critical(
|
|
173
|
-
ErrorCode.RPC_INVOCATION_TYPE_CAST,
|
|
174
|
-
`rpc.invoke() result is type-cast — remove the 'as' expression and rely on Pikku's generated types`
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const castArg = findCastArg(args)
|
|
179
|
-
if (castArg) {
|
|
180
|
-
logger.critical(
|
|
181
|
-
ErrorCode.RPC_INVOCATION_TYPE_CAST,
|
|
182
|
-
`rpc.invoke() has a type cast on an argument — remove the 'as' expression and rely on Pikku's generated types`
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const [firstArg] = args
|
|
188
|
-
if (firstArg) {
|
|
189
|
-
if (ts.isStringLiteral(firstArg)) {
|
|
190
|
-
const functionRef = firstArg.text
|
|
191
|
-
logger.debug(`• Found RPC invocation: ${functionRef}`)
|
|
192
|
-
state.rpc.invokedFunctions.add(functionRef)
|
|
193
|
-
|
|
194
|
-
let byFile = state.rpc.invokedFunctionsByFile.get(sourceFileName)
|
|
195
|
-
if (!byFile) {
|
|
196
|
-
byFile = new Set()
|
|
197
|
-
state.rpc.invokedFunctionsByFile.set(sourceFileName, byFile)
|
|
198
|
-
}
|
|
199
|
-
byFile.add(functionRef)
|
|
200
|
-
|
|
201
|
-
const namespace = extractNamespace(functionRef)
|
|
202
|
-
if (namespace) {
|
|
203
|
-
logger.debug(` → Addon detected: ${namespace}`)
|
|
204
|
-
state.rpc.usedAddons.add(namespace)
|
|
205
|
-
state.serviceAggregation.usedFunctions.add(functionRef)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Handle template literals like `function-${name}`
|
|
209
|
-
else if (
|
|
210
|
-
ts.isTemplateExpression(firstArg) ||
|
|
211
|
-
ts.isNoSubstitutionTemplateLiteral(firstArg)
|
|
212
|
-
) {
|
|
213
|
-
logger.warn(`• Found dynamic RPC invocation: ${firstArg.getText()}`)
|
|
214
|
-
logger.warn(
|
|
215
|
-
`\tYou can only use string literals for RPC function names, with ' or " and not \``
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|