@pikku/cli 0.12.90 → 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 (264) hide show
  1. package/LICENSE +106 -0
  2. package/README.md +25 -2
  3. package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
  4. package/console-app/assets/{index-C5Bd44e4.js → index-DDpIMCpy.js} +149 -149
  5. package/console-app/index.html +2 -2
  6. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  7. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  8. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
  10. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  11. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +15 -0
  15. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  16. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +43 -0
  19. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  21. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  22. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  23. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  24. package/dist/.pikku/function/pikku-function-types.gen.d.ts +4 -4
  25. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  26. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  27. package/dist/.pikku/function/pikku-functions-meta.gen.json +39 -44
  28. package/dist/.pikku/function/pikku-functions.gen.js +1 -3
  29. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  30. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  32. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  33. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +9 -0
  34. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +9 -0
  35. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  37. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  39. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  40. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  41. package/dist/.pikku/pikku-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  43. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  45. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  46. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  47. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  48. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
  50. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
  51. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
  52. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
  53. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
  54. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
  55. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
  56. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
  57. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
  58. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
  59. package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
  60. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  61. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  62. package/dist/.pikku/schemas/register.gen.js +5 -3
  63. package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
  64. package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
  65. package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
  66. package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
  67. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  68. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
  69. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  70. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  71. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  72. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  73. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  74. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  75. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  76. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  77. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  78. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  79. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  80. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  81. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  82. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +2 -8
  83. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +19 -0
  84. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
  85. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
  86. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -1
  87. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  88. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  89. package/dist/bin/pikku-bin.mjs +2 -2
  90. package/dist/src/cli.wiring.js +28 -0
  91. package/dist/src/deploy/analyzer/analyzer.js +22 -5
  92. package/dist/src/deploy/build-pipeline.js +5 -1
  93. package/dist/src/fabric/fabric-commands.d.ts +37 -9
  94. package/dist/src/fabric/fabric-commands.js +12 -0
  95. package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
  96. package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
  97. package/dist/src/fabric/functions/secrets-list.function.js +12 -10
  98. package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
  99. package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
  100. package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
  101. package/dist/src/fabric/functions/secrets-set.function.js +18 -7
  102. package/dist/src/fabric/lib/http.d.ts +7 -1
  103. package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
  104. package/dist/src/fabric/lib/sealed-box.js +72 -0
  105. package/dist/src/functions/commands/dev.js +11 -0
  106. package/dist/src/functions/commands/load-user-project.d.ts +7 -0
  107. package/dist/src/functions/commands/load-user-project.js +21 -0
  108. package/dist/src/functions/commands/pikku-command-bootstrap.js +13 -0
  109. package/dist/src/functions/commands/scenario-browser.d.ts +84 -0
  110. package/dist/src/functions/commands/scenario-browser.js +68 -0
  111. package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
  112. package/dist/src/functions/commands/scenario-environment.js +61 -0
  113. package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
  114. package/dist/src/functions/commands/scenario-formatter.js +120 -0
  115. package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
  116. package/dist/src/functions/commands/scenario-ladder.js +132 -0
  117. package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
  118. package/dist/src/functions/commands/scenario-plan.js +106 -0
  119. package/dist/src/functions/commands/scenario.d.ts +24 -0
  120. package/dist/src/functions/commands/scenario.js +322 -50
  121. package/dist/src/functions/commands/serve.js +2 -0
  122. package/dist/src/functions/commands/skills.js +27 -61
  123. package/dist/src/functions/db/db-codegen.d.ts +6 -0
  124. package/dist/src/functions/db/db-codegen.js +9 -0
  125. package/dist/src/functions/db/db-migrator.js +14 -0
  126. package/dist/src/functions/db/local-db.d.ts +8 -0
  127. package/dist/src/functions/db/local-db.js +24 -2
  128. package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
  129. package/dist/src/functions/db/migration-identifiers.js +262 -0
  130. package/dist/src/functions/db/migration-provenance.d.ts +35 -0
  131. package/dist/src/functions/db/migration-provenance.js +80 -0
  132. package/dist/src/functions/db/schema-sql.d.ts +43 -0
  133. package/dist/src/functions/db/schema-sql.js +135 -0
  134. package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
  135. package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
  136. package/dist/src/functions/wirings/functions/schemas.js +13 -1
  137. package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
  138. package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
  139. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
  140. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
  141. package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +42 -0
  142. package/dist/src/functions/wirings/scenarios/scenario-partition.js +71 -0
  143. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
  144. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
  145. package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
  146. package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
  147. package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
  148. package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
  149. package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
  150. package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
  151. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
  152. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
  153. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
  154. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
  155. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
  156. package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
  157. package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
  158. package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
  159. package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
  160. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
  161. package/dist/src/functions/workflows/all.workflow.js +2 -7
  162. package/dist/src/server/server-ready.d.ts +12 -0
  163. package/dist/src/server/server-ready.js +12 -0
  164. package/dist/src/server/spawn-dev-server.d.ts +50 -0
  165. package/dist/src/server/spawn-dev-server.js +112 -0
  166. package/dist/src/services.js +12 -1
  167. package/dist/src/utils/file-writer.js +14 -2
  168. package/dist/src/utils/meta-diff.js +5 -2
  169. package/dist/src/utils/pikku-cli-config.d.ts +14 -0
  170. package/dist/src/utils/pikku-cli-config.js +60 -0
  171. package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
  172. package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
  173. package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
  174. package/dist/src/utils/resolve-scenario-actors.js +74 -0
  175. package/dist/src/utils/serialize-schemas.d.ts +3 -1
  176. package/dist/src/utils/serialize-schemas.js +39 -4
  177. package/dist/tsconfig.tsbuildinfo +1 -1
  178. package/package.json +18 -8
  179. package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
  180. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
  181. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
  182. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
  183. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
  184. package/skills/pikku-addon/SKILL.md +0 -243
  185. package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
  186. package/skills/pikku-ai-agent/SKILL.md +0 -231
  187. package/skills/pikku-ai-vercel/SKILL.md +0 -82
  188. package/skills/pikku-ai-voice/SKILL.md +0 -88
  189. package/skills/pikku-audit/SKILL.md +0 -175
  190. package/skills/pikku-aws/SKILL.md +0 -111
  191. package/skills/pikku-backblaze/SKILL.md +0 -71
  192. package/skills/pikku-better-auth/SKILL.md +0 -298
  193. package/skills/pikku-cli/SKILL.md +0 -198
  194. package/skills/pikku-cli/references/complete-example.md +0 -82
  195. package/skills/pikku-concepts/SKILL.md +0 -250
  196. package/skills/pikku-concepts/references/concept-mapping.md +0 -556
  197. package/skills/pikku-concepts/references/packages.md +0 -29
  198. package/skills/pikku-config/SKILL.md +0 -212
  199. package/skills/pikku-cron/SKILL.md +0 -214
  200. package/skills/pikku-deploy-azure/SKILL.md +0 -71
  201. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
  202. package/skills/pikku-deploy-express/SKILL.md +0 -86
  203. package/skills/pikku-deploy-fastify/SKILL.md +0 -73
  204. package/skills/pikku-deploy-lambda/SKILL.md +0 -110
  205. package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
  206. package/skills/pikku-deploy-uws/SKILL.md +0 -88
  207. package/skills/pikku-deps/SKILL.md +0 -98
  208. package/skills/pikku-emails/SKILL.md +0 -163
  209. package/skills/pikku-fabric/SKILL.md +0 -318
  210. package/skills/pikku-fabric-debug/SKILL.md +0 -112
  211. package/skills/pikku-feature/SKILL.md +0 -258
  212. package/skills/pikku-gateway-slack/SKILL.md +0 -115
  213. package/skills/pikku-http/SKILL.md +0 -220
  214. package/skills/pikku-http/references/http-options.md +0 -55
  215. package/skills/pikku-i18n/SKILL.md +0 -137
  216. package/skills/pikku-info/SKILL.md +0 -100
  217. package/skills/pikku-jose/SKILL.md +0 -105
  218. package/skills/pikku-kysely/SKILL.md +0 -219
  219. package/skills/pikku-machine-auth/SKILL.md +0 -183
  220. package/skills/pikku-mcp/SKILL.md +0 -241
  221. package/skills/pikku-middleware/SKILL.md +0 -231
  222. package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
  223. package/skills/pikku-mongodb/SKILL.md +0 -105
  224. package/skills/pikku-n8n-import/SKILL.md +0 -109
  225. package/skills/pikku-n8n-import/SPEC.md +0 -84
  226. package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
  227. package/skills/pikku-n8n-import/references/code-translation.md +0 -121
  228. package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
  229. package/skills/pikku-paraglide/SKILL.md +0 -117
  230. package/skills/pikku-permissions/SKILL.md +0 -192
  231. package/skills/pikku-pino/SKILL.md +0 -79
  232. package/skills/pikku-product-second-opinion/README.md +0 -43
  233. package/skills/pikku-product-second-opinion/SKILL.md +0 -158
  234. package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
  235. package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
  236. package/skills/pikku-queue/SKILL.md +0 -240
  237. package/skills/pikku-react/SKILL.md +0 -212
  238. package/skills/pikku-react-query/SKILL.md +0 -242
  239. package/skills/pikku-realtime/SKILL.md +0 -236
  240. package/skills/pikku-realtime/references/other-routes.md +0 -23
  241. package/skills/pikku-redis/SKILL.md +0 -90
  242. package/skills/pikku-rpc/SKILL.md +0 -171
  243. package/skills/pikku-rtl/SKILL.md +0 -219
  244. package/skills/pikku-scenario/SKILL.md +0 -215
  245. package/skills/pikku-schedule/SKILL.md +0 -57
  246. package/skills/pikku-schema-ajv/SKILL.md +0 -62
  247. package/skills/pikku-schema-cfworker/SKILL.md +0 -63
  248. package/skills/pikku-security/SKILL.md +0 -108
  249. package/skills/pikku-services/SKILL.md +0 -248
  250. package/skills/pikku-services/references/audit-wire-service.md +0 -34
  251. package/skills/pikku-software-archaeology/README.md +0 -70
  252. package/skills/pikku-software-archaeology/SKILL.md +0 -186
  253. package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
  254. package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
  255. package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
  256. package/skills/pikku-tag-middleware/SKILL.md +0 -13
  257. package/skills/pikku-template-clone/SKILL.md +0 -40
  258. package/skills/pikku-trigger/SKILL.md +0 -181
  259. package/skills/pikku-versioning/SKILL.md +0 -173
  260. package/skills/pikku-websocket/SKILL.md +0 -243
  261. package/skills/pikku-workflow/SKILL.md +0 -172
  262. package/skills/pikku-workflow/references/workflow-reference.md +0 -63
  263. package/skills/pikku-workflows-client/SKILL.md +0 -150
  264. package/skills/pikku-ws/SKILL.md +0 -47
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ export declare const FabricSecretsRotateInput: z.ZodObject<{
3
+ branch: z.ZodString;
4
+ force: z.ZodOptional<z.ZodBoolean>;
5
+ }, z.core.$strip>;
6
+ export declare const FabricSecretsRotateOutput: z.ZodObject<{
7
+ retiredKeyId: z.ZodString;
8
+ }, z.core.$strip>;
9
+ export declare const FabricSecretsRotate: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
10
+ branch: string;
11
+ force?: boolean | undefined;
12
+ }, {
13
+ retiredKeyId: string;
14
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
15
+ branch: string;
16
+ force?: boolean | undefined;
17
+ }, {
18
+ retiredKeyId: string;
19
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
20
+ branch: string;
21
+ force?: boolean | undefined;
22
+ }, {
23
+ retiredKeyId: string;
24
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices>, undefined, undefined>;
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
3
+ import { resolveApiContext } from '../lib/config.js';
4
+ import { getFabricRPC } from '../lib/http.js';
5
+ import { resolveStageId } from '../lib/stage.js';
6
+ export const FabricSecretsRotateInput = z.object({
7
+ branch: z.string(),
8
+ force: z.boolean().optional(),
9
+ });
10
+ export const FabricSecretsRotateOutput = z.object({
11
+ retiredKeyId: z.string(),
12
+ });
13
+ export const FabricSecretsRotate = pikkuSessionlessFunc({
14
+ description: "Retire a stage's sealing key so the next deploy issues a new one. Secrets sealed to the old key must be set again.",
15
+ input: FabricSecretsRotateInput,
16
+ output: FabricSecretsRotateOutput,
17
+ func: async (_services, { branch, force }) => {
18
+ const ctx = await resolveApiContext();
19
+ if (!ctx.token)
20
+ throw new Error('Not logged in. Run `pikku fabric login` first.');
21
+ if (!ctx.projectId)
22
+ throw new Error('No fabric project linked. Run `pikku fabric link` first.');
23
+ if (!force) {
24
+ // Not a confirmation for politeness — this is the one operation here
25
+ // that destroys access to data. Fabric cannot read the sealed values, so
26
+ // it cannot carry them over, and nobody can undo it afterwards.
27
+ throw new Error(`Rotating the sealing key on ${branch} makes every secret already set on it unreadable — fabric cannot re-seal values it cannot open, so each one must be set again after the next deploy. Re-run with --force if that is what you want.`);
28
+ }
29
+ const rpc = getFabricRPC({ apiUrl: ctx.apiUrl, token: ctx.token });
30
+ const stageId = await resolveStageId(rpc, ctx.projectId, branch);
31
+ const result = await rpc.invoke('rotateStageSealingKey', { stageId });
32
+ console.log(`[fabric] retired sealing key ${result.retiredKeyId} on ${branch}`);
33
+ console.log('[fabric] deploy the stage to issue a new key, then set its secrets again');
34
+ return result;
35
+ },
36
+ });
@@ -6,7 +6,8 @@ export declare const FabricSecretsSetInput: z.ZodObject<{
6
6
  force: z.ZodOptional<z.ZodBoolean>;
7
7
  }, z.core.$strip>;
8
8
  export declare const FabricSecretsSetOutput: z.ZodObject<{
9
- ok: z.ZodBoolean;
9
+ name: z.ZodString;
10
+ keyId: z.ZodString;
10
11
  }, z.core.$strip>;
11
12
  export declare const FabricSecretsSet: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
12
13
  name: string;
@@ -14,19 +15,22 @@ export declare const FabricSecretsSet: import("../../../.pikku/pikku-types.gen.j
14
15
  value?: string | undefined;
15
16
  force?: boolean | undefined;
16
17
  }, {
17
- ok: boolean;
18
+ name: string;
19
+ keyId: string;
18
20
  }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
19
21
  name: string;
20
22
  branch: string;
21
23
  value?: string | undefined;
22
24
  force?: boolean | undefined;
23
25
  }, {
24
- ok: boolean;
26
+ name: string;
27
+ keyId: string;
25
28
  }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
