@pikku/cli 0.12.91 → 0.12.92

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 (222) hide show
  1. package/LICENSE +106 -0
  2. package/console-app/assets/{index-CpkvmAGb.js → index-DDpIMCpy.js} +149 -149
  3. package/console-app/index.html +1 -1
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +15 -0
  13. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +21 -0
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  21. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  23. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.json +35 -40
  26. package/dist/.pikku/function/pikku-functions.gen.js +1 -3
  27. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +2 -1
  32. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +2 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  37. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
  47. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  48. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  50. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  51. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
  52. package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
  53. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  54. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  55. package/dist/.pikku/schemas/register.gen.js +5 -3
  56. package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
  57. package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
  58. package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
  59. package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
  60. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  61. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
  62. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  63. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  64. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  65. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  66. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  67. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  68. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  69. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  70. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  71. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  72. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  73. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  74. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  75. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +2 -8
  76. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +1 -1
  77. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +1 -1
  78. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  79. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  80. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  81. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  82. package/dist/bin/pikku-bin.mjs +2 -2
  83. package/dist/src/cli.wiring.js +6 -0
  84. package/dist/src/deploy/analyzer/analyzer.js +22 -5
  85. package/dist/src/deploy/build-pipeline.js +5 -1
  86. package/dist/src/fabric/fabric-commands.d.ts +37 -9
  87. package/dist/src/fabric/fabric-commands.js +12 -0
  88. package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
  89. package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
  90. package/dist/src/fabric/functions/secrets-list.function.js +12 -10
  91. package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
  92. package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
  93. package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
  94. package/dist/src/fabric/functions/secrets-set.function.js +18 -7
  95. package/dist/src/fabric/lib/http.d.ts +7 -1
  96. package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
  97. package/dist/src/fabric/lib/sealed-box.js +72 -0
  98. package/dist/src/functions/commands/dev.js +9 -0
  99. package/dist/src/functions/commands/pikku-command-bootstrap.js +1 -0
  100. package/dist/src/functions/commands/scenario-browser.d.ts +17 -2
  101. package/dist/src/functions/commands/scenario-browser.js +16 -8
  102. package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
  103. package/dist/src/functions/commands/scenario-environment.js +61 -0
  104. package/dist/src/functions/commands/scenario.d.ts +6 -0
  105. package/dist/src/functions/commands/scenario.js +14 -9
  106. package/dist/src/functions/commands/skills.js +27 -61
  107. package/dist/src/functions/db/db-codegen.d.ts +6 -0
  108. package/dist/src/functions/db/db-codegen.js +9 -0
  109. package/dist/src/functions/db/db-migrator.js +14 -0
  110. package/dist/src/functions/db/local-db.d.ts +8 -0
  111. package/dist/src/functions/db/local-db.js +24 -2
  112. package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
  113. package/dist/src/functions/db/migration-identifiers.js +262 -0
  114. package/dist/src/functions/db/migration-provenance.d.ts +35 -0
  115. package/dist/src/functions/db/migration-provenance.js +80 -0
  116. package/dist/src/functions/db/schema-sql.d.ts +43 -0
  117. package/dist/src/functions/db/schema-sql.js +135 -0
  118. package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
  119. package/dist/src/functions/wirings/functions/schemas.js +13 -1
  120. package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
  121. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
  122. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
  123. package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +13 -0
  124. package/dist/src/functions/wirings/scenarios/scenario-partition.js +15 -2
  125. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
  126. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
  127. package/dist/src/functions/workflows/all.workflow.js +2 -7
  128. package/dist/src/utils/file-writer.js +14 -2
  129. package/dist/src/utils/pikku-cli-config.d.ts +14 -0
  130. package/dist/src/utils/pikku-cli-config.js +34 -0
  131. package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
  132. package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
  133. package/dist/src/utils/serialize-schemas.d.ts +3 -1
  134. package/dist/src/utils/serialize-schemas.js +39 -4
  135. package/dist/tsconfig.tsbuildinfo +1 -1
  136. package/package.json +9 -9
  137. package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
  138. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
  139. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
  140. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
  141. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
  142. package/skills/pikku-addon/SKILL.md +0 -243
  143. package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
  144. package/skills/pikku-ai-agent/SKILL.md +0 -231
  145. package/skills/pikku-ai-vercel/SKILL.md +0 -82
  146. package/skills/pikku-ai-voice/SKILL.md +0 -88
  147. package/skills/pikku-audit/SKILL.md +0 -175
  148. package/skills/pikku-aws/SKILL.md +0 -111
  149. package/skills/pikku-backblaze/SKILL.md +0 -71
  150. package/skills/pikku-better-auth/SKILL.md +0 -298
  151. package/skills/pikku-cli/SKILL.md +0 -198
  152. package/skills/pikku-cli/references/complete-example.md +0 -82
  153. package/skills/pikku-concepts/SKILL.md +0 -250
  154. package/skills/pikku-concepts/references/concept-mapping.md +0 -556
  155. package/skills/pikku-concepts/references/packages.md +0 -29
  156. package/skills/pikku-config/SKILL.md +0 -212
  157. package/skills/pikku-cron/SKILL.md +0 -214
  158. package/skills/pikku-deploy-azure/SKILL.md +0 -71
  159. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
  160. package/skills/pikku-deploy-express/SKILL.md +0 -86
  161. package/skills/pikku-deploy-fastify/SKILL.md +0 -73
  162. package/skills/pikku-deploy-lambda/SKILL.md +0 -110
  163. package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
  164. package/skills/pikku-deploy-uws/SKILL.md +0 -88
  165. package/skills/pikku-deps/SKILL.md +0 -98
  166. package/skills/pikku-emails/SKILL.md +0 -163
  167. package/skills/pikku-fabric/SKILL.md +0 -318
  168. package/skills/pikku-fabric-debug/SKILL.md +0 -112
  169. package/skills/pikku-feature/SKILL.md +0 -258
  170. package/skills/pikku-gateway-slack/SKILL.md +0 -115
  171. package/skills/pikku-http/SKILL.md +0 -220
  172. package/skills/pikku-http/references/http-options.md +0 -55
  173. package/skills/pikku-i18n/SKILL.md +0 -137
  174. package/skills/pikku-info/SKILL.md +0 -100
  175. package/skills/pikku-jose/SKILL.md +0 -105
  176. package/skills/pikku-kysely/SKILL.md +0 -219
  177. package/skills/pikku-machine-auth/SKILL.md +0 -183
  178. package/skills/pikku-mcp/SKILL.md +0 -241
  179. package/skills/pikku-middleware/SKILL.md +0 -231
  180. package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
  181. package/skills/pikku-mongodb/SKILL.md +0 -105
  182. package/skills/pikku-n8n-import/SKILL.md +0 -109
  183. package/skills/pikku-n8n-import/SPEC.md +0 -84
  184. package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
  185. package/skills/pikku-n8n-import/references/code-translation.md +0 -121
  186. package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
  187. package/skills/pikku-paraglide/SKILL.md +0 -117
  188. package/skills/pikku-permissions/SKILL.md +0 -192
  189. package/skills/pikku-pino/SKILL.md +0 -79
  190. package/skills/pikku-product-second-opinion/README.md +0 -43
  191. package/skills/pikku-product-second-opinion/SKILL.md +0 -158
  192. package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
  193. package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
  194. package/skills/pikku-queue/SKILL.md +0 -240
  195. package/skills/pikku-react/SKILL.md +0 -212
  196. package/skills/pikku-react-query/SKILL.md +0 -242
  197. package/skills/pikku-realtime/SKILL.md +0 -236
  198. package/skills/pikku-realtime/references/other-routes.md +0 -23
  199. package/skills/pikku-redis/SKILL.md +0 -90
  200. package/skills/pikku-rpc/SKILL.md +0 -171
  201. package/skills/pikku-rtl/SKILL.md +0 -219
  202. package/skills/pikku-scenario/SKILL.md +0 -382
  203. package/skills/pikku-schedule/SKILL.md +0 -57
  204. package/skills/pikku-schema-ajv/SKILL.md +0 -62
  205. package/skills/pikku-schema-cfworker/SKILL.md +0 -63
  206. package/skills/pikku-security/SKILL.md +0 -108
  207. package/skills/pikku-services/SKILL.md +0 -248
  208. package/skills/pikku-services/references/audit-wire-service.md +0 -34
  209. package/skills/pikku-software-archaeology/README.md +0 -70
  210. package/skills/pikku-software-archaeology/SKILL.md +0 -186
  211. package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
  212. package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
  213. package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
  214. package/skills/pikku-tag-middleware/SKILL.md +0 -13
  215. package/skills/pikku-template-clone/SKILL.md +0 -40
  216. package/skills/pikku-trigger/SKILL.md +0 -181
  217. package/skills/pikku-versioning/SKILL.md +0 -173
  218. package/skills/pikku-websocket/SKILL.md +0 -243
  219. package/skills/pikku-workflow/SKILL.md +0 -172
  220. package/skills/pikku-workflow/references/workflow-reference.md +0 -63
  221. package/skills/pikku-workflows-client/SKILL.md +0 -150
  222. package/skills/pikku-ws/SKILL.md +0 -47
