@pikku/inspector 0.12.43 → 0.12.45

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 (36) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/dist/add/add-credential.js +2 -0
  3. package/dist/add/add-keyed-wiring.js +2 -0
  4. package/dist/add/add-workflow.js +9 -4
  5. package/dist/utils/schema-generator.js +19 -4
  6. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
  7. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +218 -99
  8. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +62 -5
  9. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +3 -3
  10. package/package.json +3 -3
  11. package/run-tests.sh +1 -1
  12. package/src/add/add-auth.test.ts +4 -4
  13. package/src/add/add-credential.ts +2 -0
  14. package/src/add/add-keyed-wiring.ts +2 -0
  15. package/src/add/add-workflow-edge-cases.test.ts +201 -0
  16. package/src/add/add-workflow-fanout.test.ts +60 -0
  17. package/src/add/add-workflow-on-error.test.ts +118 -0
  18. package/src/add/add-workflow-return-switch.test.ts +121 -0
  19. package/src/add/add-workflow-silent-drops.test.ts +163 -0
  20. package/src/add/add-workflow-sleep-expression.test.ts +97 -0
  21. package/src/add/add-workflow.ts +9 -4
  22. package/src/add/config-value-metadata.test.ts +169 -0
  23. package/src/utils/schema-generator.test.ts +164 -0
  24. package/src/utils/schema-generator.ts +26 -3
  25. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
  26. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
  27. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
  28. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
  29. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
  30. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
  31. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
  32. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +245 -105
  33. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +67 -10
  34. package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
  35. package/src/utils/workflow/graph/workflow-graph.types.ts +3 -2
  36. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,264 @@
