@pikku/inspector 0.12.10 → 0.12.12
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 +21 -9
- package/dist/add/add-cli.js +10 -3
- package/dist/add/add-credential.js +2 -1
- package/dist/add/add-functions.js +99 -5
- package/dist/add/add-http-route.js +44 -6
- package/dist/add/add-keyed-wiring.js +3 -1
- package/dist/add/add-middleware.js +33 -4
- package/dist/add/add-permission.js +7 -7
- package/dist/add/add-workflow-graph.js +20 -1
- package/dist/error-codes.d.ts +2 -0
- package/dist/error-codes.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +2 -5
- package/dist/types.d.ts +10 -19
- package/dist/utils/extract-function-name.js +6 -0
- package/dist/utils/filter-inspector-state.js +187 -59
- package/dist/utils/filter-utils.js +13 -5
- package/dist/utils/get-property-value.d.ts +10 -0
- package/dist/utils/get-property-value.js +30 -0
- package/dist/utils/post-process.d.ts +2 -3
- package/dist/utils/post-process.js +3 -23
- package/dist/utils/resolve-addon-package.d.ts +4 -5
- package/dist/utils/resolve-addon-package.js +64 -16
- package/dist/utils/resolve-deploy-target.d.ts +28 -0
- package/dist/utils/resolve-deploy-target.js +56 -0
- package/dist/utils/resolve-versions.js +79 -0
- package/dist/utils/schema-generator.js +31 -12
- package/dist/utils/validate-auth-sessionless.d.ts +1 -1
- package/package.json +2 -2
- package/src/add/add-cli.ts +10 -3
- package/src/add/add-credential.ts +3 -0
- package/src/add/add-functions.test.ts +318 -0
- package/src/add/add-functions.ts +164 -6
- package/src/add/add-gateway.ts +5 -1
- package/src/add/add-http-route.ts +54 -7
- package/src/add/add-keyed-wiring.ts +7 -1
- package/src/add/add-mcp-prompt.ts +5 -1
- package/src/add/add-mcp-resource.ts +5 -1
- package/src/add/add-middleware.ts +42 -4
- package/src/add/add-permission.ts +7 -7
- package/src/add/add-schedule.ts +5 -1
- package/src/add/add-workflow-graph.ts +19 -1
- package/src/add/wire-name-literal.test.ts +114 -0
- package/src/error-codes.ts +2 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +1 -5
- package/src/types.ts +19 -15
- package/src/utils/extract-function-name.ts +8 -0
- package/src/utils/filter-inspector-state.test.ts +168 -64
- package/src/utils/filter-inspector-state.ts +290 -64
- package/src/utils/filter-utils.test.ts +30 -15
- package/src/utils/filter-utils.ts +14 -5
- package/src/utils/get-property-value.ts +40 -0
- package/src/utils/post-process.ts +3 -38
- package/src/utils/resolve-addon-package.ts +65 -14
- package/src/utils/resolve-deploy-target.test.ts +105 -0
- package/src/utils/resolve-deploy-target.ts +63 -0
- package/src/utils/resolve-versions.test.ts +108 -0
- package/src/utils/resolve-versions.ts +86 -0
- package/src/utils/schema-generator.ts +37 -13
- package/src/utils/validate-auth-sessionless.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -61,7 +61,8 @@ export const matchesFilters = (
|
|
|
61
61
|
if (
|
|
62
62
|
(!filters.names || filters.names.length === 0) &&
|
|
63
63
|
(!filters.tags || filters.tags.length === 0) &&
|
|
64
|
-
(!filters.
|
|
64
|
+
(!filters.wires || filters.wires.length === 0) &&
|
|
65
|
+
(!filters.excludeWires || filters.excludeWires.length === 0) &&
|
|
65
66
|
(!filters.directories || filters.directories.length === 0) &&
|
|
66
67
|
(!filters.httpRoutes || filters.httpRoutes.length === 0) &&
|
|
67
68
|
(!filters.httpMethods || filters.httpMethods.length === 0)
|
|
@@ -69,10 +70,18 @@ export const matchesFilters = (
|
|
|
69
70
|
return true
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
// Check
|
|
73
|
-
if (filters.
|
|
74
|
-
if (!filters.
|
|
75
|
-
logger.debug(`⒡ Filtered by
|
|
73
|
+
// Check wire include filter
|
|
74
|
+
if (filters.wires && filters.wires.length > 0) {
|
|
75
|
+
if (!filters.wires.includes(meta.type)) {
|
|
76
|
+
logger.debug(`⒡ Filtered by wire include: ${meta.type}:${meta.name}`)
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check wire exclude filter
|
|
82
|
+
if (filters.excludeWires && filters.excludeWires.length > 0) {
|
|
83
|
+
if (filters.excludeWires.includes(meta.type)) {
|
|
84
|
+
logger.debug(`⒡ Filtered by wire exclude: ${meta.type}:${meta.name}`)
|
|
76
85
|
return false
|
|
77
86
|
}
|
|
78
87
|
}
|
|
@@ -27,6 +27,44 @@ export const getArrayPropertyValue = (
|
|
|
27
27
|
return null
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Wiring identity fields (`name`, `secretId`, `variableId`, …) are read
|
|
32
|
+
* STATICALLY from source — a const or variable reference is keyed by its
|
|
33
|
+
* identifier text, not its runtime value, so the wiring is silently skipped at
|
|
34
|
+
* runtime (`metadata not found`). If the named property exists but is not an
|
|
35
|
+
* inline literal, raise a fatal diagnostic so the build fails instead.
|
|
36
|
+
*/
|
|
37
|
+
export const assertStringLiteralProperty = (
|
|
38
|
+
obj: ts.ObjectLiteralExpression,
|
|
39
|
+
propertyName: string,
|
|
40
|
+
wiringType: string,
|
|
41
|
+
logger?: { critical: (code: ErrorCode, message: string) => void }
|
|
42
|
+
): void => {
|
|
43
|
+
const property = obj.properties.find(
|
|
44
|
+
(p) =>
|
|
45
|
+
ts.isPropertyAssignment(p) &&
|
|
46
|
+
ts.isIdentifier(p.name) &&
|
|
47
|
+
p.name.text === propertyName
|
|
48
|
+
)
|
|
49
|
+
if (!property || !ts.isPropertyAssignment(property)) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const init = property.initializer
|
|
53
|
+
const isStaticLiteral =
|
|
54
|
+
ts.isStringLiteral(init) ||
|
|
55
|
+
ts.isNoSubstitutionTemplateLiteral(init) ||
|
|
56
|
+
ts.isNumericLiteral(init)
|
|
57
|
+
if (isStaticLiteral) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
const errorMsg = `${wiringType} has a non-literal '${propertyName}': \`${init.getText()}\`. Wiring identity fields must be inline string literals — the inspector reads them statically from source, so a const or variable reference is keyed by its identifier text and the wiring is silently skipped at runtime. Inline the literal instead, e.g. ${propertyName}: 'my-wiring-name'.`
|
|
61
|
+
if (logger) {
|
|
62
|
+
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, errorMsg)
|
|
63
|
+
} else {
|
|
64
|
+
console.error(errorMsg)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
30
68
|
export const getPropertyValue = (
|
|
31
69
|
obj: ts.ObjectLiteralExpression,
|
|
32
70
|
propertyName: string
|
|
@@ -117,6 +155,8 @@ export const getCommonWireMetaData = (
|
|
|
117
155
|
errors?: string[]
|
|
118
156
|
} = {}
|
|
119
157
|
|
|
158
|
+
assertStringLiteralProperty(obj, 'name', wiringType, logger)
|
|
159
|
+
|
|
120
160
|
obj.properties.forEach((prop) => {
|
|
121
161
|
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
122
162
|
const propName = prop.name.text
|
|
@@ -2,7 +2,6 @@ import type {
|
|
|
2
2
|
InspectorState,
|
|
3
3
|
InspectorLogger,
|
|
4
4
|
InspectorOptions,
|
|
5
|
-
InspectorModelConfig,
|
|
6
5
|
MiddlewareGroupMeta,
|
|
7
6
|
InspectorDiagnostic,
|
|
8
7
|
} from '../types.js'
|
|
@@ -538,11 +537,8 @@ export function computeRequiredSchemas(
|
|
|
538
537
|
|
|
539
538
|
export function validateAgentModels(
|
|
540
539
|
logger: InspectorLogger,
|
|
541
|
-
state: InspectorState | Omit<InspectorState, 'typesLookup'
|
|
542
|
-
modelConfig?: InspectorModelConfig
|
|
540
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
543
541
|
): void {
|
|
544
|
-
const aliases = modelConfig?.models ?? {}
|
|
545
|
-
|
|
546
542
|
for (const [, meta] of Object.entries(state.agents.agentsMeta)) {
|
|
547
543
|
const model = meta.model
|
|
548
544
|
if (!model) {
|
|
@@ -552,41 +548,10 @@ export function validateAgentModels(
|
|
|
552
548
|
)
|
|
553
549
|
continue
|
|
554
550
|
}
|
|
555
|
-
if (model.includes('/'))
|
|
556
|
-
if (!aliases[model]) {
|
|
557
|
-
const available = Object.keys(aliases)
|
|
558
|
-
logger.critical(
|
|
559
|
-
ErrorCode.INVALID_MODEL,
|
|
560
|
-
`AI agent '${meta.name}' uses model alias '${model}' which is not defined in pikku.config.json models. ` +
|
|
561
|
-
`Available aliases: ${available.join(', ') || 'none'}`
|
|
562
|
-
)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export function validateAgentOverrides(
|
|
568
|
-
logger: InspectorLogger,
|
|
569
|
-
state: InspectorState | Omit<InspectorState, 'typesLookup'>,
|
|
570
|
-
modelConfig?: InspectorModelConfig
|
|
571
|
-
): void {
|
|
572
|
-
const overrides = modelConfig?.agentOverrides ?? {}
|
|
573
|
-
const aliases = modelConfig?.models ?? {}
|
|
574
|
-
const agentNames = new Set(
|
|
575
|
-
Object.values(state.agents.agentsMeta).map((m) => m.name)
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
for (const [agentName, override] of Object.entries(overrides)) {
|
|
579
|
-
if (!agentNames.has(agentName)) {
|
|
580
|
-
logger.warn(`agentOverrides references unknown agent '${agentName}'`)
|
|
581
|
-
}
|
|
582
|
-
if (
|
|
583
|
-
override.model &&
|
|
584
|
-
!override.model.includes('/') &&
|
|
585
|
-
!aliases[override.model]
|
|
586
|
-
) {
|
|
551
|
+
if (!model.includes('/')) {
|
|
587
552
|
logger.critical(
|
|
588
553
|
ErrorCode.INVALID_MODEL,
|
|
589
|
-
`
|
|
554
|
+
`AI agent '${meta.name}' uses model '${model}', which must be provider-qualified as '<provider>/<model>' (e.g. 'openai/gpt-4').`
|
|
590
555
|
)
|
|
591
556
|
}
|
|
592
557
|
}
|
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
+
import { existsSync, readFileSync } from 'fs'
|
|
3
|
+
import { dirname, join, parse } from 'path'
|
|
4
|
+
|
|
5
|
+
const packageNameCache = new Map<string, string | null>()
|
|
6
|
+
|
|
7
|
+
const findPackageNameForFile = (filePath: string): string | null => {
|
|
8
|
+
if (packageNameCache.has(filePath)) {
|
|
9
|
+
return packageNameCache.get(filePath)!
|
|
10
|
+
}
|
|
11
|
+
const root = parse(filePath).root
|
|
12
|
+
let dir = dirname(filePath)
|
|
13
|
+
while (dir && dir !== root) {
|
|
14
|
+
const pkgPath = join(dir, 'package.json')
|
|
15
|
+
if (existsSync(pkgPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
18
|
+
const name = typeof pkg.name === 'string' ? pkg.name : null
|
|
19
|
+
packageNameCache.set(filePath, name)
|
|
20
|
+
return name
|
|
21
|
+
} catch {
|
|
22
|
+
packageNameCache.set(filePath, null)
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(dir)
|
|
27
|
+
if (parent === dir) break
|
|
28
|
+
dir = parent
|
|
29
|
+
}
|
|
30
|
+
packageNameCache.set(filePath, null)
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
2
33
|
|
|
3
34
|
/**
|
|
4
35
|
* Resolve the addon package name from an imported identifier.
|
|
5
36
|
* Checks if the identifier's import module specifier matches any
|
|
6
|
-
* configured addon package
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* addon package.
|
|
37
|
+
* configured addon package — and if the import is relative (because
|
|
38
|
+
* the identifier resolves to a source file inside the addon package
|
|
39
|
+
* itself), walks up to the nearest package.json to obtain the real
|
|
40
|
+
* package name.
|
|
11
41
|
*/
|
|
12
42
|
export const resolveAddonName = (
|
|
13
43
|
identifier: ts.Identifier,
|
|
@@ -31,18 +61,39 @@ export const resolveAddonName = (
|
|
|
31
61
|
if (!sym) return null
|
|
32
62
|
|
|
33
63
|
const decl = sym.declarations?.[0]
|
|
34
|
-
if (!decl
|
|
64
|
+
if (!decl) return null
|
|
65
|
+
|
|
66
|
+
let candidatePackage: string | null = null
|
|
35
67
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
68
|
+
if (ts.isImportSpecifier(decl)) {
|
|
69
|
+
// ImportSpecifier -> NamedImports -> ImportClause -> ImportDeclaration
|
|
70
|
+
const importDecl = decl.parent?.parent?.parent
|
|
71
|
+
if (
|
|
72
|
+
importDecl &&
|
|
73
|
+
ts.isImportDeclaration(importDecl) &&
|
|
74
|
+
ts.isStringLiteral(importDecl.moduleSpecifier)
|
|
75
|
+
) {
|
|
76
|
+
candidatePackage = importDecl.moduleSpecifier.text
|
|
77
|
+
}
|
|
78
|
+
}
|
|
40
79
|
|
|
41
|
-
|
|
80
|
+
// Bare package import path
|
|
81
|
+
if (candidatePackage && !candidatePackage.startsWith('.')) {
|
|
82
|
+
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
83
|
+
if (addonDecl.package === candidatePackage) return addonDecl.package
|
|
84
|
+
}
|
|
85
|
+
}
|
|
42
86
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
87
|
+
// Fall back to package.json lookup based on the declaration's source file.
|
|
88
|
+
// This catches the case where the identifier resolves into an addon
|
|
89
|
+
// package's own internal source (relative import inside that package).
|
|
90
|
+
const declFile = decl.getSourceFile()?.fileName
|
|
91
|
+
if (declFile) {
|
|
92
|
+
const pkgName = findPackageNameForFile(declFile)
|
|
93
|
+
if (pkgName) {
|
|
94
|
+
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
95
|
+
if (addonDecl.package === pkgName) return addonDecl.package
|
|
96
|
+
}
|
|
46
97
|
}
|
|
47
98
|
}
|
|
48
99
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import {
|
|
4
|
+
IncompatibleDeployTargetError,
|
|
5
|
+
resolveDeployTarget,
|
|
6
|
+
} from './resolve-deploy-target.js'
|
|
7
|
+
|
|
8
|
+
describe('resolveDeployTarget', () => {
|
|
9
|
+
test('default → serverless', () => {
|
|
10
|
+
assert.strictEqual(resolveDeployTarget({}, new Set()), 'serverless')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('explicit deploy: server → server', () => {
|
|
14
|
+
assert.strictEqual(
|
|
15
|
+
resolveDeployTarget({ deploy: 'server' }, new Set()),
|
|
16
|
+
'server'
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('explicit deploy: serverless with no incompatible svc → serverless', () => {
|
|
21
|
+
assert.strictEqual(
|
|
22
|
+
resolveDeployTarget(
|
|
23
|
+
{ deploy: 'serverless', services: { services: ['kysely'] } as any },
|
|
24
|
+
new Set(['fs'])
|
|
25
|
+
),
|
|
26
|
+
'serverless'
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('serverlessIncompatible service forces server even without deploy flag', () => {
|
|
31
|
+
assert.strictEqual(
|
|
32
|
+
resolveDeployTarget(
|
|
33
|
+
{ services: { services: ['metaService'] } as any },
|
|
34
|
+
new Set(['metaService'])
|
|
35
|
+
),
|
|
36
|
+
'server'
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('serverlessIncompatible takes precedence over explicit deploy: server', () => {
|
|
41
|
+
assert.strictEqual(
|
|
42
|
+
resolveDeployTarget(
|
|
43
|
+
{
|
|
44
|
+
deploy: 'server',
|
|
45
|
+
services: { services: ['metaService'] } as any,
|
|
46
|
+
},
|
|
47
|
+
new Set(['metaService'])
|
|
48
|
+
),
|
|
49
|
+
'server'
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('explicit deploy: serverless + incompatible svc → throws', () => {
|
|
54
|
+
assert.throws(
|
|
55
|
+
() =>
|
|
56
|
+
resolveDeployTarget(
|
|
57
|
+
{
|
|
58
|
+
deploy: 'serverless',
|
|
59
|
+
services: { services: ['metaService', 'unrelated'] } as any,
|
|
60
|
+
},
|
|
61
|
+
new Set(['metaService']),
|
|
62
|
+
'myFunction'
|
|
63
|
+
),
|
|
64
|
+
(err: unknown) => {
|
|
65
|
+
assert.ok(err instanceof IncompatibleDeployTargetError)
|
|
66
|
+
assert.strictEqual(err.functionName, 'myFunction')
|
|
67
|
+
assert.deepStrictEqual(err.incompatibleServices, ['metaService'])
|
|
68
|
+
assert.match(err.message, /serverless-incompatible/)
|
|
69
|
+
assert.match(err.message, /myFunction/)
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('multiple incompatible services are all reported', () => {
|
|
76
|
+
assert.throws(
|
|
77
|
+
() =>
|
|
78
|
+
resolveDeployTarget(
|
|
79
|
+
{
|
|
80
|
+
deploy: 'serverless',
|
|
81
|
+
services: {
|
|
82
|
+
services: ['metaService', 'localContent'],
|
|
83
|
+
} as any,
|
|
84
|
+
},
|
|
85
|
+
new Set(['metaService', 'localContent']),
|
|
86
|
+
'multiSvc'
|
|
87
|
+
),
|
|
88
|
+
(err: unknown) => {
|
|
89
|
+
assert.ok(err instanceof IncompatibleDeployTargetError)
|
|
90
|
+
assert.deepStrictEqual(err.incompatibleServices, [
|
|
91
|
+
'metaService',
|
|
92
|
+
'localContent',
|
|
93
|
+
])
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('handles funcMeta with no services field', () => {
|
|
100
|
+
assert.strictEqual(
|
|
101
|
+
resolveDeployTarget({ deploy: 'server' }, new Set(['metaService'])),
|
|
102
|
+
'server'
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { FunctionMeta } from '@pikku/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a function's explicit `deploy: 'serverless'` conflicts
|
|
5
|
+
* with one of its services being declared `serverlessIncompatible`.
|
|
6
|
+
* The user has to either remove the explicit flag (let it auto-resolve
|
|
7
|
+
* to 'server'), or set `deploy: 'server'` explicitly.
|
|
8
|
+
*/
|
|
9
|
+
export class IncompatibleDeployTargetError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly functionName: string,
|
|
12
|
+
public readonly incompatibleServices: string[]
|
|
13
|
+
) {
|
|
14
|
+
super(
|
|
15
|
+
`Function '${functionName}' is declared deploy: 'serverless' but uses ` +
|
|
16
|
+
`serverless-incompatible service(s) [${incompatibleServices.join(', ')}]. ` +
|
|
17
|
+
`Either remove deploy: 'serverless' (will auto-resolve to 'server'), ` +
|
|
18
|
+
`or set deploy: 'server' explicitly.`
|
|
19
|
+
)
|
|
20
|
+
this.name = 'IncompatibleDeployTargetError'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Determine the effective deploy target for a function.
|
|
26
|
+
*
|
|
27
|
+
* Resolution order:
|
|
28
|
+
* 1. If any of the function's services is in `serverlessIncompatible`:
|
|
29
|
+
* - throw if the function explicitly declares `deploy: 'serverless'`
|
|
30
|
+
* - otherwise target is 'server'
|
|
31
|
+
* 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
|
|
32
|
+
* 3. Default 'serverless'
|
|
33
|
+
*
|
|
34
|
+
* Used both by the per-unit deploy analyzer (when bucketing functions
|
|
35
|
+
* into deployment units) and by `filterInspectorState` (when
|
|
36
|
+
* `pikku all --deploy <target>` is used to emit a target-scoped set
|
|
37
|
+
* of gen files).
|
|
38
|
+
*/
|
|
39
|
+
export function resolveDeployTarget(
|
|
40
|
+
funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>,
|
|
41
|
+
serverlessIncompatible: Set<string>,
|
|
42
|
+
functionName = '<unknown>'
|
|
43
|
+
): 'serverless' | 'server' {
|
|
44
|
+
// Service compatibility wins over the explicit flag — a serverless
|
|
45
|
+
// bundle of a function that needs (e.g.) node:fs would crash at runtime.
|
|
46
|
+
const incompatibleHits: string[] = []
|
|
47
|
+
if (funcMeta.services?.services) {
|
|
48
|
+
for (const svc of funcMeta.services.services) {
|
|
49
|
+
if (serverlessIncompatible.has(svc)) incompatibleHits.push(svc)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (incompatibleHits.length > 0) {
|
|
54
|
+
if (funcMeta.deploy === 'serverless') {
|
|
55
|
+
throw new IncompatibleDeployTargetError(functionName, incompatibleHits)
|
|
56
|
+
}
|
|
57
|
+
return 'server'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (funcMeta.deploy === 'server') return 'server'
|
|
61
|
+
if (funcMeta.deploy === 'serverless') return 'serverless'
|
|
62
|
+
return 'serverless'
|
|
63
|
+
}
|
|
@@ -387,4 +387,112 @@ describe('resolveLatestVersions', () => {
|
|
|
387
387
|
assert.strictEqual(state.rpc.internalFiles.has('createUser'), false)
|
|
388
388
|
assert.ok(state.rpc.internalFiles.has('createUser@v2'))
|
|
389
389
|
})
|
|
390
|
+
|
|
391
|
+
test('rewrites CLI, channel, scheduler, queue and MCP funcId references when an unversioned function gets an implicit version', () => {
|
|
392
|
+
const state = makeState({
|
|
393
|
+
'listCards@v1': { pikkuFuncId: 'listCards@v1', version: 1 },
|
|
394
|
+
listCards: { pikkuFuncId: 'listCards' },
|
|
395
|
+
})
|
|
396
|
+
// CLI command (+ nested subcommand) referencing the unversioned function
|
|
397
|
+
state.cli = {
|
|
398
|
+
meta: {
|
|
399
|
+
programs: {
|
|
400
|
+
kanban: {
|
|
401
|
+
name: 'kanban',
|
|
402
|
+
commands: {
|
|
403
|
+
list: { pikkuFuncId: 'listCards' },
|
|
404
|
+
cards: {
|
|
405
|
+
pikkuFuncId: 'listCards',
|
|
406
|
+
subcommands: { all: { pikkuFuncId: 'listCards' } },
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
renderers: {},
|
|
412
|
+
},
|
|
413
|
+
} as any
|
|
414
|
+
// Channel slots + action-routed message wiring
|
|
415
|
+
state.channels = {
|
|
416
|
+
meta: {
|
|
417
|
+
cli: {
|
|
418
|
+
name: 'cli',
|
|
419
|
+
route: '/cli/kanban',
|
|
420
|
+
input: null,
|
|
421
|
+
connect: null,
|
|
422
|
+
disconnect: null,
|
|
423
|
+
message: { pikkuFuncId: 'listCards' },
|
|
424
|
+
messageWirings: {
|
|
425
|
+
command: { list: { pikkuFuncId: 'listCards' } },
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
files: new Set(),
|
|
430
|
+
} as any
|
|
431
|
+
state.scheduledTasks = {
|
|
432
|
+
meta: {
|
|
433
|
+
tick: { name: 'tick', schedule: '* * * * *', pikkuFuncId: 'listCards' },
|
|
434
|
+
},
|
|
435
|
+
files: new Set(),
|
|
436
|
+
} as any
|
|
437
|
+
state.queueWorkers = {
|
|
438
|
+
meta: { worker: { name: 'worker', pikkuFuncId: 'listCards' } },
|
|
439
|
+
files: new Set(),
|
|
440
|
+
} as any
|
|
441
|
+
state.mcpEndpoints = {
|
|
442
|
+
resourcesMeta: { res: { pikkuFuncId: 'listCards' } },
|
|
443
|
+
toolsMeta: { tool: { pikkuFuncId: 'listCards' } },
|
|
444
|
+
promptsMeta: { prompt: { pikkuFuncId: 'listCards' } },
|
|
445
|
+
files: new Set(),
|
|
446
|
+
} as any
|
|
447
|
+
state.triggers = {
|
|
448
|
+
meta: { cardCreated: { name: 'cardCreated' } },
|
|
449
|
+
sourceMeta: {
|
|
450
|
+
cardCreated: { name: 'cardCreated', pikkuFuncId: 'listCards' },
|
|
451
|
+
},
|
|
452
|
+
files: new Set(),
|
|
453
|
+
} as any
|
|
454
|
+
const { logger } = makeLogger()
|
|
455
|
+
|
|
456
|
+
resolveLatestVersions(state, logger)
|
|
457
|
+
|
|
458
|
+
// unversioned listCards becomes the implicit latest (v2)
|
|
459
|
+
assert.ok(state.functions.meta['listCards@v2'])
|
|
460
|
+
const program = (state as any).cli.meta.programs.kanban
|
|
461
|
+
assert.strictEqual(program.commands.list.pikkuFuncId, 'listCards@v2')
|
|
462
|
+
assert.strictEqual(program.commands.cards.pikkuFuncId, 'listCards@v2')
|
|
463
|
+
assert.strictEqual(
|
|
464
|
+
program.commands.cards.subcommands.all.pikkuFuncId,
|
|
465
|
+
'listCards@v2'
|
|
466
|
+
)
|
|
467
|
+
const channel = (state as any).channels.meta.cli
|
|
468
|
+
assert.strictEqual(channel.message.pikkuFuncId, 'listCards@v2')
|
|
469
|
+
assert.strictEqual(
|
|
470
|
+
channel.messageWirings.command.list.pikkuFuncId,
|
|
471
|
+
'listCards@v2'
|
|
472
|
+
)
|
|
473
|
+
assert.strictEqual(
|
|
474
|
+
(state as any).scheduledTasks.meta.tick.pikkuFuncId,
|
|
475
|
+
'listCards@v2'
|
|
476
|
+
)
|
|
477
|
+
assert.strictEqual(
|
|
478
|
+
(state as any).queueWorkers.meta.worker.pikkuFuncId,
|
|
479
|
+
'listCards@v2'
|
|
480
|
+
)
|
|
481
|
+
assert.strictEqual(
|
|
482
|
+
(state as any).mcpEndpoints.resourcesMeta.res.pikkuFuncId,
|
|
483
|
+
'listCards@v2'
|
|
484
|
+
)
|
|
485
|
+
assert.strictEqual(
|
|
486
|
+
(state as any).mcpEndpoints.toolsMeta.tool.pikkuFuncId,
|
|
487
|
+
'listCards@v2'
|
|
488
|
+
)
|
|
489
|
+
assert.strictEqual(
|
|
490
|
+
(state as any).mcpEndpoints.promptsMeta.prompt.pikkuFuncId,
|
|
491
|
+
'listCards@v2'
|
|
492
|
+
)
|
|
493
|
+
assert.strictEqual(
|
|
494
|
+
(state as any).triggers.sourceMeta.cardCreated.pikkuFuncId,
|
|
495
|
+
'listCards@v2'
|
|
496
|
+
)
|
|
497
|
+
})
|
|
390
498
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseVersionedId, formatVersionedId } from '@pikku/core'
|
|
2
|
+
import type { CLICommandMeta } from '@pikku/core/cli'
|
|
2
3
|
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
3
4
|
import { ErrorCode } from '../error-codes.js'
|
|
4
5
|
|
|
@@ -130,6 +131,7 @@ function updateWiringReferences(
|
|
|
130
131
|
oldId: string,
|
|
131
132
|
newId: string
|
|
132
133
|
): void {
|
|
134
|
+
// HTTP routes
|
|
133
135
|
if (state.http) {
|
|
134
136
|
for (const methods of Object.values(state.http.meta)) {
|
|
135
137
|
for (const meta of Object.values(methods)) {
|
|
@@ -139,4 +141,88 @@ function updateWiringReferences(
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
}
|
|
144
|
+
|
|
145
|
+
// Channels: connect/disconnect/message slots + action-routed message wirings
|
|
146
|
+
if (state.channels) {
|
|
147
|
+
for (const channel of Object.values(state.channels.meta)) {
|
|
148
|
+
for (const slot of [
|
|
149
|
+
channel.connect,
|
|
150
|
+
channel.disconnect,
|
|
151
|
+
channel.message,
|
|
152
|
+
]) {
|
|
153
|
+
if (slot && slot.pikkuFuncId === oldId) {
|
|
154
|
+
slot.pikkuFuncId = newId
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const routes of Object.values(channel.messageWirings)) {
|
|
158
|
+
for (const message of Object.values(routes)) {
|
|
159
|
+
if (message.pikkuFuncId === oldId) {
|
|
160
|
+
message.pikkuFuncId = newId
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// CLI programs: commands and nested subcommands. This also covers
|
|
168
|
+
// CLI-over-channel generation, which reads command funcIds from this meta.
|
|
169
|
+
if (state.cli) {
|
|
170
|
+
const updateCommands = (commands: Record<string, CLICommandMeta>): void => {
|
|
171
|
+
for (const command of Object.values(commands)) {
|
|
172
|
+
if (command.pikkuFuncId === oldId) {
|
|
173
|
+
command.pikkuFuncId = newId
|
|
174
|
+
}
|
|
175
|
+
if (command.subcommands) {
|
|
176
|
+
updateCommands(command.subcommands)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const program of Object.values(state.cli.meta.programs)) {
|
|
181
|
+
updateCommands(program.commands)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Scheduled tasks
|
|
186
|
+
if (state.scheduledTasks) {
|
|
187
|
+
for (const task of Object.values(state.scheduledTasks.meta)) {
|
|
188
|
+
if (task.pikkuFuncId === oldId) {
|
|
189
|
+
task.pikkuFuncId = newId
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Queue workers
|
|
195
|
+
if (state.queueWorkers) {
|
|
196
|
+
for (const worker of Object.values(state.queueWorkers.meta)) {
|
|
197
|
+
if (worker.pikkuFuncId === oldId) {
|
|
198
|
+
worker.pikkuFuncId = newId
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Trigger sources (TriggerSourceMeta carries the handler's pikkuFuncId).
|
|
204
|
+
// Gateways/workflows/agents reference functions by bare rpc name and are
|
|
205
|
+
// resolved at runtime via state.rpc.internalMeta, so they need no rewrite here.
|
|
206
|
+
if (state.triggers) {
|
|
207
|
+
for (const source of Object.values(state.triggers.sourceMeta)) {
|
|
208
|
+
if (source.pikkuFuncId === oldId) {
|
|
209
|
+
source.pikkuFuncId = newId
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// MCP resources, tools, and prompts
|
|
215
|
+
if (state.mcpEndpoints) {
|
|
216
|
+
for (const collection of [
|
|
217
|
+
state.mcpEndpoints.resourcesMeta,
|
|
218
|
+
state.mcpEndpoints.toolsMeta,
|
|
219
|
+
state.mcpEndpoints.promptsMeta,
|
|
220
|
+
]) {
|
|
221
|
+
for (const endpoint of Object.values(collection)) {
|
|
222
|
+
if (endpoint.pikkuFuncId === oldId) {
|
|
223
|
+
endpoint.pikkuFuncId = newId
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
142
228
|
}
|