@@ -0,0 +1,102 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { addFunction } from '@pikku/core/function';
4
+ import { pikkuState } from '@pikku/core/internal';
5
+ import { getStubTracker, isTestRun, } from '@pikku/core/services';
6
+ /**
7
+ * The instrumentation `pikku scenario run` calls on the server under test:
8
+ * snapshot/reset live coverage, and reset/read recorded stub calls.
9
+ *
10
+ * These are pikku's, not the application's. They were previously scaffolded into
11
+ * the project as ordinary `pikkuFunc`s, which is why they ended up registered in
12
+ * the app bootstrap and shipped — `expose: true` — inside every deployed bundle.
13
+ * Registering them here instead means the only process that has them is the one
14
+ * that should: a development server. Nothing is generated, nothing is written to
15
+ * the user's source tree, and a bundle cannot carry what was never in its
16
+ * bootstrap.
17
+ *
18
+ * The bodies are unchanged from the generated ones, minus their schemas: an
19
+ * instrumentation endpoint with a single optional string argument has nothing to
20
+ * validate, and dropping the schemas drops the `zod` dependency the scaffold
21
+ * silently required of every project.
22
+ */
23
+ const takeLiveCoverage = {
24
+ func: async ({ coverageService, metaService }) => {
25
+ if (!coverageService?.takeReport || !metaService?.basePath)
26
+ return null;
27
+ let functionsMeta;
28
+ try {
29
+ const content = await readFile(join(metaService.basePath, 'function', 'pikku-functions-meta-verbose.gen.json'), 'utf-8');
30
+ functionsMeta = JSON.parse(content);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ return await coverageService.takeReport(functionsMeta);
36
+ },
37
+ };
38
+ const resetLiveCoverage = {
39
+ func: async ({ coverageService }) => {
40
+ if (!coverageService)
41
+ return { enabled: false };
42
+ await coverageService.reset();
43
+ return { enabled: true };
44
+ },
45
+ };
46
+ const resetStubs = {
47
+ func: async () => {
48
+ getStubTracker().reset();
49
+ return { enabled: isTestRun() };
50
+ },
51
+ };
52
+ const getStubCalls = {
53
+ func: async (_services, data) => getStubTracker().getCalls(data?.service ?? undefined),
54
+ };
55
+ const instrumentation = {
56
+ pikkuScenarioTakeLiveCoverage: {
57
+ ...takeLiveCoverage,
58
+ title: 'Take Live Coverage',
59
+ description: 'Snapshots the live coverage collected since the server started (or since the last reset) — V8 precise coverage on Node, istanbul instrumentation on Bun — and maps it onto function body spans. Returns null unless the server was started with coverage enabled (pikku dev --coverage).',
60
+ },
61
+ pikkuScenarioResetLiveCoverage: {
62
+ ...resetLiveCoverage,
63
+ title: 'Reset Live Coverage',
64
+ description: 'Clears coverage call counts so the next takeLiveCoverage snapshot is attributable to a single scenario run. Reports enabled: false when the server was not started with coverage enabled.',
65
+ },
66
+ pikkuScenarioResetStubs: {
67
+ ...resetStubs,
68
+ title: 'Reset Stubs',
69
+ description: 'Clears recorded stub calls so the next getStubCalls result is attributable to a single scenario run. Reports enabled: false when the server was not started in test mode.',
70
+ },
71
+ pikkuScenarioGetStubCalls: {
72
+ ...getStubCalls,
73
+ title: 'Get Stub Calls',
74
+ description: 'Returns calls recorded against stubbed/spied services (via the stub()/spy() core utils). Empty unless the server records service calls (pikku dev --test).',
75
+ },
76
+ };
77
+ /**
78
+ * Register the instrumentation into the running dev server's state, so the
79
+ * scenario runner can reach it over `/rpc/<name>` exactly as before.
80
+ *
81
+ * They are registered sessionless: `auth` decides whether a session is
82
+ * *required*, and a sessioned function would demand one regardless of the flag —
83
+ * which is what made `scaffold.scenarios: true` (as opposed to `'auth'`) refuse
84
+ * every call and log a warning about it.
85
+ */
86
+ export const registerScenarioInstrumentation = (requireAuth) => {
87
+ const meta = pikkuState(null, 'function', 'meta');
88
+ for (const [name, { func, title, description }] of Object.entries(instrumentation)) {
89
+ addFunction(name, { func, auth: requireAuth });
90
+ meta[name] = {
91
+ pikkuFuncId: name,
92
+ inputSchemaName: null,
93
+ outputSchemaName: null,
94
+ expose: true,
95
+ sessionless: true,
96
+ tags: ['pikku'],
97
+ title,
98
+ description,
99
+ };
100
+ }
101
+ pikkuState(null, 'function', 'meta', meta);
102
+ };
@@ -12,6 +12,12 @@ export type WiringFileMap = Map<string, {
12
12
  * them in `.pikku/scenarios/`, reachable only from `pikku scenario run`.
13
13
  */
14
14
  export declare const isScenarioStep: (meta: FunctionsMeta[string] | undefined) => boolean;
15
+ /**
16
+ * A scenario's own body is a function too, so the app meta used to carry every
17
+ * scenario's name, schemas and hashes — and a deployed bundle imports that meta
18
+ * wholesale. Both kinds belong on the scenario side of the split.
19
+ */
20
+ export declare const isScenarioFunction: (meta: FunctionsMeta[string] | undefined) => boolean;
15
21
  export declare const partitionScenarioFunctions: (files: WiringFileMap, functionsMeta: FunctionsMeta) => {
16
22
  app: WiringFileMap;
17
23
  scenario: WiringFileMap;
@@ -27,3 +33,10 @@ export declare const partitionScenarioWorkflows: (workflowNames: string[], workf
27
33
  appFiles: WiringFileMap;
28
34
  scenarioFiles: WiringFileMap;
29
35
  };
36
+ /**
37
+ * Drop every scenario and step from a function meta map. The deploy analyzer
38
+ * reads inspector state rather than the partitioned codegen output, so it needs
39
+ * the same split applied before it decides what a deployment contains.
40
+ */
41
+ export declare const withoutScenarios: (functionsMeta: FunctionsMeta) => FunctionsMeta;
42
+ export declare const withoutScenarioWorkflows: (graphMeta: SerializedWorkflowGraphs) => SerializedWorkflowGraphs;
@@ -6,11 +6,17 @@
6
6
  * them in `.pikku/scenarios/`, reachable only from `pikku scenario run`.
7
7
  */
8
8
  export const isScenarioStep = (meta) => meta?.scenarioStep === true;
9
+ /**
10
+ * A scenario's own body is a function too, so the app meta used to carry every
11
+ * scenario's name, schemas and hashes — and a deployed bundle imports that meta
12
+ * wholesale. Both kinds belong on the scenario side of the split.
13
+ */
14
+ export const isScenarioFunction = (meta) => isScenarioStep(meta) || meta?.scenario === true;
9
15
  export const partitionScenarioFunctions = (files, functionsMeta) => {
10
16
  const app = new Map();
11
17
  const scenario = new Map();
12
18
  for (const [name, entry] of files) {
13
- if (isScenarioStep(functionsMeta[name])) {
19
+ if (isScenarioFunction(functionsMeta[name])) {
14
20
  scenario.set(name, entry);
15
21
  }
16
22
  else {
@@ -23,7 +29,7 @@ export const partitionScenarioFunctionsMeta = (functionsMeta) => {
23
29
  const app = {};
24
30
  const scenario = {};
25
31
  for (const [name, meta] of Object.entries(functionsMeta)) {
26
- if (isScenarioStep(meta)) {
32
+ if (isScenarioFunction(meta)) {
27
33
  scenario[name] = meta;
28
34
  }
29
35
  else {
@@ -56,3 +62,10 @@ export const partitionScenarioWorkflows = (workflowNames, workflowFiles, graphMe
56
62
  }
57
63
  return { appNames, scenarioNames, appFiles, scenarioFiles };
58
64
  };
65
+ /**
66
+ * Drop every scenario and step from a function meta map. The deploy analyzer
67
+ * reads inspector state rather than the partitioned codegen output, so it needs
68
+ * the same split applied before it decides what a deployment contains.
69
+ */
70
+ export const withoutScenarios = (functionsMeta) => Object.fromEntries(Object.entries(functionsMeta).filter(([, meta]) => !isScenarioFunction(meta)));
71
+ export const withoutScenarioWorkflows = (graphMeta) => Object.fromEntries(Object.entries(graphMeta ?? {}).filter(([name]) => !isScenarioWorkflow(graphMeta, name)));
@@ -0,0 +1,33 @@
1
+ import type { FunctionsMeta } from '@pikku/core';
2
+ /**
3
+ * The schema names a set of functions validates against.
4
+ *
5
+ * `getUniqueName` is the inspector's own type→schema naming, so this resolves a
6
+ * name the same way `computeRequiredSchemas` did when it filled `requiredSchemas`.
7
+ * The two must not disagree: a scenario schema this failed to name would stay on
8
+ * the app side, which is the leak the split exists to close.
9
+ */
10
+ export declare const schemaNamesFor: (meta: FunctionsMeta, getUniqueName: (name: string) => string) => Set<string>;
11
+ /**
12
+ * Split the required schemas into the app's register and the scenario register.
13
+ *
14
+ * A scenario's input/output schemas are as test-only as its body, and the app's
15
+ * `register.gen.ts` is imported by every deployed bundle — on one project half the
16
+ * registered schemas belonged to scenarios and steps. Both sets are derived from
17
+ * `requiredSchemas`, so every name lands in exactly one register and a name the
18
+ * scenario side claims is removed from the app side by construction.
19
+ *
20
+ * Two things deliberately stay on the app side: a schema an application function
21
+ * also needs (registering a shared schema twice is worse than registering it in
22
+ * one place), and a type the project asked for by hand via `schemasFromTypes`,
23
+ * whatever happens to reference it.
24
+ */
25
+ export declare const partitionRequiredSchemas: ({ functionsMeta, requiredSchemas, getUniqueName, schemasFromTypes, }: {
26
+ functionsMeta: FunctionsMeta;
27
+ requiredSchemas: Set<string>;
28
+ getUniqueName: (name: string) => string;
29
+ schemasFromTypes?: string[];
30
+ }) => {
31
+ appRequired: Set<string>;
32
+ scenarioOnly: Set<string>;
33
+ };
@@ -0,0 +1,49 @@
1
+ import { partitionScenarioFunctionsMeta } from './scenario-partition.js';
2
+ /**
3
+ * The schema names a set of functions validates against.
4
+ *
5
+ * `getUniqueName` is the inspector's own type→schema naming, so this resolves a
6
+ * name the same way `computeRequiredSchemas` did when it filled `requiredSchemas`.
7
+ * The two must not disagree: a scenario schema this failed to name would stay on
8
+ * the app side, which is the leak the split exists to close.
9
+ */
10
+ export const schemaNamesFor = (meta, getUniqueName) => {
11
+ const names = new Set();
12
+ for (const { inputs, outputs } of Object.values(meta)) {
13
+ for (const type of [inputs?.[0], outputs?.[0]]) {
14
+ if (!type)
15
+ continue;
16
+ try {
17
+ names.add(getUniqueName(type));
18
+ }
19
+ catch {
20
+ names.add(type);
21
+ }
22
+ }
23
+ }
24
+ return names;
25
+ };
26
+ /**
27
+ * Split the required schemas into the app's register and the scenario register.
28
+ *
29
+ * A scenario's input/output schemas are as test-only as its body, and the app's
30
+ * `register.gen.ts` is imported by every deployed bundle — on one project half the
31
+ * registered schemas belonged to scenarios and steps. Both sets are derived from
32
+ * `requiredSchemas`, so every name lands in exactly one register and a name the
33
+ * scenario side claims is removed from the app side by construction.
34
+ *
35
+ * Two things deliberately stay on the app side: a schema an application function
36
+ * also needs (registering a shared schema twice is worse than registering it in
37
+ * one place), and a type the project asked for by hand via `schemasFromTypes`,
38
+ * whatever happens to reference it.
39
+ */
40
+ export const partitionRequiredSchemas = ({ functionsMeta, requiredSchemas, getUniqueName, schemasFromTypes, }) => {
41
+ const { app: appFunctionsMeta, scenario: scenarioFunctionsMeta } = partitionScenarioFunctionsMeta(functionsMeta);
42
+ const appSchemaNames = schemaNamesFor(appFunctionsMeta, getUniqueName);
43
+ const requestedByHand = new Set(schemasFromTypes ?? []);
44
+ const scenarioOnly = new Set([...schemaNamesFor(scenarioFunctionsMeta, getUniqueName)].filter((name) => requiredSchemas.has(name) &&
45
+ !appSchemaNames.has(name) &&
46
+ !requestedByHand.has(name)));
47
+ const appRequired = new Set([...requiredSchemas].filter((name) => !scenarioOnly.has(name)));
48
+ return { appRequired, scenarioOnly };
49
+ };
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { pikkuWorkflowComplexFunc } from '#pikku/workflow/pikku-workflow-types.gen.js';
3
3
  import { assertSingleCoreVersion } from '../../utils/assert-single-core-version.js';
4
- import { pruneLegacyScaffoldFiles } from '../../utils/remove-legacy-scaffold-file.js';
4
+ import { pruneLegacyScaffoldFiles, removeRetiredScaffoldFiles, } from '../../utils/remove-legacy-scaffold-file.js';
5
5
  import { PikkuTypecheckFailedError, renderTscFull, renderTscSummary, runProjectTypecheck, } from '../../utils/tsc-check.js';
6
6
  const scaffoldFiles = (config) => {
7
7
  const files = [];
@@ -17,11 +17,6 @@ const scaffoldFiles = (config) => {
17
17
  file: config.consoleFunctionsFile,
18
18
  generator: 'pikkuConsoleFunctions',
19
19
  });
20
- if (config.scaffold?.scenarios && config.scenariosFunctionsFile)
21
- files.push({
22
- file: config.scenariosFunctionsFile,
23
- generator: 'pikkuScenarioFunctions',
24
- });
25
20
  if (config.scaffold?.userAdmin && config.userAdminFunctionsFile)
26
21
  files.push({
27
22
  file: config.userAdminFunctionsFile,
@@ -46,6 +41,7 @@ export const allWorkflow = pikkuWorkflowComplexFunc({
46
41
  // (workflows/RPCs "disappear"), so refuse to codegen against it.
47
42
  await assertSingleCoreVersion(config.rootDir, logger);
48
43
  await pruneLegacyScaffoldFiles(config);
44
+ await removeRetiredScaffoldFiles(config);
49
45
  const allImports = [];
50
46
  let typesDeclarationFileExists = true;
51
47
  if (!existsSync(config.outDir)) {
@@ -150,7 +146,6 @@ export const allWorkflow = pikkuWorkflowComplexFunc({
150
146
  workflow.do('Graph wirings', 'pikkuGraphWirings', null),
151
147
  workflow.do('Public RPC', 'pikkuPublicRPC', null),
152
148
  workflow.do('Console functions', 'pikkuConsoleFunctions', null),
153
- workflow.do('Scenario functions', 'pikkuScenarioFunctions', null),
154
149
  workflow.do('User admin functions', 'pikkuUserAdminFunctions', null),
155
150
  workflow.do('Events scaffold', 'pikkuEventsScaffold', null),
156
151
  workflow.do('Emails', 'pikkuEmails', null),
@@ -1,5 +1,5 @@
1
1
  import { dirname, join } from 'path';
2
- import { mkdir, readFile, writeFile, rm } from 'fs/promises';
2
+ import { mkdir, readFile, rename, writeFile, rm } from 'fs/promises';
3
3
  import { existsSync } from 'fs';
4
4
  import { getCLIVersion } from './get-cli-version.js';
5
5
  export const DO_NOT_MODIFY_COMMENT = `/**
@@ -36,7 +36,19 @@ export const writeFileInDir = async (logger, path, content, { ignoreModifyCommen
36
36
  // Only write if content has changed or file doesn't exist
37
37
  if (existingContent !== content) {
38
38
  await mkdir(dirname(path), { recursive: true });
39
- await writeFile(path, content, 'utf-8');
39
+ // Atomically, because generated files are read while they are generated:
40
+ // `pikku scenario run --spawn` bundles the registers while the dev server it
41
+ // spawned is regenerating them, and `writeFile` truncates before it writes —
42
+ // a reader that lands in that window gets an empty file, not an old one.
43
+ const tmp = `${path}.${process.pid}.tmp`;
44
+ try {
45
+ await writeFile(tmp, content, 'utf-8');
46
+ await rename(tmp, path);
47
+ }
48
+ catch (e) {
49
+ await rm(tmp, { force: true }).catch(() => { });
50
+ throw e;
51
+ }
40
52
  if (TS_FILE_REGEX.test(path)) {
41
53
  tsWriteGeneration++;
42
54
  }
@@ -1,4 +1,18 @@
1
1
  import type { PikkuCLIConfig } from '../../types/config.js';
2
2
  import type { CLILogger } from '../services/cli-logger.service.js';
3
3
  export declare const getPikkuCLIConfig: (logger: CLILogger, configFile: string | undefined, requiredFields: Array<keyof PikkuCLIConfig>, exitProcess?: boolean, outDirOverride?: string) => Promise<PikkuCLIConfig>;
4
+ /** A config that was found and parsed, but says something impossible. */
5
+ export declare class PikkuCLIConfigError extends Error {
6
+ }
7
+ /**
8
+ * The two schema registers must be two directories.
9
+ *
10
+ * `saveSchemas` owns whatever directory it is given: it writes `register.gen.ts`
11
+ * and prunes every schema file that its own required-set does not name. Point the
12
+ * scenario write at `schemaDirectory` and the second call overwrites the app's
13
+ * register with the scenario-only one and deletes the app's schema files — the
14
+ * exact leak the split exists to prevent, silently, and only in a deployed
15
+ * bundle. Nothing downstream can detect it, so it is rejected here.
16
+ */
17
+ export declare const assertSchemaDirectoriesAreDistinct: (config: Pick<PikkuCLIConfig, "schemaDirectory" | "scenarioSchemaDirectory">) => void;
4
18
  export declare const validateCLIConfig: (cliConfig: PikkuCLIConfig, required: Array<keyof PikkuCLIConfig>) => void;
@@ -32,6 +32,28 @@ async function findConfigFile() {
32
32
  }
33
33
  throw new Error('Config file pikku.config.json not found');
34
34
  }
35
+ /** A config that was found and parsed, but says something impossible. */
36
+ export class PikkuCLIConfigError extends Error {
37
+ }
38
+ /**
39
+ * The two schema registers must be two directories.
40
+ *
41
+ * `saveSchemas` owns whatever directory it is given: it writes `register.gen.ts`
42
+ * and prunes every schema file that its own required-set does not name. Point the
43
+ * scenario write at `schemaDirectory` and the second call overwrites the app's
44
+ * register with the scenario-only one and deletes the app's schema files — the
45
+ * exact leak the split exists to prevent, silently, and only in a deployed
46
+ * bundle. Nothing downstream can detect it, so it is rejected here.
47
+ */
48
+ export const assertSchemaDirectoriesAreDistinct = (config) => {
49
+ const { schemaDirectory, scenarioSchemaDirectory } = config;
50
+ if (!schemaDirectory || !scenarioSchemaDirectory) {
51
+ return;
52
+ }
53
+ if (resolve(schemaDirectory) === resolve(scenarioSchemaDirectory)) {
54
+ throw new PikkuCLIConfigError(`scenarioSchemaDirectory must not be the same directory as schemaDirectory (both resolve to ${resolve(schemaDirectory)}). Scenario schemas are written with their own register, which would replace the application one.`);
55
+ }
56
+ };
35
57
  const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields, exitProcess = false, outDirOverride) => {
36
58
  if (!configFile) {
37
59
  configFile = await findConfigFile();
@@ -374,6 +396,12 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
374
396
  if (!result.scenarioMetaDir) {
375
397
  result.scenarioMetaDir = join(scenarioDir, 'meta');
376
398
  }
399
+ // Schemas only a scenario or a step validates against. They live here rather
400
+ // than in `schemaDirectory` because the app's `register.gen.ts` is imported
401
+ // by every deployed bundle, and a test-only schema has no business in one.
402
+ if (!result.scenarioSchemaDirectory) {
403
+ result.scenarioSchemaDirectory = join(scenarioDir, 'schemas');
404
+ }
377
405
  if (!result.scenarioBootstrapFile) {
378
406
  result.scenarioBootstrapFile = join(result.outDir, 'pikku-bootstrap-scenarios.gen.ts');
379
407
  }
@@ -549,6 +577,7 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
549
577
  if (!isAbsolute(result.tsconfig)) {
550
578
  result.tsconfig = join(result.rootDir, result.tsconfig);
551
579
  }
580
+ assertSchemaDirectoriesAreDistinct(result);
552
581
  if (result.addon) {
553
582
  const packageJsonPath = join(result.rootDir, 'package.json');
554
583
  try {
@@ -568,6 +597,11 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
568
597
  return result;
569
598
  }
570
599
  catch (e) {
600
+ // A config that parsed but is contradictory is not a missing config, and
601
+ // saying so sends the reader looking for the wrong problem.
602
+ if (e instanceof PikkuCLIConfigError) {
603
+ throw e;
604
+ }
571
605
  logger.error(e);
572
606
  throw new Error(`Config file not found: ${configFile}`);
573
607
  }
@@ -1,3 +1,17 @@
1
1
  import type { PikkuCLIConfig } from '../../types/config.js';
2
2
  export declare const removeLegacyScaffoldFile: (file: string) => Promise<void>;
3
+ /**
4
+ * Delete scaffolds pikku has stopped generating.
5
+ *
6
+ * The scenario instrumentation scaffold is why this exists: those four functions
7
+ * are pikku's own and are now registered by `pikku dev` itself, because as project
8
+ * source they were indistinguishable from application code and shipped —
9
+ * `expose: true` — in every deployed bundle. The inspector ignores them by name so
10
+ * an unregenerated project is already safe, but the file is then dead code that
11
+ * still imports `zod`, so it goes.
12
+ *
13
+ * Unlike `pruneLegacyScaffoldFiles`, which removes a file from a path pikku used
14
+ * to write to, this removes a file pikku no longer writes at all.
15
+ */
16
+ export declare const removeRetiredScaffoldFiles: (config: PikkuCLIConfig) => Promise<void>;
3
17
  export declare const pruneLegacyScaffoldFiles: (config: PikkuCLIConfig) => Promise<void>;
@@ -34,6 +34,29 @@ export const removeLegacyScaffoldFile = async (file) => {
34
34
  await rm(legacy, { force: true });
35
35
  }
36
36
  };
37
+ /**
38
+ * Delete scaffolds pikku has stopped generating.
39
+ *
40
+ * The scenario instrumentation scaffold is why this exists: those four functions
41
+ * are pikku's own and are now registered by `pikku dev` itself, because as project
42
+ * source they were indistinguishable from application code and shipped —
43
+ * `expose: true` — in every deployed bundle. The inspector ignores them by name so
44
+ * an unregenerated project is already safe, but the file is then dead code that
45
+ * still imports `zod`, so it goes.
46
+ *
47
+ * Unlike `pruneLegacyScaffoldFiles`, which removes a file from a path pikku used
48
+ * to write to, this removes a file pikku no longer writes at all.
49
+ */
50
+ export const removeRetiredScaffoldFiles = async (config) => {
51
+ for (const file of [
52
+ config.scenariosFunctionsFile,
53
+ config.scenariosSchemasFile,
54
+ ]) {
55
+ if (file && existsSync(file)) {
56
+ await rm(file, { force: true });
57
+ }
58
+ }
59
+ };
37
60
  export const pruneLegacyScaffoldFiles = async (config) => {
38
61
  for (const file of scaffoldFiles(config)) {
39
62
  if (file) {
@@ -1,3 +1,5 @@
1
1
  import type { JSONValue } from '@pikku/core';
2
2
  import type { CLILogger } from '../services/cli-logger.service.js';
3
- export declare function saveSchemas(logger: CLILogger, schemaParentDir: string, schemas: Record<string, JSONValue>, requiredSchemas: Set<string>, supportsImportAttributes: boolean, packageName?: string | null): Promise<void>;
3
+ export declare function saveSchemas(logger: CLILogger, schemaParentDir: string, schemas: Record<string, JSONValue>, requiredSchemas: Set<string>, supportsImportAttributes: boolean, packageName?: string | null,
4
+ /** Names the set in log output — there is more than one now (app, scenario). */
5
+ label?: string): Promise<void>;
@@ -1,6 +1,39 @@
1
1
  import { writeFileInDir } from './file-writer.js';
2
- import { mkdir, readdir, unlink, writeFile } from 'fs/promises';
2
+ import { mkdir, readdir, readFile, rename, unlink, writeFile, } from 'fs/promises';
3
3
  const SCHEMA_FILE_SUFFIX = '.schema.json';
4
+ /**
5
+ * Write a schema file without ever exposing a half-written one.
6
+ *
7
+ * `writeFile` truncates and then writes, so a reader that opens the path in
8
+ * between sees an empty file. That reader is real: `pikku scenario run --spawn`
9
+ * bundles the register while the dev server it just spawned is running codegen,
10
+ * and esbuild fails the whole run with "Unexpected end of file in JSON" on
11
+ * whichever schema it happened to catch mid-write. A rename is atomic, so a
12
+ * reader gets either the previous complete file or the new one.
13
+ *
14
+ * Unchanged content is not rewritten at all. Most runs regenerate hundreds of
15
+ * byte-identical schemas, and every one of those is a window that does not need
16
+ * to exist — it also stops file watchers waking on output that did not change.
17
+ */
18
+ async function writeSchemaFile(path, contents) {
19
+ try {
20
+ if ((await readFile(path, 'utf-8')) === contents) {
21
+ return;
22
+ }
23
+ }
24
+ catch {
25
+ // Unreadable or absent — fall through and write it.
26
+ }
27
+ const tmp = `${path}.${process.pid}.tmp`;
28
+ try {
29
+ await writeFile(tmp, contents, 'utf-8');
30
+ await rename(tmp, path);
31
+ }
32
+ catch (e) {
33
+ await unlink(tmp).catch(() => { });
34
+ throw e;
35
+ }
36
+ }
4
37
  function toValidIdentifier(name) {
5
38
  let result = name.replace(/[-./: ]/g, '_');
6
39
  if (/^\d/.test(result)) {
@@ -55,19 +88,21 @@ async function pruneOrphanedSchemaFiles(logger, schemaParentDir, keep) {
55
88
  logger.info(`• Removed ${orphans.length} stale schema file(s).\x1b[0m`);
56
89
  }
57
90
  }
58
- export async function saveSchemas(logger, schemaParentDir, schemas, requiredSchemas, supportsImportAttributes, packageName) {
91
+ export async function saveSchemas(logger, schemaParentDir, schemas, requiredSchemas, supportsImportAttributes, packageName,
92
+ /** Names the set in log output — there is more than one now (app, scenario). */
93
+ label = 'schemas') {
59
94
  if (requiredSchemas.size === 0) {
60
95
  await writeFileInDir(logger, `${schemaParentDir}/register.gen.ts`, 'export const empty = null;');
61
96
  // Every file in schemas/ is now an orphan: register.gen.ts registers nothing, so
62
97
  // leaving them would be exactly the misleading state this prune exists to prevent.
63
98
  await pruneOrphanedSchemaFiles(logger, schemaParentDir, new Set());
64
- logger.info(`• Skipping schemas since none found.\x1b[0m`);
99
+ logger.info(`• Skipping ${label} since none found.\x1b[0m`);
65
100
  return;
66
101
  }
67
102
  await mkdir(`${schemaParentDir}/schemas`, { recursive: true });
68
103
  await Promise.all(Object.entries(schemas).map(async ([schemaName, schema]) => {
69
104
  if (requiredSchemas.has(schemaName)) {
70
- await writeFile(`${schemaParentDir}/schemas/${schemaName}.schema.json`, JSON.stringify(schema), 'utf-8');
105
+ await writeSchemaFile(`${schemaParentDir}/schemas/${schemaName}.schema.json`, JSON.stringify(schema));
71
106
  }
72
107
  }));
73
108
  // Sort so register.gen.ts is byte-identical across runs — requiredSchemas is