26
29
  name: string;
27
30
  branch: string;
28
31
  value?: string | undefined;
29
32
  force?: boolean | undefined;
30
33
  }, {
31
- ok: boolean;
34
+ name: string;
35
+ keyId: string;
32
36
  }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices>, undefined, undefined>;
@@ -3,15 +3,20 @@ import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
3
3
  import { resolveApiContext } from '../lib/config.js';
4
4
  import { getFabricRPC } from '../lib/http.js';
5
5
  import { promptSecret } from '../lib/prompt.js';
6
+ import { resolveStageId } from '../lib/stage.js';
7
+ import { seal, serializeSealedValue } from '../lib/sealed-box.js';
6
8
  export const FabricSecretsSetInput = z.object({
7
9
  name: z.string(),
8
10
  branch: z.string(),
9
11
  value: z.string().optional(),
10
12
  force: z.boolean().optional(),
11
13
  });
12
- export const FabricSecretsSetOutput = z.object({ ok: z.boolean() });
14
+ export const FabricSecretsSetOutput = z.object({
15
+ name: z.string(),
16
+ keyId: z.string(),
17
+ });
13
18
  export const FabricSecretsSet = pikkuSessionlessFunc({
14
- description: 'Set a stage-scoped secret. Encrypted with the stage KEK and stored in the project D1.',
19
+ description: 'Set a stage-scoped secret. Sealed here to the stage public key the plaintext never leaves this machine.',
15
20
  input: FabricSecretsSetInput,
16
21
  output: FabricSecretsSetOutput,
17
22
  func: async (_services, { name, branch, value }) => {
@@ -24,13 +29,19 @@ export const FabricSecretsSet = pikkuSessionlessFunc({
24
29
  if (!plaintext)
25
30
  throw new Error('Empty secret value — aborting.');
26
31
  const rpc = getFabricRPC({ apiUrl: ctx.apiUrl, token: ctx.token });
27
- const result = await rpc.invoke('setStageSecret', {
28
- projectId: ctx.projectId,
29
- branch,
32
+ const stageId = await resolveStageId(rpc, ctx.projectId, branch);
33
+ // Fabric holds only the public half of this keypair. The private half went
34
+ // to the stage's worker at deploy time and was dropped there, so the value
35
+ // sealed below can be opened by that worker and by nothing else — including
36
+ // by the fabric process that stores it.
37
+ const key = await rpc.invoke('getStageSealingKey', { stageId });
38
+ const sealed = seal(key.publicKey, key.keyId, plaintext);
39
+ const result = await rpc.invoke('setStageSealedSecret', {
40
+ stageId,
30
41
  name,
31
- value: plaintext,
42
+ sealedValue: serializeSealedValue(sealed),
32
43
  });
33
- console.log(`[fabric] ${name} set on ${branch}.`);
44
+ console.log(`[fabric] ${name} sealed to ${result.keyId} and set on ${branch}.`);
34
45
  return result;
35
46
  },
36
47
  });
@@ -1,4 +1,10 @@
1
+ /**
2
+ * Type-safe RPC client backed by the snapshot SDK in `../sdk/`. The map is
3
+ * a copy of fabric's generated rpc-map at the time of the addon's last
4
+ * release — versioning bumps are taken by republishing this package.
5
+ */
6
+ import { PikkuRPC } from '../sdk/pikku-rpc.gen.js';
1
7
  export declare function getFabricRPC(opts: {
2
8
  apiUrl: string;
3
9
  token: string | null;
4
- }): any;
10
+ }): PikkuRPC;
@@ -0,0 +1,16 @@
1
+ /** Algorithm tag carried by every sealed value. Must match fabric's. */
2
+ export declare const SEALED_BOX_ALGORITHM = "sealed-x25519-v1";
3
+ export interface SealedValue {
4
+ alg: typeof SEALED_BOX_ALGORITHM;
5
+ /** The `encryption_key` row whose public key sealed this. */
6
+ kid: string;
7
+ /** Ephemeral public key, SPKI DER as base64. */
8
+ epk: string;
9
+ iv: string;
10
+ ct: string;
11
+ tag: string;
12
+ }
13
+ /** Seal a value to a stage's public key. */
14
+ export declare function seal(publicKey: string, keyId: string, plaintext: string): SealedValue;
15
+ /** Wire form. Fabric stores exactly this string. */
16
+ export declare function serializeSealedValue(sealed: SealedValue): string;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Sealing a value to a stage's public key, here on the client.
3
+ *
4
+ * `pikku fabric secrets set` seals before it sends, so the plaintext never
5
+ * reaches fabric — not the ingress in front of it, not a request log, not the
6
+ * process handling the write. Fabric stores the sealed blob and hands it to the
7
+ * worker, which holds the only private key that opens it.
8
+ *
9
+ * This is the seal half of fabric's `sealed-x25519-v1`, and the two must stay
10
+ * byte-compatible: an ephemeral X25519 keypair, ECDH against the recipient,
11
+ * HKDF-SHA256 to a content key, AES-256-GCM to seal. Everything below is
12
+ * `node:crypto` on purpose — a sealing primitive that pulled in a dependency
13
+ * would be a supply-chain question sitting directly on the plaintext.
14
+ *
15
+ * There is deliberately no `unseal` here. The CLI has no private key and no
16
+ * business having one; opening a value happens in the worker.
17
+ */
18
+ import { createCipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, } from 'node:crypto';
19
+ /** Algorithm tag carried by every sealed value. Must match fabric's. */
20
+ export const SEALED_BOX_ALGORITHM = 'sealed-x25519-v1';
21
+ /** HKDF domain separator. Must match fabric's byte for byte. */
22
+ const SEAL_SALT = 'pikkufabric/sealed-box/v1';
23
+ /** Seal a value to a stage's public key. */
24
+ export function seal(publicKey, keyId, plaintext) {
25
+ const recipient = importPublicKey(publicKey);
26
+ const ephemeral = generateKeyPairSync('x25519');
27
+ const epk = ephemeral.publicKey.export({ type: 'spki', format: 'der' });
28
+ const contentKey = deriveContentKey(diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipient }), epk, recipient.export({ type: 'spki', format: 'der' }));
29
+ const iv = randomBytes(12);
30
+ const cipher = createCipheriv('aes-256-gcm', contentKey, iv);
31
+ const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
32
+ return {
33
+ alg: SEALED_BOX_ALGORITHM,
34
+ kid: keyId,
35
+ epk: epk.toString('base64'),
36
+ iv: iv.toString('base64'),
37
+ ct: ct.toString('base64'),
38
+ tag: cipher.getAuthTag().toString('base64'),
39
+ };
40
+ }
41
+ /** Wire form. Fabric stores exactly this string. */
42
+ export function serializeSealedValue(sealed) {
43
+ return JSON.stringify(sealed);
44
+ }
45
+ /**
46
+ * Both public keys go into the HKDF `info`, which is what stops a sealed value
47
+ * being redirected: change either the ephemeral key or the recipient and the
48
+ * derived content key changes with it.
49
+ */
50
+ function deriveContentKey(shared, epk, recipientPublic) {
51
+ return Buffer.from(hkdfSync('sha256', shared, Buffer.from(SEAL_SALT), Buffer.concat([epk, recipientPublic]), 32));
52
+ }
53
+ function importPublicKey(publicKey) {
54
+ let key;
55
+ try {
56
+ key = createPublicKey({
57
+ key: Buffer.from(publicKey, 'base64'),
58
+ type: 'spki',
59
+ format: 'der',
60
+ });
61
+ }
62
+ catch (error) {
63
+ throw new Error(`Not a valid X25519 public key (${error instanceof Error ? error.message : String(error)})`);
64
+ }
65
+ // A public key of the wrong curve would still ECDH, just against a key the
66
+ // stage's worker cannot match — the failure would surface as an unopenable
67
+ // secret much later, so refuse it at the point the type is still knowable.
68
+ if (key.asymmetricKeyType !== 'x25519') {
69
+ throw new Error(`Stage sealing key is ${key.asymmetricKeyType}, expected x25519 — this CLI is too old for it, run \`npm i -g @pikku/cli\``);
70
+ }
71
+ return key;
72
+ }
@@ -11,9 +11,11 @@ import { LocalContent, } from '@pikku/core/services/local-content';
11
11
  import { InMemorySchedulerService } from '@pikku/schedule';
