@pikku/inspector 0.12.3 → 0.12.5

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/add/add-credential.d.ts +2 -0
  3. package/dist/add/add-credential.js +118 -0
  4. package/dist/add/add-middleware.js +6 -10
  5. package/dist/add/add-permission.js +10 -12
  6. package/dist/add/add-secret.d.ts +1 -3
  7. package/dist/add/add-secret.js +0 -74
  8. package/dist/add/add-workflow.js +7 -1
  9. package/dist/error-codes.d.ts +1 -0
  10. package/dist/error-codes.js +2 -0
  11. package/dist/inspector.js +18 -7
  12. package/dist/types.d.ts +7 -0
  13. package/dist/utils/custom-types-generator.js +1 -0
  14. package/dist/utils/post-process.d.ts +9 -0
  15. package/dist/utils/post-process.js +46 -0
  16. package/dist/utils/schema-generator.js +26 -6
  17. package/dist/utils/serialize-inspector-state.d.ts +4 -0
  18. package/dist/utils/serialize-inspector-state.js +9 -0
  19. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +1 -0
  20. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +2 -0
  21. package/dist/visit.js +3 -2
  22. package/package.json +4 -3
  23. package/src/add/add-credential.ts +178 -0
  24. package/src/add/add-middleware.ts +6 -14
  25. package/src/add/add-permission.ts +10 -16
  26. package/src/add/add-secret.ts +0 -131
  27. package/src/add/add-workflow.ts +11 -1
  28. package/src/error-codes.ts +3 -0
  29. package/src/inspector.ts +25 -6
  30. package/src/types.ts +7 -0
  31. package/src/utils/custom-types-generator.ts +1 -0
  32. package/src/utils/post-process.ts +59 -0
  33. package/src/utils/schema-generator.ts +38 -10
  34. package/src/utils/serialize-inspector-state.ts +13 -0
  35. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -0
  36. package/src/utils/workflow/graph/workflow-graph.types.ts +2 -0
  37. package/src/visit.ts +3 -2
  38. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 65eccc6: Cache Zod schema generation between re-inspection passes and batch imports by source file. Schemas are cached using a fingerprint of schemaLookup entries + file mtimes, so reinspections skip Zod generation entirely when schemas haven't changed. Source file imports are grouped so each file is imported once instead of per-schema. Reduces `pikku all` from ~5 minutes to ~13 seconds on projects with many Zod schemas.
8
+ - 0f59432: Add per-user credential system with CredentialService, OAuth2 route handlers, and KyselyCredentialService with envelope encryption
9
+ - Updated dependencies [0f59432]
10
+ - Updated dependencies [52b64d1]
11
+ - @pikku/core@0.12.10
12
+
13
+ ## 0.12.4
14
+
15
+ ### Patch Changes
16
+
17
+ - 5866b66: Add critical error (PKU490) when Zod schemas and wiring calls (wireHTTPRoutes, addPermission, addHTTPMiddleware) coexist in the same file. The CLI uses tsImport to extract Zod schemas at runtime, which executes all top-level code — wiring side-effects crash in this context because pikku state metadata doesn't exist. Schemas and wirings must be in separate files.
18
+ - e412b4d: Optimize CLI codegen performance: 12x faster `pikku all`
19
+
20
+ - Reuse schemas across re-inspections (skip redundant `ts-json-schema-generator` runs)
21
+ - Cache TS schemas to disk (`.pikku/schema-cache.json`) for cross-run reuse
22
+ - Pass `oldProgram` to `ts.createProgram` for incremental TS compilation
23
+ - Cache parsed tsconfig in schema generator between runs
24
+ - Auto-include direct `addPermission`/`addHTTPMiddleware` in bootstrap via side-effect imports
25
+ - Skip `pikkuAuth()` errors when nested inside `addPermission`/`addHTTPPermission`
26
+
27
+ - Updated dependencies [e412b4d]
28
+ - Updated dependencies [53dc8c8]
29
+ - Updated dependencies [0a1cc51]
30
+ - Updated dependencies [0a1cc51]
31
+ - Updated dependencies [0a1cc51]
32
+ - Updated dependencies [0a1cc51]
33
+ - Updated dependencies [0a1cc51]
34
+ - Updated dependencies [0a1cc51]
35
+ - Updated dependencies [0a1cc51]
36
+ - Updated dependencies [0a1cc51]
37
+ - Updated dependencies [0a1cc51]
38
+ - Updated dependencies [8b9b2e9]
39
+ - Updated dependencies [8b9b2e9]
40
+ - Updated dependencies [b973d44]
41
+ - Updated dependencies [8b9b2e9]
42
+ - Updated dependencies [8b9b2e9]
43
+ - @pikku/core@0.12.9
44
+
3
45
  ## 0.12.3