1
+ ## 0.12.45
2
+
3
+ ### Patch Changes
4
+
5
+ - ae4f59a: Gate admin capabilities on scopes, and scaffold user management
6
+
7
+ Admin capabilities were gated on `user.role === 'admin'` — a single text column
8
+ meaning "can do everything". Impersonating a user, rebinding a shared
9
+ credential and reading the user directory are distinct capabilities that one
10
+ user can hold independently, so they are now scopes on an `admin` tree:
11
+
12
+ | Gate | Scope |
13
+ | -------------------------------------- | ------------------------ |
14
+ | impersonation | `admin:impersonate` |
15
+ | `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
16
+ | reading the user directory | `admin:users:list` |
17
+ | creating a user out of band | `admin:users:create` |
18
+ | ban / unban | `admin:users:ban` |
19
+ | delete a user | `admin:users:remove` |
20
+ | revoke a user's sessions | `admin:users:sessions` |
21
+ | set a user's password | `admin:users:password` |
22
+
23
+ Holding the bare `admin` scope satisfies all of them via pikku's existing
24
+ parent-grant rule, so it is a one-for-one replacement for the old role.
25
+
26
+ better-auth's `admin()` plugin is still what implements ban, delete,
27
+ session-revocation and set-password, so it stays. Its `user.role` column is no
28
+ longer something pikku grants: it is _projected_ from the scope store when a
29
+ session is built, and only from the scopes whose capability better-auth's own
30
+ endpoints gate on the caller's role. Someone granted `admin:users:list` can read
31
+ the directory — which goes straight to the auth adapter — without gaining the
32
+ power to ban, and revoking a scope demotes the role on the next sign-in. Scopes
33
+ remain the single source of truth.
34
+
35
+ New `scaffold.userAdmin` in `pikku.config.json` generates the whole set —
36
+ `pikkuAdminListUsers`, `pikkuAdminCreateUser`, `pikkuAdminSetUserBanned`,
37
+ `pikkuAdminRemoveUser`, `pikkuAdminRevokeUserSessions` and
38
+ `pikkuAdminSetUserPassword` — into your project. Listing or banning a user is
39
+ ordinary application behaviour and must not require installing the console.
40
+ Codegen fails with an actionable error if better-auth is wired without
41
+ `admin()`. The console's Users page calls these same functions, showing each
42
+ action only where the caller holds its scope.
43
+
44
+ Every scaffold now emits a directory named for its domain — `scaffold/admin/`,
45
+ `scaffold/rpc/`, `scaffold/agent/`, `scaffold/auth/`, `scaffold/console/`,
46
+ `scaffold/graph/`, `scaffold/realtime/`, `scaffold/scenarios/`,
47
+ `scaffold/webhook/`, `scaffold/workflow/` — holding its wiring file beside a
48
+ `*.schemas.gen.ts` sibling, and every generated payload is a zod schema instead
49
+ of a TypeScript generic. The schemas have to stand alone: the inspector reads a
50
+ zod schema by importing the module that declares it, which it cannot do for a
51
+ wiring file whose relative pikku-types import per-unit deploy codegen rewrites.
52
+
53
+ Resolving a schema by reference rather than by name also fixes the agent HTTP
54
+ surface. `agentCaller` and `agentStreamCaller` take the same payload but had to
55
+ repeat the type literal verbatim in each generic position, because the extractor
56
+ synthesised the schema name from the _function_ name and so recorded an
57
+ `inputSchemaName` with no schema behind it whenever the two shared a named
58
+ alias — every agent call through that alias failed with `MissingSchemaError`.
59
+ One `AgentCall` schema now backs both.
60
+
61
+ Where a payload's shape belongs to `@pikku/core` (`WorkflowRunStatus`,
62
+ `FunctionCoverageReport`, `StubCall[]`) the generated function carries no
63
+ `output` schema and the inspector infers it from the handler's return type;
64
+ re-declaring a core type in zod would be a second definition free to drift.
65
+
66
+ Upgrading rewrites the layout in place: codegen prunes the pre-directory copy of
67
+ each scaffold file before it inspects the source tree, since the old flat file
68
+ still wires the same routes and leaving it behind would wire everything twice.
69
+
70
+ `@pikku/core` gains `hasScopes(required, held)`, the non-throwing counterpart to
71
+ `verifyScopes`, and declares `auth` on `CoreSingletonServices` — the auth
72
+ instance the generated `pikkuServices` wrapper already injected but never typed.
73
+ A scope root declared twice (an addon and its host both contributing the same
74
+ `admin` tree) now flattens to one entry per id instead of emitting it twice.
75
+
76
+ BREAKING: there is no role fallback for the scope-gated capabilities. An app
77
+ that relied on the old default must register a `ScopeService` and grant `admin`
78
+ (or a narrower `admin:*` scope). Every gate fails closed and warns when no
79
+ `ScopeService` is registered. `delegatedAuth`'s `defaultRole`/`mapRole` now
80
+ grant a pikku role through the `ScopeService` instead of writing better-auth's
81
+ `role` column, and the `credentialOAuth` platform user no longer sets `banned`.
82
+
83
+ BREAKING: the console reads its user directory over the scaffolded
84
+ `pikkuAdminListUsers` RPC (gated on `admin:users:list`, backed by better-auth's
85
+ `$context.adapter`) instead of `client.admin.listUsers`, and
86
+ `UsersTableUser`/`UsersTableLabels` no longer carry `role` — there is no role
87
+ column to render. `@pikku/addon-console` no longer ships a `console:listUsers`
88
+ function: user management is not the console's job, so a host that wants the
89
+ Users page must enable `scaffold.userAdmin`.
90
+
91
+ - Updated dependencies [ae4f59a]
92
+ - @pikku/core@0.12.67
93
+
94
+ ## 0.12.44
95
+
96
+ ### Patch Changes
97
+
98
+ - 1a86d3f: Stop silently dropping DSL workflow constructs during extraction.
99
+
100
+ Four constructs produced a wrong workflow graph with no diagnostic:
101
+ - A property access on the fanout item (`users.map((u) => workflow.do(..., { userId: u.id }))`)
102
+ was dropped from the step's inputs — `extractInputSource` resolved property
103
+ access against the input param and output variables but not loop variables.
104
+ - `const [org, user] = await Promise.all([...])` dropped **both** steps, because
105
+ extraction bailed on any non-identifier binding name before reaching the
106
+ `Promise.all` branch. Array destructuring now binds each name to its matching
107
+ child step's output.
108
+ - A brace-less `for (const x of xs) await workflow.do(...)` dropped the entire
109
+ loop, since only block bodies were walked.
110
+ - Object destructuring of a step result and multi-declarator statements now
111
+ report a diagnostic instead of vanishing.
112
+
113
+ - 1a86d3f: Fix a fanout collapsing into a single step, and preserve graph node config.
114
+ - A fanout took its `stepName` from the first step of its body. Node ids _are_
115
+ step names, so the loop and that step got the same id and the step overwrote
116
+ the loop: `await Promise.all(users.map(...))` rendered as one plain call, and
117
+ everything after the loop became unreachable. A fanout is not itself a cached
118
+ step, so it no longer borrows a name.
119
+ - A `workflow.sleep` or `workflow.suspend` inside a fanout body was dropped at
120
+ extraction — `FanoutStepMeta.body` was typed RPC-only. It now admits sleep and
121
+ suspend, and the regenerated body emits them.
122
+ - Regenerating a `pikkuWorkflowGraph` dropped `onError`, `retries` and
123
+ `retryDelay` from every node, and graph-level `notes`. All four are honoured
124
+ at runtime, so the round trip silently changed behaviour.
125
+
126
+ - 1a86d3f: Support multi-step fanout bodies in DSL workflows.
127
+
128
+ A `Promise.all(array.map(...))` (or `for...of`) body containing more than one
129
+ `workflow.do` call previously extracted only a single step: `const`-captured
130
+ steps were skipped entirely by the parallel extractor, so a body like
131
+
132
+ ```ts
133
+ await Promise.all(
134
+ users.map(async (u) => {
135
+ const digestData = await workflow.do('Get pipeline', 'getDigestData', {
136
+ userId: u.id,
137
+ })
138
+ await workflow.do('Send digest', 'sendDigestEmail', { ...digestData })
139
+ })
140
+ )
141
+ ```
142
+
143
+ produced a graph with `getDigestData` missing and `sendDigestEmail` referencing
144
+ an unregistered variable. `FanoutStepMeta.child` is replaced by
145
+ `FanoutStepMeta.body: RpcStepMeta[]`, holding the per-iteration steps inline in
146
+ the same workflow — no sub-workflow boundary. Per-iteration `const` bindings are
147
+ now registered so later steps in the same iteration can reference them, and the
148
+ sequential path no longer hard-errors on bodies with more than one step.
149
+
150
+ - 1a86d3f: Add `onError` compensation to DSL workflows.
151
+
152
+ A DSL workflow had no way to express error handling at all — `try/catch` is not
153
+ an allowed statement, and step options carried only `retries`/`retryDelay`. A
154
+ step can now name a compensation RPC:
155
+
156
+ ```ts
157
+ await workflow.do(
158
+ 'Charge',
159
+ 'chargeCard',
160
+ { id },
161
+ {
162
+ retries: 3,
163
+ onError: 'refundOrder',
164
+ }
165
+ )
166
+ ```
167
+
168
+ Semantics mirror a graph node's `onError` exactly: once the step's retries are
169
+ exhausted the handler is invoked with `{ error: { message } }` and the original
170
+ error is still thrown. This is compensation, not recovery — the workflow fails
171
+ either way. The handler runs as its own durable step, so a replay cannot
172
+ compensate twice, and it does not inherit `onError` itself.
173
+
174
+ The handler is materialised as a real graph node, so it is wired like any other
175
+ RPC and the console draws a dashed red "on error" edge to it rather than the
176
+ route being invisible.
177
+
178
+ - 1a86d3f: Keep a numeric `retryDelay` numeric through the graph round-trip. The serialized
179
+ graph typed it as `string` and the DSL→graph conversion called `.toString()`, so
180
+ `retryDelay: 500` regenerated as `retryDelay: '500'` — a different value to the
181
+ runtime, which parses strings as durations.
182
+ - 1a86d3f: Stop silently dropping switch cases and spread returns from workflow graphs.
183
+ - A fall-through case (`case 'a': case 'b': ...`) recorded only the last value.
184
+ A run entering on `'a'` therefore appeared to match no case at all. Empty
185
+ clauses now carry through to the entry they fall into — the next non-empty
186
+ case, otherwise `default`, otherwise the switch exit.
187
+ - `return { ...r, extra: 1 }` produced a return node listing only `extra`, so
188
+ the graph claimed an output shape the workflow does not have, with no
189
+ diagnostic. `return r` produced no return node at all. `ReturnStepMeta` now
190
+ records a `spread` list, and the regenerated code emits it.
191
+
192
+ - 1a86d3f: Stop deleting code when regenerating a DSL workflow from its graph.
193
+
194
+ Regenerating a workflow (as the console's graph editor does) silently dropped
195
+ steps:
196
+ - Only the **first** step of an `if` arm survived. The walk stopped via a
197
+ heuristic that tested whether the next node id contained `_then_`/`_else_`,
198
+ but node ids are step names, so it never matched. Branch and switch bodies now
199
+ walk the `next` chain up to an explicit exit boundary — the enclosing flow
200
+ node's own `next`.
201
+ - Switch cases emitted only their entry node, never walking `next` at all.
202
+ - Every step was renamed to `Call <rpcName>`, because the graph conversion never
203
+ wrote `stepName` onto the node. Step names are the durable replay cache key,
204
+ so a round-trip silently invalidated in-flight runs.
205
+ - `workflow.suspend()` had no graph node, yet the preceding step's `next` still
206
+ pointed at its id — traversal dead-ended there and every following step was
207
+ deleted. `suspend` is now a real flow node, and both `suspend` and `approval`
208
+ have deserializer cases.
209
+ - Numeric and boolean `switch` case values were emitted quoted (`case '1':`),
210
+ so the case could never match. Step names and reasons containing a quote are
211
+ now escaped.
212
+
213
+ Also fixed in the same pass:
214
+ - `const [org, user] = await Promise.all([...])` regenerated as a bare
215
+ `await Promise.all([...])`, leaving both names unbound.
216
+ - A step result assigned inside a branch was re-declared with `const` inside
217
+ that branch, so any later reference was out of scope. Hoisting analysis was
218
+ keyed off the same dead node-id heuristic and never fired.
219
+ - A top-level step whose _name_ contained `_case`, `_item_`, `_then_`,
220
+ `_else_`, `_child_` or `_default_` was silently deleted, because node ids are
221
+ step names and were matched against those structural substrings. Ownership is
222
+ now read from the parent constructs themselves.
223
+
224
+ - 1a86d3f: Stop corrupting values when regenerating a workflow from its graph.
225
+ - A numeric `workflow.sleep('Wait', 5000)` came back as `'5000'`, and a numeric
226
+ `retryDelay` likewise. Durations are `string | number`; only strings are
227
+ quoted now.
228
+ - An assignment to a context variable was stored as an opaque `value`, so
229
+ `count = count + 1` regenerated as `count = 'count + 1'` — an expression
230
+ turned into a string literal. `SetStepMeta` now carries a separate
231
+ `expression` field (mirroring `SwitchCaseMeta`), so a string literal and a
232
+ code expression are no longer indistinguishable in the meta.
233
+ - A `next` that was not a single node id was coerced with a string cast: an
234
+ array became the bogus id `'a,b'` and a branch-key record became
235
+ `'[object Object]'`, severing every downstream node. Arrays, key-based
236
+ routing tables and condition lists now each render in their own shape.
237
+ - A `filter`/`some`/`every` node with no `outputVar` emitted
238
+ `const undefined = ...`, which does not parse.
239
+
240
+ - 1a86d3f: Keep a `workflow.sleep` whose duration is only known at runtime (a loop
241
+ variable, a field off the input). The closure evaluates it, so it is legal DSL;
242
+ its source text is recorded as an `expression` and emitted raw when regenerating
243
+ code, as a set step already does.
244
+ - 314ace3: Cut the schema generator's ts.Program cost — the dominant contributor to `pikku all` memory and time.
245
+
246
+ Two independent fixes: the program is now scoped to the virtual file's import
247
+ closure (870 source files instead of the whole tsconfig's 2572), and it is
248
+ released once schemas are generated instead of being pinned at module scope for
249
+ the life of the process. On a 279-function tree this cuts cold codegen ~20% and
250
+ in-pass live heap ~21%, with byte-identical schema output.
251
+
252
+ - 3d76f51: Add an optional `docsUrl` to `wireSecret`, `wireVariable`, and `wireCredential`, so a console or deploy UI reporting a missing value can link the user to where they obtain it instead of showing a bare identifier.
253
+ - Updated dependencies [1a86d3f]
254
+ - Updated dependencies [1a86d3f]
255
+ - Updated dependencies [1a86d3f]
256
+ - Updated dependencies [1a86d3f]
257
+ - Updated dependencies [1a86d3f]
258
+ - Updated dependencies [1a86d3f]
259
+ - Updated dependencies [3d76f51]
260
+ - @pikku/core@0.12.65
261
+
1
262
  ## 0.12.43
2
263
 
3
264
  ### Patch Changes
@@ -22,6 +22,7 @@ export const addCredential = (logger, node, checker, state, _options) => {
22
22
  const displayNameValue = getPropertyValue(obj, 'displayName');
23
23
  const descriptionValue = getPropertyValue(obj, 'description');
24
24
  const typeValue = getPropertyValue(obj, 'type');
25
+ const docsUrlValue = getPropertyValue(obj, 'docsUrl');
25
26
  if (!nameValue) {
26
27
  logger.critical(ErrorCode.MISSING_NAME, "Credential is missing the required 'name' property.");
27
28
  return;
@@ -113,6 +114,7 @@ export const addCredential = (logger, node, checker, state, _options) => {
113
114
  displayName: displayNameValue,
114
115
  description: descriptionValue || undefined,
115
116
  type: typeValue,
117
+ docsUrl: docsUrlValue || undefined,
116
118
  schema: schemaLookupName,
117
119
  oauth2,
118
120
  sourceFile,
@@ -26,6 +26,7 @@ export const createAddKeyedWiring = (config) => {
26
26
  const displayNameValue = getPropertyValue(obj, 'displayName');
27
27
  const descriptionValue = getPropertyValue(obj, 'description');
28
28
  const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod');
29
+ const docsUrlValue = getPropertyValue(obj, 'docsUrl');
29
30
  const idValue = getPropertyValue(obj, config.idField);
30
31
  let schemaVariableName = null;
31
32
  let schemaSourceFile = null;
@@ -102,6 +103,7 @@ export const createAddKeyedWiring = (config) => {
102
103
  displayName: displayNameValue,
103
104
  description: descriptionValue || undefined,
104
105
  rotationPeriod: rotationPeriodValue || undefined,
106
+ docsUrl: docsUrlValue || undefined,
105
107
  [config.idField]: idValue,
106
108
  schema: schemaLookupName,
107
109
  sourceFile,
@@ -62,8 +62,8 @@ function hasInlineSteps(steps) {
62
62
  if (step.defaultSteps && hasInlineSteps(step.defaultSteps))
63
63
  return true;
64
64
  }
65
- else if (step.type === 'fanout' && step.child) {
66
- if (hasInlineSteps([step.child]))
65
+ else if (step.type === 'fanout' && step.body) {
66
+ if (hasInlineSteps(step.body))
67
67
  return true;
68
68
  }
69
69
  else if (step.type === 'parallel' && step.children) {
@@ -80,6 +80,11 @@ export function collectInvokedRPCs(steps, rpcs) {
80
80
  for (const step of steps) {
81
81
  if (step.type === 'rpc' && step.rpcName) {
82
82
  rpcs.add(step.rpcName);
83
+ // The compensation handler is invoked at runtime too, so it has to be
84
+ // wired like any other step.
85
+ if (step.options?.onError) {
86
+ rpcs.add(step.options.onError);
87
+ }
83
88
  }
84
89
  else if (step.type === 'branch') {
85
90
  for (const branch of step.branches) {
@@ -96,8 +101,8 @@ export function collectInvokedRPCs(steps, rpcs) {
96
101
  if (step.defaultSteps)
97
102
  collectInvokedRPCs(step.defaultSteps, rpcs);
98
103
  }
99
- else if (step.type === 'fanout' && step.child) {
100
- collectInvokedRPCs([step.child], rpcs);
104
+ else if (step.type === 'fanout' && step.body) {
105
+ collectInvokedRPCs(step.body, rpcs);
101
106
  }
102
107
  else if (step.type === 'parallel' && step.children) {
103
108
  collectInvokedRPCs(step.children, rpcs);
@@ -67,7 +67,9 @@ const inspectorVersion = (() => {
67
67
  try {
68
68
  const pkgUrl = new URL('../../package.json', import.meta.url);
69
69
  const pkg = JSON.parse(readFileSync(pkgUrl, 'utf-8'));
70
- return typeof pkg.version === 'string' ? pkg.version : `v${SCHEMA_CACHE_VERSION}`;
70
+ return typeof pkg.version === 'string'
71
+ ? pkg.version
72
+ : `v${SCHEMA_CACHE_VERSION}`;
71
73
  }
72
74
  catch {
73
75
  return `v${SCHEMA_CACHE_VERSION}`;
@@ -112,7 +114,7 @@ function writeDiskTSSchemas(logger, cacheDir, key, schemas) {
112
114
  logger.debug(`Failed to persist TS-schema cache: ${e.message}`);
113
115
  }
114
116
  }
115
- function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileContent) {
117
+ function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtualFileContent) {
116
118
  const configPath = resolve(tsconfig);
117
119
  // Cache the parsed tsconfig — it doesn't change between runs
118
120
  if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
@@ -123,7 +125,11 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
123
125
  cachedSchemaProgram = undefined;
124
126
  }
125
127
  const resolvedVirtualPath = resolve(virtualFilePath);
126
- const fileNames = [...cachedParsedConfig.fileNames, resolvedVirtualPath];
128
+ // The virtual file imports every type it references, so it is a complete root
129
+ // on its own and TypeScript pulls in exactly the transitive closure it needs.
130
+ // Rooting at cachedParsedConfig.fileNames instead loaded the whole project
131
+ // (2572 files vs 870 on a real tree) without widening what could be resolved.
132
+ const fileNames = [resolvedVirtualPath];
127
133
  const defaultHost = ts.createCompilerHost(cachedParsedConfig.options);
128
134
  const customHost = {
129
135
  ...defaultHost,
@@ -144,9 +150,11 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
144
150
  return defaultHost.readFile(fileName);
145
151
  },
146
152
  };
153
+ const startProgram = performance.now();
147
154
  const program = ts.createProgram(fileNames, cachedParsedConfig.options, customHost, cachedSchemaProgram // reuse previous program for incremental compilation
148
155
  );
149
156
  cachedSchemaProgram = program;
157
+ logger.debug(`Created schema program in ${(performance.now() - startProgram).toFixed(0)}ms (${program.getSourceFiles().length} files)`);
150
158
  return program;
151
159
  }
152
160
  function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, generatedZodSchemas) {
@@ -192,7 +200,7 @@ function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, funct
192
200
  }
193
201
  logger.debug(`generateTSSchemas needed for ${uncoveredSchemas.length} types: ${uncoveredSchemas.slice(0, 3).join(', ')}${uncoveredSchemas.length > 3 ? '...' : ''}`);
194
202
  const virtualFilePath = join(dirname(resolve(tsconfig)), '__pikku_virtual_types__.ts');
195
- const program = createProgramWithVirtualFile(tsconfig, virtualFilePath, customTypesContent);
203
+ const program = createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, customTypesContent);
196
204
  const generator = createGenerator({
197
205
  tsProgram: program,
198
206
  skipTypeCheck: true,
@@ -408,6 +416,13 @@ export async function generateAllSchemas(logger, config, state) {
408
416
  }
409
417
  }
410
418
  const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, zodSchemas);
419
+ // Release the program once the schemas are out of it. It spans the whole
420
+ // tsconfig (~2.5k files, ~600MB on a large tree) and was previously pinned at
421
+ // module scope for the life of the process, so it stacked on top of the main
422
+ // inspector program and pushed `pikku all` past a 2GB heap (#982). The result
423
+ // cache below is what the same-process fast path actually needs; the program
424
+ // only bought incremental reuse on a rebuild, at a cost that dwarfs it.
425
+ cachedSchemaProgram = undefined;
411
426
  cachedCustomTypesContent = customTypesContent;
412
427
  cachedTSSchemas = tsSchemas;
413
428
  if (config.cacheDir && cacheKey) {