12
12
  import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
13
13
  import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
14
+ import { registerScenarioInstrumentation } from '../wirings/scenarios/register-scenario-instrumentation.js';
14
15
  import { startCoverageService } from './start-coverage.js';
15
16
  import { createDevAIAgentRunner } from './dev-ai-runner.js';
16
17
  import { resolveConsoleMount } from './serve-console.js';
18
+ import { serverReadyLine } from '../../server/server-ready.js';
17
19
  export const dev = pikkuSessionlessFunc({
18
20
  remote: true,
19
21
  func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test }, { rpc }) => {
@@ -114,6 +116,14 @@ export const dev = pikkuSessionlessFunc({
114
116
  return;
115
117
  }
116
118
  await loadUserBootstrap(pikkuDir);
119
+ // Scenario instrumentation exists only on a development server: the runner
120
+ // calls it over RPC to reset and snapshot coverage and stub calls. It is
121
+ // registered here, after the app bootstrap, rather than generated into the
122
+ // project — an app bootstrap is what a deployed bundle imports, and coverage
123
+ // endpoints have no business in one.
124
+ if (config.scaffold?.scenarios) {
125
+ registerScenarioInstrumentation(config.scaffold.scenarios === 'auth');
126
+ }
117
127
  const configModule = await loadUserModule(pikkuConfigFactory.file);
118
128
  const servicesModule = await loadUserModule(singletonServicesFactory.file);
119
129
  const userCreateConfig = configModule[pikkuConfigFactory.variable];
@@ -238,6 +248,7 @@ export const dev = pikkuSessionlessFunc({
238
248
  if (consoleMount) {
239
249
  logger.info(`Pikku Console available at http://${hostname}:${resolvedPort}${consoleMount.urlPrefix}`);
240
250
  }
251
+ logger.info(serverReadyLine(hostname, resolvedPort));
241
252
  let configWatcher;
242
253
  let watcher;
243
254
  process.once('SIGINT', async () => {
@@ -4,6 +4,13 @@
4
4
  * `pikkuState`.
5
5
  */
6
6
  export declare function loadUserBootstrap(pikkuDir: string): Promise<void>;
7
+ /**
8
+ * Load the generated `pikku-bootstrap-scenarios.gen.{ts,js}`, which imports the
9
+ * app bootstrap and then registers the scenarios, features and steps that are
10
+ * deliberately kept out of it. Falls back to the app bootstrap for a project
11
+ * generated before the split existed.
12
+ */
13
+ export declare function loadScenarioBootstrap(pikkuDir: string): Promise<void>;
7
14
  /**
8
15
  * Import a user-source TypeScript file (e.g. their config or services
9
16
  * factory) so it can be loaded from inside @pikku/cli's compiled JS.
@@ -39,6 +39,27 @@ export async function loadUserBootstrap(pikkuDir) {
39
39
  ensureTsxRegistered();
40
40
  await importUserPath(bootstrapPath);
41
41
  }
42
+ /**
43
+ * Load the generated `pikku-bootstrap-scenarios.gen.{ts,js}`, which imports the
44
+ * app bootstrap and then registers the scenarios, features and steps that are
45
+ * deliberately kept out of it. Falls back to the app bootstrap for a project
46
+ * generated before the split existed.
47
+ */
48
+ export async function loadScenarioBootstrap(pikkuDir) {
49
+ const scenarioTs = join(pikkuDir, 'pikku-bootstrap-scenarios.gen.ts');
50
+ const scenarioJs = join(pikkuDir, 'pikku-bootstrap-scenarios.gen.js');
51
+ const scenarioPath = existsSync(scenarioTs)
52
+ ? scenarioTs
53
+ : existsSync(scenarioJs)
54
+ ? scenarioJs
55
+ : null;
56
+ if (!scenarioPath) {
57
+ await loadUserBootstrap(pikkuDir);
58
+ return;
59
+ }
60
+ ensureTsxRegistered();
61
+ await importUserPath(scenarioPath);
62
+ }
42
63
  /**
43
64
  * Import a user-source TypeScript file (e.g. their config or services
44
65
  * factory) so it can be loaded from inside @pikku/cli's compiled JS.
@@ -64,5 +64,18 @@ export const pikkuBootstrap = pikkuSessionlessFunc({
64
64
  })
65
65
  .join('\n');
66
66
  await writeFileInDir(logger, config.bootstrapFile, allBootstrapImports);
67
+ // The scenario bootstrap is the only entry point that registers scenarios,
68
+ // features and their steps. It pulls in the app bootstrap first so a runner
69
+ // needs to load one file, and so the meta files it merges onto are already
70
+ // in place however this module is reached.
71
+ const scenarioBootstrapImports = [
72
+ config.bootstrapFile,
73
+ join(config.scenarioSchemaDirectory, 'register.gen.ts'),
74
+ config.scenarioStepsMetaFile,
75
+ config.scenarioStepsFile,
76
+ config.scenarioWiringsMetaFile,
77
+ config.scenarioWiringsFile,
78
+ ].map((to) => `import '${getFileImportRelativePath(config.scenarioBootstrapFile, to, config.packageMappings, config.forceRelativeImports)}'`);
79
+ await writeFileInDir(logger, config.scenarioBootstrapFile, scenarioBootstrapImports.join('\n'));
67
80
  },
68
81
  });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The scenario runner's browser lifecycle.
3
+ *
4
+ * Two jobs, both kept out of the run loop so they are testable without
5
+ * launching a browser: deciding whether a run needs a driver at all (and
6
+ * failing early, with instructions, when it does and cannot have one), and
7
+ * making the driver's optional isolation and diagnostics safe to call
8
+ * unconditionally.
9
+ */
10
+ import type { ScenarioBrowserFailure, ScenarioBrowserProvider } from '@pikku/core/workflow';
11
+ import type { ScenarioActorConfig } from '@pikku/core/services';
12
+ /** The default driver, used when a project names no other. */
13
+ export declare const DEFAULT_BROWSER_DRIVER = "@pikku/playwright";
14
+ export interface ScenarioBrowserDriverOptions {
15
+ secret: string;
16
+ actors: Record<string, ScenarioActorConfig>;
17
+ signInPath?: string;
18
+ failureDir?: string;
19
+ config: unknown;
20
+ }
21
+ /**
22
+ * What a browser driver package exports.
23
+ *
24
+ * Nothing here is playwright-specific: a driver is anything that can hand a
25
+ * scenario step a `ScenarioBrowserProvider`. `@pikku/playwright` is the one
26
+ * pikku ships, named by `scenarios.browserDriver` only as a default.
27
+ */
28
+ export interface ScenarioBrowserDriver {
29
+ createScenarioBrowserProvider?: (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
30
+ /** The class form the bundled driver exports. */
31
+ PlaywrightScenarioBrowserProvider?: new (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
32
+ /**
33
+ * Driver-specific config (headed, slowMo, timeouts) resolved from the env.
34
+ *
35
+ * A driver may resolve `appUrl` itself when the caller passes none — that is
36
+ * how a target known only at run time (a sandbox hostname in the env) is
37
+ * reached without a config file per deployment. A driver that answers with a
38
+ * placeholder rather than a real target must say so with
39
+ * `appUrlSource: 'default'`, which the runner reports as a missing appUrl.
40
+ */
41
+ browserConfigFromEnv?: (overrides: {
42
+ appUrl?: string;
43
+ apiUrl: string;
44
+ }) => unknown;
45
+ }
46
+ export interface ResolveScenarioBrowserProviderOptions {
47
+ /** The environment name, for error messages that tell the user what to edit. */
48
+ environment: string;
49
+ apiUrl: string;
50
+ appUrl?: string;
51
+ secret: string;
52
+ actors: Record<string, ScenarioActorConfig>;
53
+ signInPath?: string;
54
+ /** Where failure screenshots are written. */
55
+ failureDir: string;
56
+ /** The scenarios that declared browser steps, named in the missing-driver error. */
57
+ browserScenarios: string[];
58
+ /** Package to drive the browser. Defaults to `@pikku/playwright`. */
59
+ driver?: string;
60
+ importDriver?: (specifier: string) => Promise<ScenarioBrowserDriver>;
61
+ }
62
+ /**
63
+ * Both failure modes here are discovered before the first scenario starts. A
64
+ * missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
65
+ * learn about.
66
+ *
67
+ * `appUrl` is checked after the driver has resolved its config rather than
68
+ * before, so a driver that knows the target from its own environment (a
69
+ * sandbox hostname) is allowed to supply it. The check is not dropped: a driver
70
+ * reporting `appUrlSource: 'default'` resolved nothing and fails the same way,
71
+ * because a run against a placeholder URL is worse than one that refuses to
72
+ * start.
73
+ */
74
+ export declare const resolveScenarioBrowserProvider: ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver, importDriver, }: ResolveScenarioBrowserProviderOptions) => Promise<ScenarioBrowserProvider>;
75
+ /**
76
+ * The three calls the run loop makes, safe against a run with no browser and
77
+ * against a driver that implements neither optional member.
78
+ */
79
+ export interface ScenarioBrowserLifecycle {
80
+ reset(): Promise<void>;
81
+ captureFailure(label: string): Promise<ScenarioBrowserFailure[]>;
82
+ close(): Promise<void>;
83
+ }
84
+ export declare const scenarioBrowserLifecycle: (provider: ScenarioBrowserProvider | undefined) => ScenarioBrowserLifecycle;
@@ -0,0 +1,68 @@
1
+ /** The default driver, used when a project names no other. */
2
+ export const DEFAULT_BROWSER_DRIVER = '@pikku/playwright';
3
+ /**
4
+ * Both failure modes here are discovered before the first scenario starts. A
5
+ * missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
6
+ * learn about.
7
+ *
8
+ * `appUrl` is checked after the driver has resolved its config rather than
9
+ * before, so a driver that knows the target from its own environment (a
10
+ * sandbox hostname) is allowed to supply it. The check is not dropped: a driver
11
+ * reporting `appUrlSource: 'default'` resolved nothing and fails the same way,
12
+ * because a run against a placeholder URL is worse than one that refuses to
13
+ * start.
14
+ */
15
+ export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver = DEFAULT_BROWSER_DRIVER, importDriver = (specifier) => import(specifier), }) => {
16
+ const module = await importDriver(driver).catch(() => {
17
+ const install = driver === DEFAULT_BROWSER_DRIVER
18
+ ? `Run 'yarn add -D ${driver} @playwright/test'`
19
+ : `Install '${driver}', or point scenarios.browserDriver at a package that is`;
20
+ throw new Error(`Scenarios ${browserScenarios.join(', ')} declare browser steps but '${driver}' could not be loaded. ` +
21
+ `${install}, or run with --no-browser to skip them.`);
22
+ });
23
+ const config = (module.browserConfigFromEnv?.({ appUrl, apiUrl }) ?? {
24
+ appUrl,
25
+ apiUrl,
26
+ });
27
+ if (!config.appUrl || config.appUrlSource === 'default') {
28
+ throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl', and '${driver}' resolved none from the environment. ` +
29
+ `Add it to scenarios.environments.${environment} in pikku.config.json, pass --app-url for a target that only exists at run time, or run with --no-browser to skip them.`);
30
+ }
31
+ const options = {
32
+ secret,
33
+ actors,
34
+ signInPath,
35
+ failureDir,
36
+ config,
37
+ };
38
+ if (module.createScenarioBrowserProvider) {
39
+ return module.createScenarioBrowserProvider(options);
40
+ }
41
+ if (module.PlaywrightScenarioBrowserProvider) {
42
+ return new module.PlaywrightScenarioBrowserProvider(options);
43
+ }
44
+ throw new Error(`'${driver}' is not a scenario browser driver: it exports neither ` +
45
+ `'createScenarioBrowserProvider' nor a provider class. A driver returns an object with ` +
46
+ `sessionFor() and close(), and optionally reset() and captureFailure().`);
47
+ };
48
+ export const scenarioBrowserLifecycle = (provider) => ({
49
+ // Deliberately NOT swallowed: a browser that cannot be reset would run the
50
+ // next scenario against the last one's session, and a scenario failing on
51
+ // its reset is far easier to diagnose than one failing on stale state.
52
+ reset: async () => {
53
+ await provider?.reset?.();
54
+ },
55
+ captureFailure: async (label) => {
56
+ try {
57
+ return (await provider?.captureFailure?.(label)) ?? [];
58
+ }
59
+ catch {
60
+ // This runs while a scenario is already failing. Whatever went wrong
61
+ // here matters less than the error we were called to describe.
62
+ return [];
63
+ }
64
+ },
65
+ close: async () => {
66
+ await provider?.close();
67
+ },
68
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolving `pikku scenario run <environment>` to the URLs the run targets.
3
+ *
4
+ * The configured environment is the base; `--api-url`/`--app-url` override it
5
+ * for one invocation, which is what makes a suite runnable against a target
6
+ * that only exists at run time (a freshly provisioned sandbox) without writing
7
+ * a pikku.config.json per deployment.
8
+ */
9
+ /** One entry of `scenarios.environments` in pikku.config.json. */
10
+ export interface ScenarioEnvironment {
11
+ apiUrl: string;
12
+ signInPath?: string;
13
+ rpcPath?: string;
14
+ appUrl?: string;
15
+ }
16
+ export interface ResolveScenarioEnvironmentOptions {
17
+ /** The name given on the command line. Must exist in config. */
18
+ environment: string;
19
+ environments: Record<string, ScenarioEnvironment>;
20
+ /** `--api-url`, overriding the configured `apiUrl`. */
21
+ apiUrl?: string;
22
+ /** `--app-url`, overriding the configured `appUrl`. */
23
+ appUrl?: string;
24
+ /** `--spawn`, which is only coherent against a local `apiUrl`. */
25
+ spawn?: boolean;
26
+ }
27
+ /**
28
+ * Applied once, so actors, step env, the browser driver and the spawned server
29
+ * all see the same target. The environment is still looked up by name: the
30
+ * flags override fields on a configured environment, they do not invent one.
31
+ */
32
+ export declare const resolveScenarioEnvironment: ({ environment, environments, apiUrl, appUrl, spawn, }: ResolveScenarioEnvironmentOptions) => ScenarioEnvironment;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resolving `pikku scenario run <environment>` to the URLs the run targets.
3
+ *
4
+ * The configured environment is the base; `--api-url`/`--app-url` override it
5
+ * for one invocation, which is what makes a suite runnable against a target
6
+ * that only exists at run time (a freshly provisioned sandbox) without writing
7
+ * a pikku.config.json per deployment.
8
+ */
9
+ const LOCAL_HOSTNAMES = new Set([
10
+ 'localhost',
11
+ '127.0.0.1',
12
+ '0.0.0.0',
13
+ '::1',
14
+ '[::1]',
15
+ ]);
16
+ /**
17
+ * Absolute URLs only: the value is concatenated with paths (`${apiUrl}/rpc`)
18
+ * and parsed by the spawn path, so a relative one fails far from its cause.
19
+ */
20
+ const assertAbsoluteUrl = (flag, value) => {
21
+ let url;
22
+ try {
23
+ url = new URL(value);
24
+ }
25
+ catch {
26
+ throw new Error(`${flag} '${value}' is not a valid absolute URL — pass one including the scheme, e.g. ${flag} https://sandbox-a1b2.example.com/api`);
27
+ }
28
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
29
+ throw new Error(`${flag} '${value}' must be an http(s) URL, not '${url.protocol.replace(':', '')}'.`);
30
+ }
31
+ return url;
32
+ };
33
+ /**
34
+ * Applied once, so actors, step env, the browser driver and the spawned server
35
+ * all see the same target. The environment is still looked up by name: the
36
+ * flags override fields on a configured environment, they do not invent one.
37
+ */
38
+ export const resolveScenarioEnvironment = ({ environment, environments, apiUrl, appUrl, spawn = false, }) => {
39
+ const configured = environments[environment];
40
+ if (!configured) {
41
+ const known = Object.keys(environments);
42
+ throw new Error(`Unknown scenario environment '${environment}'. ` +
43
+ (known.length
44
+ ? `Configured environments: ${known.join(', ')}`
45
+ : `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
46
+ }
47
+ const resolved = { ...configured };
48
+ if (apiUrl !== undefined) {
49
+ const url = assertAbsoluteUrl('--api-url', apiUrl);
50
+ if (spawn && !LOCAL_HOSTNAMES.has(url.hostname)) {
51
+ throw new Error(`--spawn starts a server on this machine, but --api-url points at '${url.hostname}'. ` +
52
+ `Drop --spawn to run against an already-serving target, or point --api-url at localhost.`);
53
+ }
54
+ resolved.apiUrl = apiUrl;
55
+ }
56
+ if (appUrl !== undefined) {
57
+ assertAbsoluteUrl('--app-url', appUrl);
58
+ resolved.appUrl = appUrl;
59
+ }
60
+ return resolved;
61
+ };