4
46
 
5
47
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addCredential: AddWiring;
@@ -0,0 +1,118 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue, getArrayPropertyValue, } from '../utils/get-property-value.js';
3
+ import { ErrorCode } from '../error-codes.js';
4
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
5
+ export const addCredential = (logger, node, checker, state, _options) => {
6
+ if (!ts.isCallExpression(node)) {
7
+ return;
8
+ }
9
+ const args = node.arguments;
10
+ const firstArg = args[0];
11
+ const expression = node.expression;
12
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireCredential') {
13
+ return;
14
+ }
15
+ if (!firstArg) {
16
+ return;
17
+ }
18
+ if (ts.isObjectLiteralExpression(firstArg)) {
19
+ const obj = firstArg;
20
+ const nameValue = getPropertyValue(obj, 'name');
21
+ const displayNameValue = getPropertyValue(obj, 'displayName');
22
+ const descriptionValue = getPropertyValue(obj, 'description');
23
+ const typeValue = getPropertyValue(obj, 'type');
24
+ if (!nameValue) {
25
+ logger.critical(ErrorCode.MISSING_NAME, "Credential is missing the required 'name' property.");
26
+ return;
27
+ }
28
+ if (!displayNameValue) {
29
+ logger.critical(ErrorCode.MISSING_NAME, `Credential '${nameValue}' is missing the required 'displayName' property.`);
30
+ return;
31
+ }
32
+ if (!typeValue || (typeValue !== 'singleton' && typeValue !== 'wire')) {
33
+ logger.critical(ErrorCode.MISSING_NAME, `Credential '${nameValue}' is missing or has invalid 'type' property. Must be 'singleton' or 'wire'.`);
34
+ return;
35
+ }
36
+ let schemaVariableName = null;
37
+ let schemaSourceFile = null;
38
+ let schemaIdentifier = null;
39
+ for (const prop of obj.properties) {
40
+ if (ts.isPropertyAssignment(prop) &&
41
+ ts.isIdentifier(prop.name) &&
42
+ prop.name.text === 'schema') {
43
+ if (ts.isIdentifier(prop.initializer)) {
44
+ schemaVariableName = prop.initializer.text;
45
+ schemaIdentifier = prop.initializer;
46
+ const symbol = checker.getSymbolAtLocation(prop.initializer);
47
+ if (symbol) {
48
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0];
49
+ if (decl) {
50
+ if (ts.isImportSpecifier(decl)) {
51
+ const aliasedSymbol = checker.getAliasedSymbol(symbol);
52
+ if (aliasedSymbol) {
53
+ const aliasedDecl = aliasedSymbol.valueDeclaration ||
54
+ aliasedSymbol.declarations?.[0];
55
+ if (aliasedDecl) {
56
+ schemaSourceFile = aliasedDecl.getSourceFile().fileName;
57
+ }
58
+ }
59
+ }
60
+ else {
61
+ schemaSourceFile = decl.getSourceFile().fileName;
62
+ }
63
+ }
64
+ }
65
+ }
66
+ break;
67
+ }
68
+ }
69
+ let oauth2 = undefined;
70
+ const oauth2Prop = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
71
+ ts.isIdentifier(p.name) &&
72
+ p.name.text === 'oauth2');
73
+ if (oauth2Prop &&
74
+ ts.isPropertyAssignment(oauth2Prop) &&
75
+ ts.isObjectLiteralExpression(oauth2Prop.initializer)) {
76
+ const oauth2Obj = oauth2Prop.initializer;
77
+ const appCredentialSecretId = getPropertyValue(oauth2Obj, 'appCredentialSecretId');
78
+ const tokenSecretId = getPropertyValue(oauth2Obj, 'tokenSecretId');
79
+ const authorizationUrl = getPropertyValue(oauth2Obj, 'authorizationUrl');
80
+ const tokenUrl = getPropertyValue(oauth2Obj, 'tokenUrl');
81
+ const scopes = getArrayPropertyValue(oauth2Obj, 'scopes');
82
+ const pkce = getPropertyValue(oauth2Obj, 'pkce');
83
+ if (appCredentialSecretId && authorizationUrl && tokenUrl && scopes) {
84
+ oauth2 = {
85
+ appCredentialSecretId,
86
+ tokenSecretId: tokenSecretId || undefined,
87
+ authorizationUrl,
88
+ tokenUrl,
89
+ scopes,
90
+ pkce: pkce || undefined,
91
+ };
92
+ }
93
+ }
94
+ const sourceFile = node.getSourceFile().fileName;
95
+ state.credentials.files.add(sourceFile);
96
+ let schemaLookupName;
97
+ if (schemaVariableName && schemaSourceFile && schemaIdentifier) {
98
+ const vendor = detectSchemaVendorOrError(schemaIdentifier, checker, logger, `Credential '${nameValue}'`, schemaSourceFile);
99
+ if (vendor) {
100
+ schemaLookupName = `CredentialSchema_${nameValue}`;
101
+ state.schemaLookup.set(schemaLookupName, {
102
+ variableName: schemaVariableName,
103
+ sourceFile: schemaSourceFile,
104
+ vendor,
105
+ });
106
+ }
107
+ }
108
+ state.credentials.definitions.push({
109
+ name: nameValue,
110
+ displayName: displayNameValue,
111
+ description: descriptionValue || undefined,
112
+ type: typeValue,
113
+ schema: schemaLookupName,
114
+ oauth2,
115
+ sourceFile,
116
+ });
117
+ }
118
+ };
@@ -189,12 +189,10 @@ export const addMiddleware = (logger, node, checker, state) => {
189
189
  return;
190
190
  }
