@pikku/inspector 0.12.59 → 0.12.61
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 +373 -0
- package/OPTIMIZATION-PLAN.md +19 -19
- package/dist/add/add-agent-scorer.d.ts +2 -0
- package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
- package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
- package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +5 -0
- package/dist/add/add-functions.js +10 -1
- package/dist/add/add-http-route.js +18 -18
- package/dist/add/add-keyed-wiring.d.ts +2 -0
- package/dist/add/add-keyed-wiring.js +3 -1
- package/dist/add/add-middleware.js +5 -5
- package/dist/add/add-personas.js +3 -0
- package/dist/add/add-secret.js +1 -0
- package/dist/add/add-variable.js +1 -0
- package/dist/add/add-wire-remote-addon.js +2 -1
- package/dist/add/add-workflow-graph.js +63 -6
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/inspector.js +13 -3
- package/dist/types.d.ts +10 -5
- package/dist/utils/annotate-http-route-auth.js +2 -1
- package/dist/utils/check-secret-sinks.js +2 -1
- package/dist/utils/compute-required-schemas.d.ts +1 -1
- package/dist/utils/contract-hashes.d.ts +2 -1
- package/dist/utils/contract-hashes.js +1 -1
- package/dist/utils/extract-function-name.js +1 -1
- package/dist/utils/extract-services.d.ts +1 -1
- package/dist/utils/extract-surface-usage.d.ts +29 -0
- package/dist/utils/extract-surface-usage.js +97 -0
- package/dist/utils/filter-inspector-state.js +53 -16
- package/dist/utils/filter-utils.d.ts +1 -1
- package/dist/utils/get-property-value.js +42 -3
- package/dist/utils/load-addon-functions-meta.js +31 -0
- package/dist/utils/middleware.d.ts +2 -2
- package/dist/utils/middleware.js +5 -5
- package/dist/utils/permissions.d.ts +1 -1
- package/dist/utils/post-process.d.ts +28 -8
- package/dist/utils/post-process.js +83 -18
- package/dist/utils/resolve-deploy-target.d.ts +1 -1
- package/dist/utils/resolve-function-meta.d.ts +1 -1
- package/dist/utils/resolve-function-types.d.ts +1 -1
- package/dist/utils/resolve-versions.js +1 -1
- package/dist/utils/schema-generator.d.ts +1 -1
- package/dist/utils/serialize-inspector-state.d.ts +4 -3
- package/dist/utils/serialize-inspector-state.js +6 -4
- package/dist/utils/serialize-openapi-json.d.ts +1 -1
- package/dist/utils/validate-auth-sessionless.js +2 -1
- package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
- package/dist/visit.js +4 -4
- package/package.json +10 -10
- package/run-tests.sh +0 -0
- package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
- package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
- package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
- package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
- package/src/add/add-auth.ts +2 -2
- package/src/add/add-cli.ts +6 -0
- package/src/add/add-credential.ts +3 -6
- package/src/add/add-file-with-factory.ts +2 -6
- package/src/add/add-functions.ts +23 -25
- package/src/add/add-http-route-ref.test.ts +149 -0
- package/src/add/add-http-route.ts +25 -28
- package/src/add/add-keyed-wiring.ts +8 -7
- package/src/add/add-middleware.ts +7 -5
- package/src/add/add-permission.ts +1 -1
- package/src/add/add-personas.test.ts +24 -0
- package/src/add/add-personas.ts +3 -1
- package/src/add/add-scenario-step.test.ts +35 -0
- package/src/add/add-scope.test.ts +4 -4
- package/src/add/add-scope.ts +4 -8
- package/src/add/add-secret.ts +1 -0
- package/src/add/add-system-role.test.ts +8 -8
- package/src/add/add-system-role.ts +2 -4
- package/src/add/add-variable.ts +1 -0
- package/src/add/add-wire-remote-addon.ts +4 -1
- package/src/add/add-workflow-graph-foreach.test.ts +174 -0
- package/src/add/add-workflow-graph.ts +78 -6
- package/src/add/add-workflow-sleep-expression.test.ts +3 -4
- package/src/add/addon-contracts.test.ts +1 -1
- package/src/add/config-value-metadata.test.ts +94 -0
- package/src/add/secret-sink-check.test.ts +4 -1
- package/src/error-codes.ts +3 -1
- package/src/index.ts +3 -4
- package/src/inspector.ts +20 -3
- package/src/types.ts +10 -7
- package/src/utils/annotate-http-route-auth.test.ts +5 -7
- package/src/utils/annotate-http-route-auth.ts +2 -1
- package/src/utils/check-secret-sinks.ts +4 -1
- package/src/utils/compute-required-schemas.ts +1 -1
- package/src/utils/contract-hashes.test.ts +1 -1
- package/src/utils/contract-hashes.ts +3 -2
- package/src/utils/extract-function-name.ts +1 -1
- package/src/utils/extract-services.ts +4 -2
- package/src/utils/extract-surface-usage.test.ts +147 -0
- package/src/utils/extract-surface-usage.ts +121 -0
- package/src/utils/filter-inspector-state.test.ts +138 -12
- package/src/utils/filter-inspector-state.ts +82 -21
- package/src/utils/filter-utils.ts +1 -1
- package/src/utils/get-property-value.ts +43 -5
- package/src/utils/load-addon-functions-meta.ts +40 -0
- package/src/utils/middleware.test.ts +23 -23
- package/src/utils/middleware.ts +6 -6
- package/src/utils/permissions.ts +1 -1
- package/src/utils/post-process.test.ts +68 -11
- package/src/utils/post-process.ts +91 -28
- package/src/utils/resolve-deploy-target.ts +1 -1
- package/src/utils/resolve-function-meta.ts +1 -1
- package/src/utils/resolve-function-types.ts +1 -1
- package/src/utils/resolve-versions.ts +1 -1
- package/src/utils/schema-generator.ts +2 -1
- package/src/utils/serialize-inspector-state.ts +10 -7
- package/src/utils/serialize-mcp-json.ts +1 -1
- package/src/utils/serialize-openapi-json.ts +1 -1
- package/src/utils/validate-auth-sessionless.ts +2 -1
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
- package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
- package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
- package/src/visit.ts +4 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add/add-ai-scorer.d.ts +0 -2
- package/dist/utils/secret-brokers.d.ts +0 -2
- package/dist/utils/secret-brokers.js +0 -9
|
@@ -3,10 +3,23 @@ import type {
|
|
|
3
3
|
InspectorFilters,
|
|
4
4
|
InspectorLogger,
|
|
5
5
|
} from '../types.js'
|
|
6
|
-
import type { PikkuWiringTypes } from '@pikku/core'
|
|
7
|
-
import { parseVersionedId } from '@pikku/core'
|
|
6
|
+
import type { PikkuWiringTypes } from '@pikku/core/types'
|
|
7
|
+
import { parseVersionedId } from '@pikku/core/utils'
|
|
8
|
+
import { makeContextBasedId } from './extract-function-name.js'
|
|
8
9
|
import { aggregateRequiredServices } from './post-process.js'
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
IncompatibleDeployTargetError,
|
|
12
|
+
resolveDeployTarget,
|
|
13
|
+
} from './resolve-deploy-target.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The deploy filter, asked about one function at a time rather than handed over
|
|
17
|
+
* as a set, so that a function's own deploy conflict is raised only if this app
|
|
18
|
+
* turns out to wire it.
|
|
19
|
+
*/
|
|
20
|
+
type DeployFilter = {
|
|
21
|
+
has(pikkuFuncId: string): boolean
|
|
22
|
+
}
|
|
10
23
|
|
|
11
24
|
// Module-level Set to track warned groups across multiple filter calls
|
|
12
25
|
const globalWarnedGroups = new Set<string>()
|
|
@@ -97,6 +110,13 @@ function matchesFilters(
|
|
|
97
110
|
meta: {
|
|
98
111
|
type: PikkuWiringTypes
|
|
99
112
|
name: string
|
|
113
|
+
/**
|
|
114
|
+
* Further ids the `--names` filter may address this wiring by. An HTTP
|
|
115
|
+
* route wired with `ref('ns:fn')` carries the addon's own function id as
|
|
116
|
+
* its name, so its `http:<method>:<route>` wiring id is the only id that
|
|
117
|
+
* names this route rather than every wiring onto that addon function.
|
|
118
|
+
*/
|
|
119
|
+
altNames?: string[]
|
|
100
120
|
tags?: string[]
|
|
101
121
|
filePath?: string
|
|
102
122
|
httpRoute?: string
|
|
@@ -105,9 +125,10 @@ function matchesFilters(
|
|
|
105
125
|
},
|
|
106
126
|
logger: InspectorLogger,
|
|
107
127
|
warnedGroups?: Set<string>,
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
128
|
+
// Kept pikkuFuncIds under the deploy filter; null when the filter is
|
|
129
|
+
// inactive. Asked about one id at a time, which is what lets a conflict in an
|
|
130
|
+
// addon's own metadata surface only for a function this app actually wires.
|
|
131
|
+
keptByDeploy?: DeployFilter | null
|
|
111
132
|
): boolean {
|
|
112
133
|
// If no filters, allow everything
|
|
113
134
|
if (Object.keys(filters).length === 0) return true
|
|
@@ -188,10 +209,15 @@ function matchesFilters(
|
|
|
188
209
|
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
|
-
const
|
|
212
|
+
const candidateNames = [meta.name, ...(meta.altNames ?? [])]
|
|
192
213
|
const matchesNamePattern = (pattern: string) =>
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
candidateNames.some((candidate) => {
|
|
215
|
+
const { baseName } = parseVersionedId(candidate)
|
|
216
|
+
return (
|
|
217
|
+
matchesWildcard(candidate, pattern) ||
|
|
218
|
+
(baseName !== candidate && matchesWildcard(baseName, pattern))
|
|
219
|
+
)
|
|
220
|
+
})
|
|
195
221
|
|
|
196
222
|
// Check name include filter (match against both full ID and base name for versioned functions)
|
|
197
223
|
if (filters.names && filters.names.length > 0) {
|
|
@@ -330,7 +356,7 @@ export function filterInspectorState(
|
|
|
330
356
|
// Precompute kept-function set for the deploy filter (if active).
|
|
331
357
|
// resolveDeployTarget throws IncompatibleDeployTargetError when an
|
|
332
358
|
// explicit deploy: 'serverless' clashes with serverlessIncompatible.
|
|
333
|
-
let keptByDeploy:
|
|
359
|
+
let keptByDeploy: DeployFilter | null = null
|
|
334
360
|
if (
|
|
335
361
|
(filters.target && filters.target.length > 0) ||
|
|
336
362
|
(filters.excludeTarget && filters.excludeTarget.length > 0)
|
|
@@ -341,17 +367,56 @@ export function filterInspectorState(
|
|
|
341
367
|
: null
|
|
342
368
|
const incompatible = new Set(filters.serverlessIncompatible ?? [])
|
|
343
369
|
const defaultTarget = filters.defaultTarget ?? 'serverless'
|
|
344
|
-
|
|
345
|
-
for
|
|
370
|
+
const kept = new Set<string>()
|
|
371
|
+
// An addon publishes metadata for everything it exports, so a conflict in a
|
|
372
|
+
// function this app never wires is not this app's build error. Holding it
|
|
373
|
+
// here and raising it from `has` reports it at the one place the app is
|
|
374
|
+
// known to depend on that function.
|
|
375
|
+
const conflicts = new Map<string, IncompatibleDeployTargetError>()
|
|
376
|
+
keptByDeploy = {
|
|
377
|
+
has: (funcId: string) => {
|
|
378
|
+
const conflict = conflicts.get(funcId)
|
|
379
|
+
if (conflict) throw conflict
|
|
380
|
+
return kept.has(funcId)
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
const keepByTarget = (
|
|
384
|
+
funcId: string,
|
|
385
|
+
funcMeta: unknown,
|
|
386
|
+
incompatibleServices: Set<string>
|
|
387
|
+
) => {
|
|
346
388
|
const target = resolveDeployTarget(
|
|
347
389
|
funcMeta as any,
|
|
348
|
-
|
|
390
|
+
incompatibleServices,
|
|
349
391
|
funcId,
|
|
350
392
|
defaultTarget
|
|
351
393
|
)
|
|
352
|
-
if (allowed && !allowed.has(target))
|
|
353
|
-
if (excluded && excluded.has(target))
|
|
354
|
-
|
|
394
|
+
if (allowed && !allowed.has(target)) return
|
|
395
|
+
if (excluded && excluded.has(target)) return
|
|
396
|
+
kept.add(funcId)
|
|
397
|
+
}
|
|
398
|
+
for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
|
|
399
|
+
keepByTarget(funcId, funcMeta, incompatible)
|
|
400
|
+
}
|
|
401
|
+
// A wiring onto an addon function records the addon's own id, so the
|
|
402
|
+
// addon's published metadata is the only place its deploy target can be
|
|
403
|
+
// read from. Its serverlessIncompatible services are namespace-scoped —
|
|
404
|
+
// a service name means whatever the addon that declared it means.
|
|
405
|
+
for (const [namespace, addonMeta] of Object.entries(
|
|
406
|
+
state.addonFunctions ?? {}
|
|
407
|
+
)) {
|
|
408
|
+
const addonIncompatible = new Set(
|
|
409
|
+
state.addonServerlessIncompatible?.get(namespace) ?? []
|
|
410
|
+
)
|
|
411
|
+
for (const [funcName, funcMeta] of Object.entries(addonMeta)) {
|
|
412
|
+
const funcId = `${namespace}:${funcName}`
|
|
413
|
+
try {
|
|
414
|
+
keepByTarget(funcId, funcMeta, addonIncompatible)
|
|
415
|
+
} catch (e) {
|
|
416
|
+
if (!(e instanceof IncompatibleDeployTargetError)) throw e
|
|
417
|
+
conflicts.set(funcId, e)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
355
420
|
}
|
|
356
421
|
}
|
|
357
422
|
|
|
@@ -459,6 +524,7 @@ export function filterInspectorState(
|
|
|
459
524
|
{
|
|
460
525
|
type: 'http' as PikkuWiringTypes,
|
|
461
526
|
name: routeMeta.pikkuFuncId, // Use function name, not route
|
|
527
|
+
altNames: [makeContextBasedId('http', method, routeMeta.route)],
|
|
462
528
|
tags: routeMeta.tags,
|
|
463
529
|
filePath,
|
|
464
530
|
httpRoute: routeMeta.route,
|
|
@@ -478,11 +544,6 @@ export function filterInspectorState(
|
|
|
478
544
|
filteredState.serviceAggregation.usedFunctions.add(
|
|
479
545
|
routeMeta.pikkuFuncId
|
|
480
546
|
)
|
|
481
|
-
if (routeMeta.refTarget) {
|
|
482
|
-
filteredState.serviceAggregation.usedFunctions.add(
|
|
483
|
-
routeMeta.refTarget
|
|
484
|
-
)
|
|
485
|
-
}
|
|
486
547
|
// For workflow/agent routes, also add the base name
|
|
487
548
|
// so the workflow/agent definition survives pruning
|
|
488
549
|
const colonIdx = routeMeta.pikkuFuncId.indexOf(':')
|
|
@@ -127,6 +127,46 @@ export const assertStringLiteralProperty = (
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Fold a node that is a compile-time constant string down to its value.
|
|
132
|
+
*
|
|
133
|
+
* The fallback below this is `getText()`, which returns the node's *source*.
|
|
134
|
+
* For a lone `'…'` that is indistinguishable from the value, so nobody noticed
|
|
135
|
+
* that anything written as an expression comes through raw: a `description`
|
|
136
|
+
* split across lines as `'Fetch the thing ' + 'from upstream'` reaches
|
|
137
|
+
* `infra.json` with the quotes and the `+` still in it, and is rendered that
|
|
138
|
+
* way in the fabric console.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately checker-free, unlike `extractStringLiteral`. Every value this
|
|
141
|
+
* resolves is a literal in the same expression, so no symbol resolution is
|
|
142
|
+
* needed, and `getPropertyValue` is called from a dozen places that have no
|
|
143
|
+
* checker to hand. Anything it cannot fold — an identifier, a template with a
|
|
144
|
+
* substitution — returns `undefined` and takes the old path, so this only ever
|
|
145
|
+
* turns a wrong answer into a right one.
|
|
146
|
+
*/
|
|
147
|
+
const foldStaticString = (node: ts.Node): string | undefined => {
|
|
148
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
149
|
+
return node.text
|
|
150
|
+
}
|
|
151
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
152
|
+
return foldStaticString(node.expression)
|
|
153
|
+
}
|
|
154
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
155
|
+
return foldStaticString(node.expression)
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
ts.isBinaryExpression(node) &&
|
|
159
|
+
node.operatorToken.kind === ts.SyntaxKind.PlusToken
|
|
160
|
+
) {
|
|
161
|
+
const left = foldStaticString(node.left)
|
|
162
|
+
if (left === undefined) return undefined
|
|
163
|
+
const right = foldStaticString(node.right)
|
|
164
|
+
if (right === undefined) return undefined
|
|
165
|
+
return left + right
|
|
166
|
+
}
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
130
170
|
export const getPropertyValue = (
|
|
131
171
|
obj: ts.ObjectLiteralExpression,
|
|
132
172
|
propertyName: string
|
|
@@ -174,11 +214,9 @@ export const getPropertyValue = (
|
|
|
174
214
|
return Number(initializer.text)
|
|
175
215
|
}
|
|
176
216
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
) {
|
|
181
|
-
return initializer.text
|
|
217
|
+
const staticString = foldStaticString(initializer)
|
|
218
|
+
if (staticString !== undefined) {
|
|
219
|
+
return staticString
|
|
182
220
|
}
|
|
183
221
|
|
|
184
222
|
return initializer.getText()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
1
2
|
import { readFile, readdir } from 'fs/promises'
|
|
2
3
|
import { createRequire } from 'module'
|
|
3
4
|
import { join, dirname } from 'path'
|
|
@@ -141,6 +142,44 @@ const resolveAddonMeta = (
|
|
|
141
142
|
return null
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Give the addon's input/output type names a declaration file to be imported
|
|
147
|
+
* from, so a wiring that references an addon function can name its contract.
|
|
148
|
+
*
|
|
149
|
+
* Without this the only thing that knows where `GetScenarioArtifactInput` lives
|
|
150
|
+
* is the type checker, and it only knows once the consumer's *own* rpc map has
|
|
151
|
+
* already been generated with the addon's routes in it — which is to say on the
|
|
152
|
+
* second run. A first, cold run resolved `ref('console:getScenarioArtifact')`
|
|
153
|
+
* against a map that did not mention it yet and fell back to the whole
|
|
154
|
+
* `FlattenedRPCMap`, so the same source produced a different http-map depending
|
|
155
|
+
* on whether `.pikku` happened to be there. The addon publishes these types at a
|
|
156
|
+
* fixed subpath; reading them from the metadata makes the first run agree with
|
|
157
|
+
* the second.
|
|
158
|
+
*/
|
|
159
|
+
const registerAddonTypes = (
|
|
160
|
+
state: InspectorState,
|
|
161
|
+
metaPath: string,
|
|
162
|
+
meta: Record<string, any>
|
|
163
|
+
): void => {
|
|
164
|
+
// Derived from the metadata's own location rather than resolved as a package
|
|
165
|
+
// subpath: the addon exports that declaration under a `types`-only condition,
|
|
166
|
+
// which `require.resolve` will not follow.
|
|
167
|
+
const typesPath = join(
|
|
168
|
+
dirname(metaPath),
|
|
169
|
+
'..',
|
|
170
|
+
'rpc',
|
|
171
|
+
'pikku-rpc-wirings-map.internal.gen.d.ts'
|
|
172
|
+
)
|
|
173
|
+
if (!existsSync(typesPath)) return
|
|
174
|
+
|
|
175
|
+
for (const funcMeta of Object.values(meta)) {
|
|
176
|
+
for (const name of [funcMeta.inputSchemaName, funcMeta.outputSchemaName]) {
|
|
177
|
+
if (!name || state.functions.typesMap.exists(name, typesPath)) continue
|
|
178
|
+
state.functions.typesMap.addType(name, typesPath)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
/**
|
|
145
184
|
* After the setup sweep discovers wireAddon() declarations, load each addon
|
|
146
185
|
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
@@ -177,6 +216,7 @@ export async function loadAddonFunctionsMeta(
|
|
|
177
216
|
logger.debug(
|
|
178
217
|
`Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`
|
|
179
218
|
)
|
|
219
|
+
registerAddonTypes(state, metaPath, meta)
|
|
180
220
|
|
|
181
221
|
// If wireAddon has mcp: true, expose addon functions with mcp: true as MCP tools
|
|
182
222
|
if (decl.mcp) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
|
-
import {
|
|
3
|
+
import { resolveAgentMiddleware } from './middleware.js'
|
|
4
4
|
import { getInitialInspectorState } from '../inspector.js'
|
|
5
5
|
import * as ts from 'typescript'
|
|
6
6
|
|
|
@@ -26,39 +26,39 @@ function createMockChecker(): ts.TypeChecker {
|
|
|
26
26
|
return {} as any
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
describe('
|
|
30
|
-
test('should return undefined when no
|
|
29
|
+
describe('resolveAgentMiddleware', () => {
|
|
30
|
+
test('should return undefined when no agentMiddleware property exists', () => {
|
|
31
31
|
const state = getInitialInspectorState('/test')
|
|
32
32
|
const src = createSourceFile('const x = { name: "test" }')
|
|
33
33
|
const obj = getObjectLiteral(src)
|
|
34
34
|
const checker = createMockChecker()
|
|
35
35
|
|
|
36
|
-
const result =
|
|
36
|
+
const result = resolveAgentMiddleware(state, obj, checker)
|
|
37
37
|
|
|
38
38
|
assert.strictEqual(result, undefined)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
test('should return undefined when
|
|
41
|
+
test('should return undefined when agentMiddleware array is empty', () => {
|
|
42
42
|
const state = getInitialInspectorState('/test')
|
|
43
|
-
const src = createSourceFile('const x = {
|
|
43
|
+
const src = createSourceFile('const x = { agentMiddleware: [] }')
|
|
44
44
|
const obj = getObjectLiteral(src)
|
|
45
45
|
const checker = createMockChecker()
|
|
46
46
|
|
|
47
|
-
const result =
|
|
47
|
+
const result = resolveAgentMiddleware(state, obj, checker)
|
|
48
48
|
|
|
49
49
|
assert.strictEqual(result, undefined)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test('should resolve
|
|
52
|
+
test('should resolve agentMiddleware identifiers as wire metadata', () => {
|
|
53
53
|
const state = getInitialInspectorState('/test')
|
|
54
|
-
state.
|
|
54
|
+
state.agentMiddleware.definitions['myAIMW'] = {
|
|
55
55
|
services: { optimized: true, services: ['logger'] },
|
|
56
56
|
sourceFile: '/test/middleware.ts',
|
|
57
57
|
position: 0,
|
|
58
58
|
exportedName: 'myAIMW',
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const code = 'const x = {
|
|
61
|
+
const code = 'const x = { agentMiddleware: [myAIMW] }'
|
|
62
62
|
const program = ts.createProgram({
|
|
63
63
|
rootNames: ['test.ts'],
|
|
64
64
|
options: { target: ts.ScriptTarget.ESNext },
|
|
@@ -77,7 +77,7 @@ describe('resolveAIMiddleware', () => {
|
|
|
77
77
|
const sourceFile = program.getSourceFile('test.ts')!
|
|
78
78
|
const obj = getObjectLiteral(sourceFile)
|
|
79
79
|
|
|
80
|
-
const result =
|
|
80
|
+
const result = resolveAgentMiddleware(state, obj, checker)
|
|
81
81
|
|
|
82
82
|
assert.ok(result)
|
|
83
83
|
assert.strictEqual(result.length, 1)
|
|
@@ -88,22 +88,22 @@ describe('resolveAIMiddleware', () => {
|
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
test('should resolve multiple
|
|
91
|
+
test('should resolve multiple agentMiddleware entries', () => {
|
|
92
92
|
const state = getInitialInspectorState('/test')
|
|
93
|
-
state.
|
|
93
|
+
state.agentMiddleware.definitions['firstMW'] = {
|
|
94
94
|
services: { optimized: true, services: ['logger'] },
|
|
95
95
|
sourceFile: '/test/middleware.ts',
|
|
96
96
|
position: 0,
|
|
97
97
|
exportedName: 'firstMW',
|
|
98
98
|
}
|
|
99
|
-
state.
|
|
99
|
+
state.agentMiddleware.definitions['secondMW'] = {
|
|
100
100
|
services: { optimized: true, services: [] },
|
|
101
101
|
sourceFile: '/test/middleware.ts',
|
|
102
102
|
position: 100,
|
|
103
103
|
exportedName: 'secondMW',
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const code = 'const x = {
|
|
106
|
+
const code = 'const x = { agentMiddleware: [firstMW, secondMW] }'
|
|
107
107
|
const program = ts.createProgram({
|
|
108
108
|
rootNames: ['test.ts'],
|
|
109
109
|
options: { target: ts.ScriptTarget.ESNext },
|
|
@@ -122,7 +122,7 @@ describe('resolveAIMiddleware', () => {
|
|
|
122
122
|
const sourceFile = program.getSourceFile('test.ts')!
|
|
123
123
|
const obj = getObjectLiteral(sourceFile)
|
|
124
124
|
|
|
125
|
-
const result =
|
|
125
|
+
const result = resolveAgentMiddleware(state, obj, checker)
|
|
126
126
|
|
|
127
127
|
assert.ok(result)
|
|
128
128
|
assert.strictEqual(result.length, 2)
|
|
@@ -132,16 +132,16 @@ describe('resolveAIMiddleware', () => {
|
|
|
132
132
|
assert.strictEqual((result[1] as any).name, 'secondMW')
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
test('should mark inline
|
|
135
|
+
test('should mark inline agentMiddleware (null exportedName)', () => {
|
|
136
136
|
const state = getInitialInspectorState('/test')
|
|
137
|
-
state.
|
|
137
|
+
state.agentMiddleware.definitions['inlineMW'] = {
|
|
138
138
|
services: { optimized: true, services: [] },
|
|
139
139
|
sourceFile: '/test/middleware.ts',
|
|
140
140
|
position: 0,
|
|
141
141
|
exportedName: null,
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const code = 'const x = {
|
|
144
|
+
const code = 'const x = { agentMiddleware: [inlineMW] }'
|
|
145
145
|
const program = ts.createProgram({
|
|
146
146
|
rootNames: ['test.ts'],
|
|
147
147
|
options: { target: ts.ScriptTarget.ESNext },
|
|
@@ -160,7 +160,7 @@ describe('resolveAIMiddleware', () => {
|
|
|
160
160
|
const sourceFile = program.getSourceFile('test.ts')!
|
|
161
161
|
const obj = getObjectLiteral(sourceFile)
|
|
162
162
|
|
|
163
|
-
const result =
|
|
163
|
+
const result = resolveAgentMiddleware(state, obj, checker)
|
|
164
164
|
|
|
165
165
|
assert.ok(result)
|
|
166
166
|
assert.strictEqual(result.length, 1)
|
|
@@ -171,10 +171,10 @@ describe('resolveAIMiddleware', () => {
|
|
|
171
171
|
})
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
-
test('should handle
|
|
174
|
+
test('should handle agentMiddleware referencing unknown definitions', () => {
|
|
175
175
|
const state = getInitialInspectorState('/test')
|
|
176
176
|
|
|
177
|
-
const code = 'const x = {
|
|
177
|
+
const code = 'const x = { agentMiddleware: [unknownMW] }'
|
|
178
178
|
const program = ts.createProgram({
|
|
179
179
|
rootNames: ['test.ts'],
|
|
180
180
|
options: { target: ts.ScriptTarget.ESNext },
|
|
@@ -193,7 +193,7 @@ describe('resolveAIMiddleware', () => {
|
|
|
193
193
|
const sourceFile = program.getSourceFile('test.ts')!
|
|
194
194
|
const obj = getObjectLiteral(sourceFile)
|
|
195
195
|
|
|
196
|
-
const result =
|
|
196
|
+
const result = resolveAgentMiddleware(state, obj, checker)
|
|
197
197
|
|
|
198
198
|
assert.ok(result)
|
|
199
199
|
assert.strictEqual(result.length, 1)
|
package/src/utils/middleware.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import type { MiddlewareMetadata } from '@pikku/core'
|
|
2
|
+
import type { MiddlewareMetadata } from '@pikku/core/middleware'
|
|
3
3
|
import { extractFunctionName } from './extract-function-name.js'
|
|
4
4
|
import type { InspectorState } from '../types.js'
|
|
5
5
|
|
|
@@ -214,24 +214,24 @@ export function resolveHTTPMiddlewareFromObject(
|
|
|
214
214
|
)
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function
|
|
217
|
+
function getAgentMiddlewareNode(
|
|
218
218
|
obj: ts.ObjectLiteralExpression
|
|
219
219
|
): ts.Expression | undefined {
|
|
220
220
|
const prop = obj.properties.find(
|
|
221
221
|
(p) =>
|
|
222
222
|
ts.isPropertyAssignment(p) &&
|
|
223
223
|
ts.isIdentifier(p.name) &&
|
|
224
|
-
p.name.text === '
|
|
224
|
+
p.name.text === 'agentMiddleware'
|
|
225
225
|
) as ts.PropertyAssignment | undefined
|
|
226
226
|
return prop?.initializer
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
export function
|
|
229
|
+
export function resolveAgentMiddleware(
|
|
230
230
|
state: InspectorState,
|
|
231
231
|
obj: ts.ObjectLiteralExpression,
|
|
232
232
|
checker: ts.TypeChecker
|
|
233
233
|
): MiddlewareMetadata[] | undefined {
|
|
234
|
-
const explicitNode =
|
|
234
|
+
const explicitNode = getAgentMiddlewareNode(obj)
|
|
235
235
|
if (!explicitNode) return undefined
|
|
236
236
|
|
|
237
237
|
const names = extractMiddlewarePikkuNames(
|
|
@@ -240,7 +240,7 @@ export function resolveAIMiddleware(
|
|
|
240
240
|
state.rootDir
|
|
241
241
|
)
|
|
242
242
|
const resolved: MiddlewareMetadata[] = names.map((name) => {
|
|
243
|
-
const def = state.
|
|
243
|
+
const def = state.agentMiddleware.definitions[name]
|
|
244
244
|
return {
|
|
245
245
|
type: 'wire' as const,
|
|
246
246
|
name,
|
package/src/utils/permissions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import type { PermissionMetadata } from '@pikku/core'
|
|
2
|
+
import type { PermissionMetadata } from '@pikku/core/function'
|
|
3
3
|
import { extractFunctionName } from './extract-function-name.js'
|
|
4
4
|
import type { InspectorState } from '../types.js'
|
|
5
5
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { strict as assert } from 'assert'
|
|
2
2
|
import { describe, test } from 'node:test'
|
|
3
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import {
|
|
@@ -142,7 +142,7 @@ function makeState(
|
|
|
142
142
|
|
|
143
143
|
const CONSOLE_PARENT_SERVICES = [
|
|
144
144
|
'metaService',
|
|
145
|
-
'
|
|
145
|
+
'agentRunner',
|
|
146
146
|
'deploymentService',
|
|
147
147
|
'credentialService',
|
|
148
148
|
]
|
|
@@ -187,7 +187,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
187
187
|
runAgent: {
|
|
188
188
|
services: {
|
|
189
189
|
optimized: true,
|
|
190
|
-
services: ['
|
|
190
|
+
services: ['agentRunner', 'deploymentService'],
|
|
191
191
|
},
|
|
192
192
|
},
|
|
193
193
|
},
|
|
@@ -197,7 +197,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
197
197
|
aggregateRequiredServices(state)
|
|
198
198
|
const required = state.serviceAggregation.requiredServices
|
|
199
199
|
assert.ok(required.has('metaService'))
|
|
200
|
-
assert.ok(!required.has('
|
|
200
|
+
assert.ok(!required.has('agentRunner'))
|
|
201
201
|
assert.ok(!required.has('deploymentService'))
|
|
202
202
|
assert.ok(!required.has('credentialService'))
|
|
203
203
|
})
|
|
@@ -258,7 +258,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
258
258
|
const required = state.serviceAggregation.requiredServices
|
|
259
259
|
assert.ok(required.has('kysely'))
|
|
260
260
|
assert.ok(!required.has('metaService'))
|
|
261
|
-
assert.ok(!required.has('
|
|
261
|
+
assert.ok(!required.has('agentRunner'))
|
|
262
262
|
})
|
|
263
263
|
|
|
264
264
|
test('no used addon functions adds no parent services', () => {
|
|
@@ -279,7 +279,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
279
279
|
aggregateRequiredServices(state)
|
|
280
280
|
const required = state.serviceAggregation.requiredServices
|
|
281
281
|
assert.ok(!required.has('metaService'))
|
|
282
|
-
assert.ok(!required.has('
|
|
282
|
+
assert.ok(!required.has('agentRunner'))
|
|
283
283
|
})
|
|
284
284
|
|
|
285
285
|
test('internal services in addon function meta never force the blanket', () => {
|
|
@@ -300,7 +300,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
300
300
|
aggregateRequiredServices(state)
|
|
301
301
|
const required = state.serviceAggregation.requiredServices
|
|
302
302
|
assert.ok(required.has('metaService'))
|
|
303
|
-
assert.ok(!required.has('
|
|
303
|
+
assert.ok(!required.has('agentRunner'))
|
|
304
304
|
assert.ok(!required.has('rpc'))
|
|
305
305
|
})
|
|
306
306
|
|
|
@@ -345,7 +345,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
345
345
|
aggregateRequiredServices(state)
|
|
346
346
|
const required = state.serviceAggregation.requiredServices
|
|
347
347
|
assert.ok(required.has('metaService'))
|
|
348
|
-
assert.ok(!required.has('
|
|
348
|
+
assert.ok(!required.has('agentRunner'))
|
|
349
349
|
})
|
|
350
350
|
|
|
351
351
|
test('multiple used addon functions union their parent services', () => {
|
|
@@ -357,7 +357,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
357
357
|
services: { optimized: true, services: ['metaService'] },
|
|
358
358
|
},
|
|
359
359
|
runAgent: {
|
|
360
|
-
services: { optimized: true, services: ['
|
|
360
|
+
services: { optimized: true, services: ['agentRunner'] },
|
|
361
361
|
},
|
|
362
362
|
},
|
|
363
363
|
},
|
|
@@ -366,7 +366,7 @@ describe('aggregateRequiredServices — per-function addon services', () => {
|
|
|
366
366
|
aggregateRequiredServices(state)
|
|
367
367
|
const required = state.serviceAggregation.requiredServices
|
|
368
368
|
assert.ok(required.has('metaService'))
|
|
369
|
-
assert.ok(required.has('
|
|
369
|
+
assert.ok(required.has('agentRunner'))
|
|
370
370
|
assert.ok(!required.has('deploymentService'))
|
|
371
371
|
assert.ok(!required.has('credentialService'))
|
|
372
372
|
})
|
|
@@ -579,10 +579,67 @@ describe('validateRemoteAddonDependencies (wireRemoteAddon must be a devDependen
|
|
|
579
579
|
}
|
|
580
580
|
})
|
|
581
581
|
|
|
582
|
-
test('
|
|
582
|
+
test('a non-remote (wireAddon) declaration is judged on resolvability, not package.json', () => {
|
|
583
|
+
// Being listed as a dependency is not what makes an addon usable — it is
|
|
584
|
+
// discovered because it is wired, and it can arrive by workspace link or a
|
|
585
|
+
// local addons/ directory without ever appearing here. So a declared but
|
|
586
|
+
// absent package is still an error.
|
|
583
587
|
const { logger, criticals } = makeCriticalLogger()
|
|
584
588
|
const dir = writePkg({ '@addon/local': '1.0.0' }, {})
|
|
585
589
|
try {
|
|
590
|
+
validateRemoteAddonDependencies(
|
|
591
|
+
logger,
|
|
592
|
+
makeRemoteState(dir, { package: '@addon/local' })
|
|
593
|
+
)
|
|
594
|
+
assert.equal(criticals.length, 1)
|
|
595
|
+
assert.equal(criticals[0]!.code, ErrorCode.ADDON_NOT_INSTALLED)
|
|
596
|
+
} finally {
|
|
597
|
+
rmSync(dir, { recursive: true, force: true })
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('a non-remote addon present only in node_modules passes', () => {
|
|
602
|
+
// The inverse, and the case that matters: installed by link, absent from
|
|
603
|
+
// package.json. Reported as missing, this would fail every project whose
|
|
604
|
+
// addon lives in a local addons/ directory.
|
|
605
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
606
|
+
const dir = writePkg({}, {})
|
|
607
|
+
try {
|
|
608
|
+
const pkgDir = join(dir, 'node_modules', '@addon', 'local')
|
|
609
|
+
mkdirSync(pkgDir, { recursive: true })
|
|
610
|
+
writeFileSync(
|
|
611
|
+
join(pkgDir, 'package.json'),
|
|
612
|
+
JSON.stringify({ name: '@addon/local', version: '1.0.0' })
|
|
613
|
+
)
|
|
614
|
+
validateRemoteAddonDependencies(
|
|
615
|
+
logger,
|
|
616
|
+
makeRemoteState(dir, { package: '@addon/local' })
|
|
617
|
+
)
|
|
618
|
+
assert.deepEqual(criticals, [])
|
|
619
|
+
} finally {
|
|
620
|
+
rmSync(dir, { recursive: true, force: true })
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
test('an exports map that refuses package.json is not read as missing', () => {
|
|
625
|
+
// `require.resolve('<pkg>/package.json')` throws ERR_PACKAGE_PATH_NOT_EXPORTED
|
|
626
|
+
// on a package with a restrictive exports map. Reaching the exports map
|
|
627
|
+
// means the package was found, so that must not count as absent.
|
|
628
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
629
|
+
const dir = writePkg({}, {})
|
|
630
|
+
try {
|
|
631
|
+
const pkgDir = join(dir, 'node_modules', '@addon', 'local')
|
|
632
|
+
mkdirSync(pkgDir, { recursive: true })
|
|
633
|
+
writeFileSync(
|
|
634
|
+
join(pkgDir, 'package.json'),
|
|
635
|
+
JSON.stringify({
|
|
636
|
+
name: '@addon/local',
|
|
637
|
+
version: '1.0.0',
|
|
638
|
+
main: 'index.js',
|
|
639
|
+
exports: { '.': './index.js' },
|
|
640
|
+
})
|
|
641
|
+
)
|
|
642
|
+
writeFileSync(join(pkgDir, 'index.js'), 'module.exports = {}\n')
|
|
586
643
|
validateRemoteAddonDependencies(
|
|
587
644
|
logger,
|
|
588
645
|
makeRemoteState(dir, { package: '@addon/local' })
|