@pikku/inspector 0.12.11 → 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 +13 -0
- package/dist/add/add-cli.js +10 -3
- package/dist/add/add-credential.js +2 -1
- package/dist/add/add-functions.js +28 -1
- package/dist/add/add-http-route.js +24 -5
- 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 +1 -0
- package/dist/error-codes.js +1 -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/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 +149 -0
- package/src/add/add-functions.ts +37 -1
- package/src/add/add-gateway.ts +5 -1
- package/src/add/add-http-route.ts +26 -6
- 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 +1 -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/tsconfig.tsbuildinfo +1 -1
package/src/add/add-gateway.ts
CHANGED
|
@@ -70,7 +70,11 @@ export const addGateway: AddWiring = (
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
73
|
-
? resolveAddonName(
|
|
73
|
+
? resolveAddonName(
|
|
74
|
+
funcInitializer,
|
|
75
|
+
checker,
|
|
76
|
+
state.rpc.wireAddonDeclarations
|
|
77
|
+
)
|
|
74
78
|
: null
|
|
75
79
|
|
|
76
80
|
if (!nameValue || !typeValue) {
|
|
@@ -273,12 +273,32 @@ export function registerHTTPRoute({
|
|
|
273
273
|
|
|
274
274
|
const input = fnMeta.inputs?.[0] || null
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const inputTypes = state.typesLookup.get(
|
|
276
|
+
const getRouteInputKeys = (): string[] | null => {
|
|
277
|
+
const targetFuncName = refAddonTarget ?? funcName
|
|
278
|
+
const inputTypes = state.typesLookup.get(targetFuncName)
|
|
279
279
|
if (inputTypes && inputTypes.length > 0) {
|
|
280
|
-
|
|
280
|
+
return extractTypeKeys(inputTypes[0])
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const targetMeta = resolveFunctionMeta(state, targetFuncName)
|
|
284
|
+
if (targetMeta?.inputSchemaName) {
|
|
285
|
+
const schema = state.schemas[targetMeta.inputSchemaName] as any
|
|
286
|
+
const properties = schema?.properties
|
|
287
|
+
if (properties && typeof properties === 'object') {
|
|
288
|
+
return Object.keys(properties)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
281
291
|
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Validate that route params and query params exist in function input type
|
|
296
|
+
if (params.length > 0 || query.length > 0) {
|
|
297
|
+
const inputKeys = getRouteInputKeys()
|
|
298
|
+
if (!inputKeys) {
|
|
299
|
+
// Input shape isn't inspectable at this phase (e.g. addon ref or opaque handler).
|
|
300
|
+
// Skip param/query validation rather than emitting a false positive.
|
|
301
|
+
} else {
|
|
282
302
|
// Check path params
|
|
283
303
|
if (params.length > 0) {
|
|
284
304
|
const missingParams = params.filter((p) => !inputKeys.includes(p))
|
|
@@ -286,7 +306,7 @@ export function registerHTTPRoute({
|
|
|
286
306
|
logger.critical(
|
|
287
307
|
ErrorCode.ROUTE_PARAM_MISMATCH,
|
|
288
308
|
`Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
|
|
289
|
-
`not found in function '${funcName}' input type. ` +
|
|
309
|
+
`not found in function '${refAddonTarget ?? funcName}' input type. ` +
|
|
290
310
|
`Input type has: [${inputKeys.join(', ')}]`
|
|
291
311
|
)
|
|
292
312
|
return
|
|
@@ -300,7 +320,7 @@ export function registerHTTPRoute({
|
|
|
300
320
|
logger.critical(
|
|
301
321
|
ErrorCode.ROUTE_QUERY_MISMATCH,
|
|
302
322
|
`Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
|
|
303
|
-
`not found in function '${funcName}' input type. ` +
|
|
323
|
+
`not found in function '${refAddonTarget ?? funcName}' input type. ` +
|
|
304
324
|
`Input type has: [${inputKeys.join(', ')}]`
|
|
305
325
|
)
|
|
306
326
|
return
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getPropertyValue,
|
|
4
|
+
assertStringLiteralProperty,
|
|
5
|
+
} from '../utils/get-property-value.js'
|
|
3
6
|
import type { AddWiring, InspectorState } from '../types.js'
|
|
4
7
|
import { ErrorCode } from '../error-codes.js'
|
|
5
8
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
@@ -39,6 +42,9 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
39
42
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
40
43
|
const obj = firstArg
|
|
41
44
|
|
|
45
|
+
assertStringLiteralProperty(obj, 'name', config.label, logger)
|
|
46
|
+
assertStringLiteralProperty(obj, config.idField, config.label, logger)
|
|
47
|
+
|
|
42
48
|
const nameValue = getPropertyValue(obj, 'name') as string | null
|
|
43
49
|
const displayNameValue = getPropertyValue(obj, 'displayName') as
|
|
44
50
|
| string
|
|
@@ -74,7 +74,11 @@ export const addMCPPrompt: AddWiring = (
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
77
|
-
? resolveAddonName(
|
|
77
|
+
? resolveAddonName(
|
|
78
|
+
funcInitializer,
|
|
79
|
+
checker,
|
|
80
|
+
state.rpc.wireAddonDeclarations
|
|
81
|
+
)
|
|
78
82
|
: null
|
|
79
83
|
|
|
80
84
|
ensureFunctionMetadata(
|
|
@@ -83,7 +83,11 @@ export const addMCPResource: AddWiring = (
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
86
|
-
? resolveAddonName(
|
|
86
|
+
? resolveAddonName(
|
|
87
|
+
funcInitializer,
|
|
88
|
+
checker,
|
|
89
|
+
state.rpc.wireAddonDeclarations
|
|
90
|
+
)
|
|
87
91
|
: null
|
|
88
92
|
|
|
89
93
|
ensureFunctionMetadata(
|
|
@@ -245,7 +245,7 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
245
245
|
return
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
if (expression.text === '
|
|
248
|
+
if (expression.text === 'addTagMiddleware') {
|
|
249
249
|
const tagArg = args[0]
|
|
250
250
|
const middlewareArrayArg = args[1]
|
|
251
251
|
|
|
@@ -257,13 +257,13 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
if (!tag) {
|
|
260
|
-
logger.warn(`•
|
|
260
|
+
logger.warn(`• addTagMiddleware call without valid tag string`)
|
|
261
261
|
return
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
|
|
265
265
|
logger.error(
|
|
266
|
-
`•
|
|
266
|
+
`• addTagMiddleware('${tag}', ...) must have a literal array as second argument`
|
|
267
267
|
)
|
|
268
268
|
return
|
|
269
269
|
}
|
|
@@ -329,7 +329,7 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
329
329
|
if (!isFactory && exportedName) {
|
|
330
330
|
logger.warn(
|
|
331
331
|
`• Middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
|
|
332
|
-
`For tree-shaking, use: export const ${exportedName} = () =>
|
|
332
|
+
`For tree-shaking, use: export const ${exportedName} = () => addTagMiddleware('${tag}', [...])`
|
|
333
333
|
)
|
|
334
334
|
}
|
|
335
335
|
|
|
@@ -352,6 +352,43 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
352
352
|
return
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
if (expression.text === 'addGlobalMiddleware') {
|
|
356
|
+
const middlewareArrayArg = args[0]
|
|
357
|
+
if (
|
|
358
|
+
!middlewareArrayArg ||
|
|
359
|
+
!ts.isArrayLiteralExpression(middlewareArrayArg)
|
|
360
|
+
) {
|
|
361
|
+
logger.error(
|
|
362
|
+
`• addGlobalMiddleware(...) must have a literal array as its only argument`
|
|
363
|
+
)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
const refs = extractMiddlewareRefs(
|
|
367
|
+
middlewareArrayArg,
|
|
368
|
+
checker,
|
|
369
|
+
state.rootDir
|
|
370
|
+
)
|
|
371
|
+
const definitionIds = refs.map((r) => r.definitionId)
|
|
372
|
+
if (definitionIds.length > 0) {
|
|
373
|
+
renameTempDefinitions(state, definitionIds, 'global', 'middleware')
|
|
374
|
+
}
|
|
375
|
+
const sourceFile = node.getSourceFile().fileName
|
|
376
|
+
for (let i = 0; i < refs.length; i++) {
|
|
377
|
+
const instanceId = makeContextBasedId('global', 'middleware', String(i))
|
|
378
|
+
state.middleware.instances[instanceId] = {
|
|
379
|
+
definitionId: definitionIds[i],
|
|
380
|
+
sourceFile,
|
|
381
|
+
position: node.getStart(),
|
|
382
|
+
isFactoryCall: refs[i].isFactoryCall,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Without this, bootstrap codegen's "import every file with a wire-call"
|
|
386
|
+
// pass skips middleware-only files and the registration never runs.
|
|
387
|
+
state.http.files.add(sourceFile)
|
|
388
|
+
logger.debug(`• Found global middleware group with ${refs.length} entries`)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
355
392
|
if (expression.text === 'addHTTPMiddleware') {
|
|
356
393
|
const patternArg = args[0]
|
|
357
394
|
const middlewareArrayArg = args[1]
|
|
@@ -452,6 +489,7 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
452
489
|
instanceIds,
|
|
453
490
|
isFactory,
|
|
454
491
|
})
|
|
492
|
+
state.http.files.add(sourceFile)
|
|
455
493
|
|
|
456
494
|
logger.debug(
|
|
457
495
|
`• Found HTTP route middleware group: ${pattern} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
|
|
@@ -45,7 +45,7 @@ function isInsidePermissionContainer(node: ts.Node): boolean {
|
|
|
45
45
|
ts.isCallExpression(current) &&
|
|
46
46
|
ts.isIdentifier(current.expression) &&
|
|
47
47
|
(current.expression.text === 'pikkuPermissionFactory' ||
|
|
48
|
-
current.expression.text === '
|
|
48
|
+
current.expression.text === 'addTagPermission' ||
|
|
49
49
|
current.expression.text === 'addHTTPPermission')
|
|
50
50
|
) {
|
|
51
51
|
return true
|
|
@@ -338,9 +338,9 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
|
338
338
|
|
|
339
339
|
// Handle addPermission('tag', [permission1, permission2])
|
|
340
340
|
// Supports two patterns:
|
|
341
|
-
// 1. export const x = () =>
|
|
342
|
-
// 2. export const x =
|
|
343
|
-
if (expression.text === '
|
|
341
|
+
// 1. export const x = () => addTagPermission('tag', [...]) (factory - tree-shakeable)
|
|
342
|
+
// 2. export const x = addTagPermission('tag', [...]) (direct - no tree-shaking)
|
|
343
|
+
if (expression.text === 'addTagPermission') {
|
|
344
344
|
const tagArg = args[0]
|
|
345
345
|
const permissionsArrayArg = args[1]
|
|
346
346
|
|
|
@@ -353,7 +353,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
if (!tag) {
|
|
356
|
-
logger.warn(`•
|
|
356
|
+
logger.warn(`• addTagPermission call without valid tag string`)
|
|
357
357
|
return
|
|
358
358
|
}
|
|
359
359
|
|
|
@@ -363,7 +363,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
|
363
363
|
!ts.isObjectLiteralExpression(permissionsArrayArg)
|
|
364
364
|
) {
|
|
365
365
|
logger.error(
|
|
366
|
-
`•
|
|
366
|
+
`• addTagPermission('${tag}', ...) must have a literal array or object as second argument`
|
|
367
367
|
)
|
|
368
368
|
return
|
|
369
369
|
}
|
|
@@ -416,7 +416,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
|
416
416
|
if (!isFactory && exportedName) {
|
|
417
417
|
logger.warn(
|
|
418
418
|
`• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
|
|
419
|
-
`For tree-shaking, use: export const ${exportedName} = () =>
|
|
419
|
+
`For tree-shaking, use: export const ${exportedName} = () => addTagPermission('${tag}', [...])`
|
|
420
420
|
)
|
|
421
421
|
}
|
|
422
422
|
|
package/src/add/add-schedule.ts
CHANGED
|
@@ -73,7 +73,11 @@ export const addSchedule: AddWiring = (
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
76
|
-
? resolveAddonName(
|
|
76
|
+
? resolveAddonName(
|
|
77
|
+
funcInitializer,
|
|
78
|
+
checker,
|
|
79
|
+
state.rpc.wireAddonDeclarations
|
|
80
|
+
)
|
|
77
81
|
: null
|
|
78
82
|
|
|
79
83
|
if (!nameValue || !scheduleValue) {
|
|
@@ -357,6 +357,8 @@ function extractGraphFromNewFormat(
|
|
|
357
357
|
input: {},
|
|
358
358
|
next: undefined,
|
|
359
359
|
onError: undefined,
|
|
360
|
+
retries: undefined,
|
|
361
|
+
retryDelay: undefined,
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
364
|
|
|
@@ -382,6 +384,8 @@ function extractGraphFromNewFormat(
|
|
|
382
384
|
nodes[nodeId].next = nodeConfig.next
|
|
383
385
|
nodes[nodeId].onError = nodeConfig.onError
|
|
384
386
|
nodes[nodeId].input = nodeConfig.input
|
|
387
|
+
nodes[nodeId].retries = nodeConfig.retries
|
|
388
|
+
nodes[nodeId].retryDelay = nodeConfig.retryDelay
|
|
385
389
|
}
|
|
386
390
|
}
|
|
387
391
|
}
|
|
@@ -400,10 +404,14 @@ function extractNodeConfigFromObject(
|
|
|
400
404
|
next: any
|
|
401
405
|
onError: any
|
|
402
406
|
input: Record<string, any>
|
|
407
|
+
retries: number | undefined
|
|
408
|
+
retryDelay: string | number | undefined
|
|
403
409
|
} {
|
|
404
410
|
let next: any = undefined
|
|
405
411
|
let onError: any = undefined
|
|
406
412
|
let input: Record<string, any> = {}
|
|
413
|
+
let retries: number | undefined = undefined
|
|
414
|
+
let retryDelay: string | number | undefined = undefined
|
|
407
415
|
|
|
408
416
|
for (const prop of obj.properties) {
|
|
409
417
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue
|
|
@@ -416,10 +424,20 @@ function extractNodeConfigFromObject(
|
|
|
416
424
|
onError = extractNextConfig(prop.initializer, checker)
|
|
417
425
|
} else if (propName === 'input') {
|
|
418
426
|
input = extractInputMapping(prop.initializer, checker)
|
|
427
|
+
} else if (propName === 'retries') {
|
|
428
|
+
if (ts.isNumericLiteral(prop.initializer)) {
|
|
429
|
+
retries = Number(prop.initializer.text)
|
|
430
|
+
}
|
|
431
|
+
} else if (propName === 'retryDelay') {
|
|
432
|
+
if (ts.isNumericLiteral(prop.initializer)) {
|
|
433
|
+
retryDelay = Number(prop.initializer.text)
|
|
434
|
+
} else if (ts.isStringLiteral(prop.initializer)) {
|
|
435
|
+
retryDelay = prop.initializer.text
|
|
436
|
+
}
|
|
419
437
|
}
|
|
420
438
|
}
|
|
421
439
|
|
|
422
|
-
return { next, onError, input }
|
|
440
|
+
return { next, onError, input, retries, retryDelay }
|
|
423
441
|
}
|
|
424
442
|
|
|
425
443
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
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 { ErrorCode } from '../error-codes.js'
|
|
8
|
+
import type { InspectorLogger } from '../types.js'
|
|
9
|
+
|
|
10
|
+
const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
11
|
+
({
|
|
12
|
+
debug: () => {},
|
|
13
|
+
info: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
error: () => {},
|
|
16
|
+
critical: (code: ErrorCode, message: string) => {
|
|
17
|
+
criticals.push({ code, message })
|
|
18
|
+
},
|
|
19
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
20
|
+
}) satisfies InspectorLogger
|
|
21
|
+
|
|
22
|
+
describe('wiring name must be a string literal', () => {
|
|
23
|
+
test('logs a critical error when a queue worker name is a const reference', async () => {
|
|
24
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-nonliteral-name-'))
|
|
25
|
+
const file = join(rootDir, 'queue.ts')
|
|
26
|
+
|
|
27
|
+
await writeFile(
|
|
28
|
+
file,
|
|
29
|
+
[
|
|
30
|
+
"import { pikkuSessionlessFunc, wireQueueWorker } from '@pikku/core'",
|
|
31
|
+
'const QUEUE_NAME = "stripe-webhook-event"',
|
|
32
|
+
'export const handler = pikkuSessionlessFunc({',
|
|
33
|
+
' func: async () => ({ ok: true })',
|
|
34
|
+
'})',
|
|
35
|
+
'wireQueueWorker({',
|
|
36
|
+
' name: QUEUE_NAME,',
|
|
37
|
+
' func: handler,',
|
|
38
|
+
'})',
|
|
39
|
+
].join('\n')
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
43
|
+
try {
|
|
44
|
+
await inspect(makeLogger(criticals), [file], { rootDir })
|
|
45
|
+
const hit = criticals.find(
|
|
46
|
+
(entry) => entry.code === ErrorCode.NON_LITERAL_WIRE_NAME
|
|
47
|
+
)
|
|
48
|
+
assert.ok(hit, 'expected NON_LITERAL_WIRE_NAME critical')
|
|
49
|
+
assert.match(hit!.message, /QUEUE_NAME/)
|
|
50
|
+
} finally {
|
|
51
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('logs a critical error when a secret id is a const reference', async () => {
|
|
56
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-nonliteral-secret-'))
|
|
57
|
+
const file = join(rootDir, 'secret.ts')
|
|
58
|
+
|
|
59
|
+
await writeFile(
|
|
60
|
+
file,
|
|
61
|
+
[
|
|
62
|
+
"import { wireSecret } from '@pikku/core'",
|
|
63
|
+
'const SECRET_ID = "STRIPE_SECRET_KEY"',
|
|
64
|
+
'wireSecret({',
|
|
65
|
+
' secretId: SECRET_ID,',
|
|
66
|
+
" name: 'Stripe secret key',",
|
|
67
|
+
" displayName: 'Stripe secret key',",
|
|
68
|
+
'})',
|
|
69
|
+
].join('\n')
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
73
|
+
try {
|
|
74
|
+
await inspect(makeLogger(criticals), [file], { rootDir })
|
|
75
|
+
const hit = criticals.find(
|
|
76
|
+
(entry) => entry.code === ErrorCode.NON_LITERAL_WIRE_NAME
|
|
77
|
+
)
|
|
78
|
+
assert.ok(hit, 'expected NON_LITERAL_WIRE_NAME critical')
|
|
79
|
+
assert.match(hit!.message, /SECRET_ID/)
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('does not flag a queue worker whose name is an inline literal', async () => {
|
|
86
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-literal-name-'))
|
|
87
|
+
const file = join(rootDir, 'queue.ts')
|
|
88
|
+
|
|
89
|
+
await writeFile(
|
|
90
|
+
file,
|
|
91
|
+
[
|
|
92
|
+
"import { pikkuSessionlessFunc, wireQueueWorker } from '@pikku/core'",
|
|
93
|
+
'export const handler = pikkuSessionlessFunc({',
|
|
94
|
+
' func: async () => ({ ok: true })',
|
|
95
|
+
'})',
|
|
96
|
+
'wireQueueWorker({',
|
|
97
|
+
" name: 'stripe-webhook-event',",
|
|
98
|
+
' func: handler,',
|
|
99
|
+
'})',
|
|
100
|
+
].join('\n')
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
104
|
+
try {
|
|
105
|
+
await inspect(makeLogger(criticals), [file], { rootDir })
|
|
106
|
+
const hit = criticals.find(
|
|
107
|
+
(entry) => entry.code === ErrorCode.NON_LITERAL_WIRE_NAME
|
|
108
|
+
)
|
|
109
|
+
assert.equal(hit, undefined)
|
|
110
|
+
} finally {
|
|
111
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
})
|
package/src/error-codes.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
} from './utils/serialize-inspector-state.js'
|
|
10
10
|
export type { SerializableInspectorState } from './utils/serialize-inspector-state.js'
|
|
11
11
|
export { filterInspectorState } from './utils/filter-inspector-state.js'
|
|
12
|
+
export { resolveDeployTarget } from './utils/resolve-deploy-target.js'
|
|
12
13
|
export {
|
|
13
14
|
generateCustomTypes,
|
|
14
15
|
sanitizeTypeName,
|
package/src/inspector.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { findCommonAncestor } from './utils/find-root-dir.js'
|
|
|
12
12
|
import {
|
|
13
13
|
aggregateRequiredServices,
|
|
14
14
|
validateAgentModels,
|
|
15
|
-
validateAgentOverrides,
|
|
16
15
|
validateSecretOverrides,
|
|
17
16
|
validateVariableOverrides,
|
|
18
17
|
validateCredentialOverrides,
|
|
@@ -250,8 +249,6 @@ export const inspect = async (
|
|
|
250
249
|
const rootDir = options.rootDir || findCommonAncestor(routeFiles)
|
|
251
250
|
|
|
252
251
|
const startSourceFiles = performance.now()
|
|
253
|
-
// Filter source files to only include files within the project rootDir
|
|
254
|
-
// This prevents picking up types from external packages (including workspace symlinks)
|
|
255
252
|
const sourceFiles = program
|
|
256
253
|
.getSourceFiles()
|
|
257
254
|
.filter((sf) => sf.fileName.startsWith(rootDir))
|
|
@@ -354,8 +351,7 @@ export const inspect = async (
|
|
|
354
351
|
)
|
|
355
352
|
}
|
|
356
353
|
|
|
357
|
-
validateAgentModels(logger, state
|
|
358
|
-
validateAgentOverrides(logger, state, options.modelConfig)
|
|
354
|
+
validateAgentModels(logger, state)
|
|
359
355
|
validateSecretOverrides(logger, state)
|
|
360
356
|
validateVariableOverrides(logger, state)
|
|
361
357
|
validateCredentialOverrides(logger, state)
|
package/src/types.ts
CHANGED
|
@@ -179,10 +179,28 @@ export interface InspectorPermissionState {
|
|
|
179
179
|
export type InspectorFilters = {
|
|
180
180
|
names?: string[] // Wildcard support: "email-*" matches "email-worker", "email-sender"
|
|
181
181
|
tags?: string[]
|
|
182
|
-
|
|
182
|
+
wires?: string[]
|
|
183
183
|
directories?: string[]
|
|
184
184
|
httpRoutes?: string[] // HTTP route patterns: "/api/*", "/webhooks/*"
|
|
185
185
|
httpMethods?: string[] // HTTP methods: "GET", "POST", "DELETE", etc.
|
|
186
|
+
|
|
187
|
+
excludeNames?: string[]
|
|
188
|
+
excludeTags?: string[]
|
|
189
|
+
excludeWires?: string[]
|
|
190
|
+
excludeDirectories?: string[]
|
|
191
|
+
excludeHttpRoutes?: string[]
|
|
192
|
+
excludeHttpMethods?: string[]
|
|
193
|
+
|
|
194
|
+
// Keep only functions whose effective deploy target is in this list.
|
|
195
|
+
// A function's effective target is its explicit `deploy` field, or
|
|
196
|
+
// 'server' if any of its services are listed in `serverlessIncompatible`,
|
|
197
|
+
// otherwise 'serverless'.
|
|
198
|
+
target?: Array<'serverless' | 'server'>
|
|
199
|
+
excludeTarget?: Array<'serverless' | 'server'>
|
|
200
|
+
// Service names that, when consumed by a function, force its target
|
|
201
|
+
// to 'server'. Sourced from `pikku.config.json` →
|
|
202
|
+
// `deploy.serverlessIncompatible`. Used only when deploy filters are set.
|
|
203
|
+
serverlessIncompatible?: string[]
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
export type AddonConfig = {
|
|
@@ -192,19 +210,6 @@ export type AddonConfig = {
|
|
|
192
210
|
forceInclude?: boolean
|
|
193
211
|
}
|
|
194
212
|
|
|
195
|
-
export type ModelConfigEntry =
|
|
196
|
-
| string
|
|
197
|
-
| { model: string; temperature?: number; maxSteps?: number }
|
|
198
|
-
|
|
199
|
-
export type InspectorModelConfig = {
|
|
200
|
-
models?: Record<string, ModelConfigEntry>
|
|
201
|
-
agentDefaults?: { temperature?: number; maxSteps?: number }
|
|
202
|
-
agentOverrides?: Record<
|
|
203
|
-
string,
|
|
204
|
-
{ model?: string; temperature?: number; maxSteps?: number }
|
|
205
|
-
>
|
|
206
|
-
}
|
|
207
|
-
|
|
208
213
|
export type InspectorOptions = Partial<{
|
|
209
214
|
setupOnly: boolean
|
|
210
215
|
rootDir: string
|
|
@@ -225,7 +230,6 @@ export type InspectorOptions = Partial<{
|
|
|
225
230
|
}
|
|
226
231
|
tags: string[]
|
|
227
232
|
manifest: VersionManifest
|
|
228
|
-
modelConfig: InspectorModelConfig
|
|
229
233
|
oldProgram: ts.Program | undefined
|
|
230
234
|
}>
|
|
231
235
|
|
|
@@ -444,6 +444,14 @@ export function extractFunctionName(
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
if (result.version !== null) {
|
|
447
|
+
// Strip trailing VN suffix if it matches the version (e.g. createCardV1 + version:1 → createCard@v1)
|
|
448
|
+
const vSuffix = `V${result.version}`
|
|
449
|
+
if (
|
|
450
|
+
result.pikkuFuncId.endsWith(vSuffix) &&
|
|
451
|
+
result.pikkuFuncId.length > vSuffix.length
|
|
452
|
+
) {
|
|
453
|
+
result.pikkuFuncId = result.pikkuFuncId.slice(0, -vSuffix.length)
|
|
454
|
+
}
|
|
447
455
|
result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version)
|
|
448
456
|
}
|
|
449
457
|
|