191
191
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
192
- if (refs.length === 0) {
193
- logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`);
194
- return;
195
- }
196
192
  const definitionIds = refs.map((r) => r.definitionId);
197
- renameTempDefinitions(state, definitionIds, 'tag', tag);
193
+ if (definitionIds.length > 0) {
194
+ renameTempDefinitions(state, definitionIds, 'tag', tag);
195
+ }
198
196
  const sourceFile = node.getSourceFile().fileName;
199
197
  const instanceIds = [];
200
198
  for (let i = 0; i < refs.length; i++) {
@@ -273,12 +271,10 @@ export const addMiddleware = (logger, node, checker, state) => {
273
271
  return;
274
272
  }
275
273
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
276
- if (refs.length === 0) {
277
- logger.warn(`• addHTTPMiddleware('${pattern}', ...) has empty middleware array`);
278
- return;
279
- }
280
274
  const definitionIds = refs.map((r) => r.definitionId);
281
- renameTempDefinitions(state, definitionIds, 'http', pattern);
275
+ if (definitionIds.length > 0) {
276
+ renameTempDefinitions(state, definitionIds, 'http', pattern);
277
+ }
282
278
  const sourceFile = node.getSourceFile().fileName;
283
279
  const instanceIds = [];
284
280
  for (let i = 0; i < refs.length; i++) {
@@ -21,12 +21,14 @@ function renameTempDefinitions(state, definitionIds, groupType, groupKey) {
21
21
  definitionIds[idx] = newId;
22
22
  }
23
23
  }
24
- function isInsidePermissionFactory(node) {
24
+ function isInsidePermissionContainer(node) {
25
25
  let current = node.parent;
26
26
  while (current) {
27
27
  if (ts.isCallExpression(current) &&
28
28
  ts.isIdentifier(current.expression) &&
29
- current.expression.text === 'pikkuPermissionFactory') {
29
+ (current.expression.text === 'pikkuPermissionFactory' ||
30
+ current.expression.text === 'addPermission' ||
31
+ current.expression.text === 'addHTTPPermission')) {
30
32
  return true;
31
33
  }
32
34
  current = current.parent;
@@ -47,7 +49,7 @@ export const addPermission = (logger, node, checker, state) => {
47
49
  // Handle pikkuPermission(...) - individual permission function definition
48
50
  if (expression.text === 'pikkuPermission') {
49
51
  // Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
50
- if (isInsidePermissionFactory(node))
52
+ if (isInsidePermissionContainer(node))
51
53
  return;
52
54
  const arg = args[0];
53
55
  if (!arg)
@@ -110,7 +112,7 @@ export const addPermission = (logger, node, checker, state) => {
110
112
  return;
111
113
  }
112
114
  if (expression.text === 'pikkuAuth') {
113
- if (isInsidePermissionFactory(node))
115
+ if (isInsidePermissionContainer(node))
114
116
  return;
115
117
  const arg = args[0];
116
118
  if (!arg)
@@ -269,11 +271,9 @@ export const addPermission = (logger, node, checker, state) => {
269
271
  }
270
272
  // Extract permission pikkuFuncIds from array
271
273
  const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
272
- if (permissionNames.length === 0) {
273
- logger.warn(`• addPermission('${tag}', ...) has empty permissions array`);
274
- return;
274
+ if (permissionNames.length > 0) {
275
+ renameTempDefinitions(state, permissionNames, 'tag', tag);
275
276
  }
276
- renameTempDefinitions(state, permissionNames, 'tag', tag);
277
277
  const allServices = new Set();
278
278
  for (const permissionName of permissionNames) {
279
279
  const permissionMeta = state.permissions.definitions[permissionName];
@@ -348,11 +348,9 @@ export const addPermission = (logger, node, checker, state) => {
348
348
  }
349
349
  // Extract permission pikkuFuncIds from array
350
350
  const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
351
- if (permissionNames.length === 0) {
352
- logger.warn(`• addHTTPPermission('${pattern}', ...) has empty permissions array`);
353
- return;
351
+ if (permissionNames.length > 0) {
352
+ renameTempDefinitions(state, permissionNames, 'http', pattern);
354
353
  }
355
- renameTempDefinitions(state, permissionNames, 'http', pattern);
356
354
  const allServices = new Set();
357
355
  for (const permissionName of permissionNames) {
358
356
  const permissionMeta = state.permissions.definitions[permissionName];
@@ -1,3 +1 @@
1
- import type { AddWiring } from '../types.js';
2
- export declare const addSecret: AddWiring;
3
- export declare const addOAuth2Credential: AddWiring;
1
+ export declare const addSecret: import("../types.js").AddWiring;
@@ -1,6 +1,3 @@
1
- import * as ts from 'typescript';
2
- import { getPropertyValue, getArrayPropertyValue, } from '../utils/get-property-value.js';
3
- import { ErrorCode } from '../error-codes.js';
4
1
  import { createAddKeyedWiring } from './add-keyed-wiring.js';
5
2
  export const addSecret = createAddKeyedWiring({
6
3
  functionName: 'wireSecret',
@@ -9,74 +6,3 @@ export const addSecret = createAddKeyedWiring({
9
6
  schemaPrefix: 'SecretSchema',
10
7
  getState: (state) => state.secrets,
11
8
  });
12
- export const addOAuth2Credential = (logger, node, _checker, state, _options) => {
13
- if (!ts.isCallExpression(node)) {
14
- return;
15
- }
16
- const args = node.arguments;
17
- const firstArg = args[0];
18
- const expression = node.expression;
19
- if (!ts.isIdentifier(expression) ||
20
- expression.text !== 'wireOAuth2Credential') {
21
- return;
22
- }
23
- if (!firstArg) {
24
- return;
25
- }
26
- if (ts.isObjectLiteralExpression(firstArg)) {
27
- const obj = firstArg;
28
- const nameValue = getPropertyValue(obj, 'name');
29
- const displayNameValue = getPropertyValue(obj, 'displayName');
30
- const descriptionValue = getPropertyValue(obj, 'description');
31
- const secretIdValue = getPropertyValue(obj, 'secretId');
32
- const tokenSecretIdValue = getPropertyValue(obj, 'tokenSecretId');
33
- const authorizationUrlValue = getPropertyValue(obj, 'authorizationUrl');
34
- const tokenUrlValue = getPropertyValue(obj, 'tokenUrl');
35
- const scopesValue = getArrayPropertyValue(obj, 'scopes');
36
- const pkceValue = getPropertyValue(obj, 'pkce');
37
- if (!nameValue) {
38
- logger.critical(ErrorCode.MISSING_NAME, "OAuth2 Credential is missing the required 'name' property.");
39
- return;
40
- }
41
- if (!displayNameValue) {
42
- logger.critical(ErrorCode.MISSING_NAME, `OAuth2 Credential '${nameValue}' is missing the required 'displayName' property.`);
43
- return;
44
- }
45
- if (!secretIdValue) {
46
- logger.critical(ErrorCode.MISSING_NAME, `OAuth2 Credential '${nameValue}' is missing the required 'secretId' property.`);
47
- return;
48
- }
49
- if (!tokenSecretIdValue) {
50
- logger.critical(ErrorCode.MISSING_NAME, `OAuth2 Credential '${nameValue}' is missing the required 'tokenSecretId' property.`);
51
- return;
52
- }
53
- if (!authorizationUrlValue) {
54
- logger.critical(ErrorCode.MISSING_NAME, `OAuth2 Credential '${nameValue}' is missing the required 'authorizationUrl' property.`);
55
- return;
56
- }
57
- if (!tokenUrlValue) {
58
- logger.critical(ErrorCode.MISSING_NAME, `OAuth2 Credential '${nameValue}' is missing the required 'tokenUrl' property.`);
59
- return;
60
- }
61
- if (!scopesValue || scopesValue.length === 0) {
62
- logger.critical(ErrorCode.MISSING_NAME, `OAuth2 Credential '${nameValue}' is missing the required 'scopes' property.`);
63
- return;
64
- }
65
- const sourceFile = node.getSourceFile().fileName;
66
- state.secrets.files.add(sourceFile);
67
- state.secrets.definitions.push({
68
- name: nameValue,
69
- displayName: displayNameValue,
70
- description: descriptionValue || undefined,
71
- secretId: secretIdValue,
72
- oauth2: {
73
- tokenSecretId: tokenSecretIdValue,
74
- authorizationUrl: authorizationUrlValue,
75
- tokenUrl: tokenUrlValue,
76
- scopes: scopesValue,
77
- pkce: pkceValue || undefined,
78
- },
79
- sourceFile,
80
- });
81
- }
82
- };
@@ -3,7 +3,7 @@ import { extractFunctionName } from '../utils/extract-function-name.js';
3
3
  import { extractFunctionNode } from '../utils/extract-function-node.js';
4
4
  import { ErrorCode } from '../error-codes.js';
5
5
  import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
6
- import { getCommonWireMetaData } from '../utils/get-property-value.js';
6
+ import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
7
7
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
8
8
  /**
9
9
  * Recursively check if any step has inline type (non-serializable)
@@ -182,6 +182,7 @@ export const addWorkflow = (logger, node, checker, state) => {
182
182
  let summary;
183
183
  let description;
184
184
  let errors;
185
+ let inline;
185
186
  if (ts.isObjectLiteralExpression(firstArg)) {
186
187
  const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger);
187
188
  if (metadata.disabled)
@@ -190,6 +191,10 @@ export const addWorkflow = (logger, node, checker, state) => {
190
191
  summary = metadata.summary;
191
192
  description = metadata.description;
192
193
  errors = metadata.errors;
194
+ const inlineProp = getPropertyValue(firstArg, 'inline');
195
+ if (inlineProp === true) {
196
+ inline = true;
197
+ }
193
198
  }
194
199
  // Validate that we got a valid function
195
200
  if (ts.isObjectLiteralExpression(firstArg) &&
@@ -272,5 +277,6 @@ export const addWorkflow = (logger, node, checker, state) => {
272
277
  description,
273
278
  errors,
274
279
  tags,
280
+ inline,
275
281
  };
276
282
  };
@@ -49,6 +49,7 @@ export declare enum ErrorCode {
49
49
  MANIFEST_INTEGRITY_ERROR = "PKU865",
50
50
  MISSING_MODEL = "PKU145",
51
51
  INVALID_MODEL = "PKU146",
52
+ SCHEMA_AND_WIRING_COLOCATED = "PKU490",
52
53
  SERVICES_NOT_DESTRUCTURED = "PKU410",
53
54
  WIRES_NOT_DESTRUCTURED = "PKU411",
54
55
  WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901"
@@ -58,6 +58,8 @@ export var ErrorCode;
58
58
  // Model configuration errors
59
59
  ErrorCode["MISSING_MODEL"] = "PKU145";
60
60
  ErrorCode["INVALID_MODEL"] = "PKU146";
61
+ // File structure errors
62
+ ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
61
63
  // Optimization diagnostics
62
64
  ErrorCode["SERVICES_NOT_DESTRUCTURED"] = "PKU410";
63
65
  ErrorCode["WIRES_NOT_DESTRUCTURED"] = "PKU411";
package/dist/inspector.js CHANGED
@@ -4,7 +4,7 @@ import { visitSetup, visitRoutes } from './visit.js';
4
4
  import { TypesMap } from './types-map.js';
5
5
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
6
6
  import { findCommonAncestor } from './utils/find-root-dir.js';
7
- import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, } from './utils/post-process.js';
7
+ import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
8
8
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
9
9
  import { pikkuState } from '@pikku/core/internal';
10
10
  import { resolveLatestVersions } from './utils/resolve-versions.js';
@@ -118,6 +118,10 @@ export function getInitialInspectorState(rootDir) {
118
118
  definitions: [],
119
119
  files: new Set(),
120
120
  },
121
+ credentials: {
122
+ definitions: [],
123
+ files: new Set(),
124
+ },
121
125
  variables: {
122
126
  definitions: [],
123
127
  files: new Set(),
@@ -174,11 +178,11 @@ export function getInitialInspectorState(rootDir) {
174
178
  openAPISpec: null,
175
179
  diagnostics: [],
176
180
  addonFunctions: {},
181
+ program: null,
177
182
  };
178
183
  }
179
184
  export const inspect = async (logger, routeFiles, options = {}) => {
180
- const startProgram = performance.now();
181
- const program = ts.createProgram(routeFiles, {
185
+ const compilerOptions = {
182
186
  target: ts.ScriptTarget.ESNext,
183
187
  module: ts.ModuleKind.Node16,
184
188
  skipLibCheck: true,
@@ -187,8 +191,11 @@ export const inspect = async (logger, routeFiles, options = {}) => {
187
191
  types: [],
188
192
  allowJs: false,
189
193
  checkJs: false,
190
- });
191
- logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(2)}ms`);
194
+ };
195
+ const startProgram = performance.now();
196
+ const program = ts.createProgram(routeFiles, compilerOptions, undefined, // host
197
+ options.oldProgram);
198
+ logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
192
199
  const startChecker = performance.now();
193
200
  const checker = program.getTypeChecker();
194
201
  logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
@@ -207,7 +214,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
207
214
  for (const sourceFile of sourceFiles) {
208
215
  ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, options));
209
216
  }
210
- logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`);
217
+ logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
211
218
  // Load addon function metadata so wirings can reference addon functions
212
219
  await loadAddonFunctionsMeta(logger, state);
213
220
  if (!options.setupOnly) {
@@ -216,10 +223,12 @@ export const inspect = async (logger, routeFiles, options = {}) => {
216
223
  for (const sourceFile of sourceFiles) {
217
224
  ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, options));
218
225
  }
219
- logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`);
226
+ logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
220
227
  resolveLatestVersions(state, logger);
221
228
  if (options.schemaConfig) {
229
+ const startSchemas = performance.now();
222
230
  state.schemas = await generateAllSchemas(logger, options.schemaConfig, state);
231
+ logger.debug(`generateAllSchemas took ${(performance.now() - startSchemas).toFixed(0)}ms`);
223
232
  computeContractHashes(state.schemas, state.functions.typesMap, state.functions.meta);
224
233
  computeRequiredSchemas(state, options);
225
234
  }
@@ -248,6 +257,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
248
257
  computeMiddlewareGroupsMeta(state);
249
258
  computePermissionsGroupsMeta(state);
250
259
  computeDiagnostics(state);
260
+ validateSchemaWiringSeparation(logger, state);
251
261
  if (options.openAPI) {
252
262
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
253
263
  }
@@ -256,5 +266,6 @@ export const inspect = async (logger, routeFiles, options = {}) => {
256
266
  validateSecretOverrides(logger, state);
257
267
  validateVariableOverrides(logger, state);
258
268
  }
269
+ state.program = program;
259
270
  return state;
260
271
  };
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ import type { AIAgentMeta } from '@pikku/core/ai-agent';
11
11
  import type { CLIMeta } from '@pikku/core/cli';
12
12
  import type { NodesMeta } from '@pikku/core/node';
13
13
  import type { SecretDefinitions } from '@pikku/core/secret';
14
+ import type { CredentialDefinitions } from '@pikku/core/credential';
14
15
  import type { VariableDefinitions } from '@pikku/core/variable';
15
16
  import type { TypesMap } from './types-map.js';
16
17
  import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
@@ -193,6 +194,7 @@ export type InspectorOptions = Partial<{
193
194
  tags: string[];
194
195
  manifest: VersionManifest;
195
196
  modelConfig: InspectorModelConfig;
197
+ oldProgram: ts.Program | undefined;
196
198
  }>;
197
199
  export interface InspectorLogger {
198
200
  info: (message: string) => void;
@@ -348,6 +350,10 @@ export interface InspectorState {
348
350
  definitions: SecretDefinitions;
349
351
  files: Set<string>;
350
352
  };
353
+ credentials: {
354
+ definitions: CredentialDefinitions;
355
+ files: Set<string>;
356
+ };
351
357
  variables: {
352
358
  definitions: VariableDefinitions;
353
359
  files: Set<string>;
@@ -393,4 +399,5 @@ export interface InspectorState {
393
399
  openAPISpec: Record<string, any> | null;
394
400
  diagnostics: InspectorDiagnostic[];
395
401
  addonFunctions: Record<string, FunctionsMeta>;
402
+ program: ts.Program | null;
396
403
  }
@@ -9,6 +9,7 @@ export function sanitizeTypeName(name) {
9
9
  }
10
10
  export function generateCustomTypes(typesMap, requiredTypes) {
11
11
  const typeDeclarations = Array.from(typesMap.customTypes.entries())
12
+ .sort(([a], [b]) => a.localeCompare(b))
12
13
  .filter(([_name, { type }]) => {
13
14
  const hasUndefinedGeneric = /\b(Name|In|Out|Key)\b/.test(type) && /\[.*\]/.test(type);
14
15
  return !hasUndefinedGeneric;
@@ -22,4 +22,13 @@ export declare function computePermissionsGroupsMeta(state: InspectorState): voi
22
22
  export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
23
23
  export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
24
24
  export declare function validateAgentOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
25
+ /**
26
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
27
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
28
+ *
29
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
30
+ * all top-level code in the file. Wiring calls crash during this process
31
+ * because the pikku state metadata doesn't exist in the CLI context.
32
+ */
33
+ export declare function validateSchemaWiringSeparation(logger: InspectorLogger, state: InspectorState): void;
25
34
  export declare function computeDiagnostics(state: InspectorState): void;
@@ -378,6 +378,52 @@ export function validateAgentOverrides(logger, state, modelConfig) {
378
378
  }
379
379
  }
380
380
  }
381
+ /**
382
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
383
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
384
+ *
385
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
386
+ * all top-level code in the file. Wiring calls crash during this process
387
+ * because the pikku state metadata doesn't exist in the CLI context.
388
+ */
389
+ export function validateSchemaWiringSeparation(logger, state) {
390
+ // Collect files that contain schemas
391
+ const schemaFiles = new Set();
392
+ for (const ref of state.schemaLookup.values()) {
393
+ schemaFiles.add(ref.sourceFile);
394
+ }
395
+ // Collect files that contain wiring side-effects
396
+ const wiringFiles = new Set();
397
+ // HTTP route wirings
398
+ for (const file of state.http.files) {
399
+ wiringFiles.add(file);
400
+ }
401
+ // Permission wirings (addPermission calls)
402
+ for (const meta of state.permissions.tagPermissions.values()) {
403
+ wiringFiles.add(meta.sourceFile);
404
+ }
405
+ for (const meta of state.http.routePermissions.values()) {
406
+ wiringFiles.add(meta.sourceFile);
407
+ }
408
+ // Middleware wirings (addHTTPMiddleware calls)
409
+ for (const meta of state.http.routeMiddleware.values()) {
410
+ wiringFiles.add(meta.sourceFile);
411
+ }
412
+ for (const meta of state.middleware.tagMiddleware.values()) {
413
+ wiringFiles.add(meta.sourceFile);
414
+ }
415
+ // Check for overlap
416
+ for (const file of schemaFiles) {
417
+ if (wiringFiles.has(file)) {
418
+ const schemas = Array.from(state.schemaLookup.entries())
419
+ .filter(([, ref]) => ref.sourceFile === file)
420
+ .map(([name]) => name);
421
+ logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(', ')}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
422
+ `These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
423
+ `Move the route/wiring definitions to a dedicated wiring file.`);
424
+ }
425
+ }
426
+ }
381
427
  export function computeDiagnostics(state) {
382
428
  const diagnostics = [];
383
429
  for (const [id, meta] of Object.entries(state.functions.meta)) {
@@ -46,14 +46,25 @@ function primitiveTypeToSchema(typeStr) {
46
46
  }
47
47
  return null;
48
48
  }
49
+ // Cached state for schema program reuse across inspect() calls
50
+ let cachedSchemaProgram;
51
+ let cachedParsedConfig;
52
+ let cachedTsconfigPath;
53
+ let cachedCustomTypesContent;
54
+ let cachedTSSchemas;
49
55
  function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileContent) {
50
56
  const configPath = resolve(tsconfig);
51
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
52
- const basePath = dirname(configPath);
53
- const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
57
+ // Cache the parsed tsconfig — it doesn't change between runs
58
+ if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
59
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
60
+ const basePath = dirname(configPath);
61
+ cachedParsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
62
+ cachedTsconfigPath = configPath;
63
+ cachedSchemaProgram = undefined;
64
+ }
54
65
  const resolvedVirtualPath = resolve(virtualFilePath);
55
- const fileNames = [...parsedConfig.fileNames, resolvedVirtualPath];
56
- const defaultHost = ts.createCompilerHost(parsedConfig.options);
66
+ const fileNames = [...cachedParsedConfig.fileNames, resolvedVirtualPath];
67
+ const defaultHost = ts.createCompilerHost(cachedParsedConfig.options);
57
68
  const customHost = {
58
69
  ...defaultHost,
59
70
  getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) {
@@ -73,7 +84,10 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
73
84
  return defaultHost.readFile(fileName);
74
85
  },
75
86
  };
76
- return ts.createProgram(fileNames, parsedConfig.options, customHost);
87
+ const program = ts.createProgram(fileNames, cachedParsedConfig.options, customHost, cachedSchemaProgram // reuse previous program for incremental compilation
88
+ );
89
+ cachedSchemaProgram = program;
90
+ return program;
77
91
  }
78
92
  function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, schemaLookup) {
79
93
  const schemasSet = new Set(typesMap.customTypes.keys());
@@ -204,6 +218,12 @@ export async function generateAllSchemas(logger, config, state) {
204
218
  const zodSchemas = await generateZodSchemas(logger, state.schemaLookup, state.functions.typesMap);
205
219
  const requiredTypes = new Set();
206
220
  const customTypesContent = generateCustomTypes(state.functions.typesMap, requiredTypes);
221
+ if (cachedTSSchemas && cachedCustomTypesContent === customTypesContent) {
222
+ logger.debug('Reusing cached TS schemas (types unchanged)');
223
+ return { ...cachedTSSchemas, ...zodSchemas };
224
+ }
207
225
  const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, state.schemaLookup);
226
+ cachedCustomTypesContent = customTypesContent;
227
+ cachedTSSchemas = tsSchemas;
208
228
  return { ...tsSchemas, ...zodSchemas };
209
229
  }