@pikku/cli 0.12.91 → 0.12.93

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 (241) hide show
  1. package/LICENSE +106 -0
  2. package/console-app/assets/{index-D0HG8q0B.css → index-BYj7ZbUU.css} +1 -1
  3. package/console-app/assets/{index-CpkvmAGb.js → index-CmAplHjZ.js} +151 -149
  4. package/console-app/index.html +2 -2
  5. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  7. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  8. package/dist/.pikku/cli/pikku-cli-channel.js +16 -1
  9. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +15 -0
  14. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  17. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +48 -0
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  19. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  21. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  22. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  23. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  24. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  26. package/dist/.pikku/function/pikku-functions-meta.gen.json +73 -40
  27. package/dist/.pikku/function/pikku-functions.gen.js +1 -3
  28. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  29. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  31. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  32. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +2 -1
  33. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +2 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  37. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  38. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  40. package/dist/.pikku/pikku-types.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  42. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  45. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  47. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +3 -1
  48. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  50. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  51. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  52. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
  53. package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
  54. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  55. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  56. package/dist/.pikku/schemas/register.gen.js +13 -3
  57. package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
  58. package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
  59. package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
  60. package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
  61. package/dist/.pikku/schemas/schemas/KnowledgeIndexInput.schema.json +1 -0
  62. package/dist/.pikku/schemas/schemas/KnowledgeIndexOutput.schema.json +1 -0
  63. package/dist/.pikku/schemas/schemas/KnowledgeValidateInput.schema.json +1 -0
  64. package/dist/.pikku/schemas/schemas/KnowledgeValidateOutput.schema.json +1 -0
  65. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  66. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
  67. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
  68. package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
  69. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  70. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  71. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  72. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  73. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  74. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  75. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  76. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  77. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  78. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  79. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  80. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +2 -8
  81. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +1 -1
  82. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +1 -1
  83. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  84. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  85. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  86. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  87. package/dist/bin/pikku-bin.mjs +2 -2
  88. package/dist/src/cli.wiring.js +29 -0
  89. package/dist/src/deploy/analyzer/analyzer.js +22 -5
  90. package/dist/src/deploy/build-pipeline.js +5 -1
  91. package/dist/src/fabric/fabric-commands.d.ts +37 -9
  92. package/dist/src/fabric/fabric-commands.js +12 -0
  93. package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
  94. package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
  95. package/dist/src/fabric/functions/secrets-list.function.js +12 -10
  96. package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
  97. package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
  98. package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
  99. package/dist/src/fabric/functions/secrets-set.function.js +18 -7
  100. package/dist/src/fabric/functions/validate.function.js +16 -7
  101. package/dist/src/fabric/lib/http.d.ts +7 -1
  102. package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
  103. package/dist/src/fabric/lib/sealed-box.js +72 -0
  104. package/dist/src/functions/commands/dev.js +9 -0
  105. package/dist/src/functions/commands/knowledge-index.d.ts +30 -0
  106. package/dist/src/functions/commands/knowledge-index.js +11 -0
  107. package/dist/src/functions/commands/knowledge-validate.d.ts +33 -0
  108. package/dist/src/functions/commands/knowledge-validate.js +11 -0
  109. package/dist/src/functions/commands/new-addon.d.ts +18 -0
  110. package/dist/src/functions/commands/new-addon.js +40 -24
  111. package/dist/src/functions/commands/pikku-command-bootstrap.js +1 -0
  112. package/dist/src/functions/commands/scenario-browser.d.ts +17 -2
  113. package/dist/src/functions/commands/scenario-browser.js +16 -8
  114. package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
  115. package/dist/src/functions/commands/scenario-environment.js +61 -0
  116. package/dist/src/functions/commands/scenario.d.ts +6 -0
  117. package/dist/src/functions/commands/scenario.js +14 -9
  118. package/dist/src/functions/commands/skills.js +27 -61
  119. package/dist/src/functions/db/db-codegen.d.ts +6 -0
  120. package/dist/src/functions/db/db-codegen.js +9 -0
  121. package/dist/src/functions/db/db-migrator.js +14 -0
  122. package/dist/src/functions/db/local-db.d.ts +8 -0
  123. package/dist/src/functions/db/local-db.js +24 -2
  124. package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
  125. package/dist/src/functions/db/migration-identifiers.js +262 -0
  126. package/dist/src/functions/db/migration-provenance.d.ts +35 -0
  127. package/dist/src/functions/db/migration-provenance.js +80 -0
  128. package/dist/src/functions/db/schema-sql.d.ts +43 -0
  129. package/dist/src/functions/db/schema-sql.js +135 -0
  130. package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
  131. package/dist/src/functions/knowledge/render.d.ts +3 -0
  132. package/dist/src/functions/knowledge/render.js +66 -0
  133. package/dist/src/functions/knowledge/schemas.d.ts +51 -0
  134. package/dist/src/functions/knowledge/schemas.js +25 -0
  135. package/dist/src/functions/wirings/credentials/serialize-credentials-types.js +8 -1
  136. package/dist/src/functions/wirings/functions/schemas.js +13 -1
  137. package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
  138. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
  139. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
  140. package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +13 -0
  141. package/dist/src/functions/wirings/scenarios/scenario-partition.js +15 -2
  142. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
  143. package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
  144. package/dist/src/functions/wirings/secrets/serialize-secrets-types.js +10 -3
  145. package/dist/src/functions/wirings/variables/serialize-variables-types.js +8 -1
  146. package/dist/src/functions/workflows/all.workflow.js +2 -7
  147. package/dist/src/utils/file-writer.js +14 -2
  148. package/dist/src/utils/pikku-cli-config.d.ts +14 -0
  149. package/dist/src/utils/pikku-cli-config.js +34 -0
  150. package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
  151. package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
  152. package/dist/src/utils/serialize-schemas.d.ts +3 -1
  153. package/dist/src/utils/serialize-schemas.js +39 -4
  154. package/dist/tsconfig.tsbuildinfo +1 -1
  155. package/package.json +10 -9
  156. package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
  157. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
  158. package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
  159. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
  160. package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
  161. package/skills/pikku-addon/SKILL.md +0 -243
  162. package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
  163. package/skills/pikku-ai-agent/SKILL.md +0 -231
  164. package/skills/pikku-ai-vercel/SKILL.md +0 -82
  165. package/skills/pikku-ai-voice/SKILL.md +0 -88
  166. package/skills/pikku-audit/SKILL.md +0 -175
  167. package/skills/pikku-aws/SKILL.md +0 -111
  168. package/skills/pikku-backblaze/SKILL.md +0 -71
  169. package/skills/pikku-better-auth/SKILL.md +0 -298
  170. package/skills/pikku-cli/SKILL.md +0 -198
  171. package/skills/pikku-cli/references/complete-example.md +0 -82
  172. package/skills/pikku-concepts/SKILL.md +0 -250
  173. package/skills/pikku-concepts/references/concept-mapping.md +0 -556
  174. package/skills/pikku-concepts/references/packages.md +0 -29
  175. package/skills/pikku-config/SKILL.md +0 -212
  176. package/skills/pikku-cron/SKILL.md +0 -214
  177. package/skills/pikku-deploy-azure/SKILL.md +0 -71
  178. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
  179. package/skills/pikku-deploy-express/SKILL.md +0 -86
  180. package/skills/pikku-deploy-fastify/SKILL.md +0 -73
  181. package/skills/pikku-deploy-lambda/SKILL.md +0 -110
  182. package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
  183. package/skills/pikku-deploy-uws/SKILL.md +0 -88
  184. package/skills/pikku-deps/SKILL.md +0 -98
  185. package/skills/pikku-emails/SKILL.md +0 -163
  186. package/skills/pikku-fabric/SKILL.md +0 -318
  187. package/skills/pikku-fabric-debug/SKILL.md +0 -112
  188. package/skills/pikku-feature/SKILL.md +0 -258
  189. package/skills/pikku-gateway-slack/SKILL.md +0 -115
  190. package/skills/pikku-http/SKILL.md +0 -220
  191. package/skills/pikku-http/references/http-options.md +0 -55
  192. package/skills/pikku-i18n/SKILL.md +0 -137
  193. package/skills/pikku-info/SKILL.md +0 -100
  194. package/skills/pikku-jose/SKILL.md +0 -105
  195. package/skills/pikku-kysely/SKILL.md +0 -219
  196. package/skills/pikku-machine-auth/SKILL.md +0 -183
  197. package/skills/pikku-mcp/SKILL.md +0 -241
  198. package/skills/pikku-middleware/SKILL.md +0 -231
  199. package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
  200. package/skills/pikku-mongodb/SKILL.md +0 -105
  201. package/skills/pikku-n8n-import/SKILL.md +0 -109
  202. package/skills/pikku-n8n-import/SPEC.md +0 -84
  203. package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
  204. package/skills/pikku-n8n-import/references/code-translation.md +0 -121
  205. package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
  206. package/skills/pikku-paraglide/SKILL.md +0 -117
  207. package/skills/pikku-permissions/SKILL.md +0 -192
  208. package/skills/pikku-pino/SKILL.md +0 -79
  209. package/skills/pikku-product-second-opinion/README.md +0 -43
  210. package/skills/pikku-product-second-opinion/SKILL.md +0 -158
  211. package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
  212. package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
  213. package/skills/pikku-queue/SKILL.md +0 -240
  214. package/skills/pikku-react/SKILL.md +0 -212
  215. package/skills/pikku-react-query/SKILL.md +0 -242
  216. package/skills/pikku-realtime/SKILL.md +0 -236
  217. package/skills/pikku-realtime/references/other-routes.md +0 -23
  218. package/skills/pikku-redis/SKILL.md +0 -90
  219. package/skills/pikku-rpc/SKILL.md +0 -171
  220. package/skills/pikku-rtl/SKILL.md +0 -219
  221. package/skills/pikku-scenario/SKILL.md +0 -382
  222. package/skills/pikku-schedule/SKILL.md +0 -57
  223. package/skills/pikku-schema-ajv/SKILL.md +0 -62
  224. package/skills/pikku-schema-cfworker/SKILL.md +0 -63
  225. package/skills/pikku-security/SKILL.md +0 -108
  226. package/skills/pikku-services/SKILL.md +0 -248
  227. package/skills/pikku-services/references/audit-wire-service.md +0 -34
  228. package/skills/pikku-software-archaeology/README.md +0 -70
  229. package/skills/pikku-software-archaeology/SKILL.md +0 -186
  230. package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
  231. package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
  232. package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
  233. package/skills/pikku-tag-middleware/SKILL.md +0 -13
  234. package/skills/pikku-template-clone/SKILL.md +0 -40
  235. package/skills/pikku-trigger/SKILL.md +0 -181
  236. package/skills/pikku-versioning/SKILL.md +0 -173
  237. package/skills/pikku-websocket/SKILL.md +0 -243
  238. package/skills/pikku-workflow/SKILL.md +0 -172
  239. package/skills/pikku-workflow/references/workflow-reference.md +0 -63
  240. package/skills/pikku-workflows-client/SKILL.md +0 -150
  241. package/skills/pikku-ws/SKILL.md +0 -47
@@ -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,6 +11,7 @@ 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';
@@ -115,6 +116,14 @@ export const dev = pikkuSessionlessFunc({
115
116
  return;
116
117
  }
117
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
+ }
118
127
  const configModule = await loadUserModule(pikkuConfigFactory.file);
119
128
  const servicesModule = await loadUserModule(singletonServicesFactory.file);
120
129
  const userCreateConfig = configModule[pikkuConfigFactory.variable];
@@ -0,0 +1,30 @@
1
+ import { renderKnowledgeIndex } from '../knowledge/render.js';
2
+ export declare const knowledgeIndex: import("#pikku").PikkuFunctionConfig<{
3
+ check?: boolean | undefined;
4
+ }, {
5
+ ok: boolean;
6
+ check: boolean;
7
+ files: {
8
+ path: string;
9
+ action: "unchanged" | "created" | "updated";
10
+ }[];
11
+ }, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
12
+ check?: boolean | undefined;
13
+ }, {
14
+ ok: boolean;
15
+ check: boolean;
16
+ files: {
17
+ path: string;
18
+ action: "unchanged" | "created" | "updated";
19
+ }[];
20
+ }, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
21
+ check?: boolean | undefined;
22
+ }, {
23
+ ok: boolean;
24
+ check: boolean;
25
+ files: {
26
+ path: string;
27
+ action: "unchanged" | "created" | "updated";
28
+ }[];
29
+ }, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
30
+ export { renderKnowledgeIndex };
@@ -0,0 +1,11 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { runKnowledgeIndex } from '@pikku/knowledge';
3
+ import { renderKnowledgeIndex } from '../knowledge/render.js';
4
+ import { KnowledgeIndexInputSchema, KnowledgeIndexOutputSchema, } from '../knowledge/schemas.js';
5
+ export const knowledgeIndex = pikkuSessionlessFunc({
6
+ description: "Refresh every index.md so each section lists the notes actually in it, replacing only the generated block and leaving each index's prose alone.",
7
+ input: KnowledgeIndexInputSchema,
8
+ output: KnowledgeIndexOutputSchema,
9
+ func: async ({ config }, { check }) => runKnowledgeIndex(config.rootDir, check ?? false),
10
+ });
11
+ export { renderKnowledgeIndex };
@@ -0,0 +1,33 @@
1
+ import { renderKnowledgeValidate } from '../knowledge/render.js';
2
+ export declare const knowledgeValidate: import("#pikku").PikkuFunctionConfig<Record<string, never>, {
3
+ ok: boolean;
4
+ notes: number;
5
+ findings: {
6
+ id: string;
7
+ severity: "warn" | "error" | "info";
8
+ message: string;
9
+ path: string;
10
+ fixHint: string;
11
+ }[];
12
+ }, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<Record<string, never>, {
13
+ ok: boolean;
14
+ notes: number;
15
+ findings: {
16
+ id: string;
17
+ severity: "warn" | "error" | "info";
18
+ message: string;
19
+ path: string;
20
+ fixHint: string;
21
+ }[];
22
+ }, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<Record<string, never>, {
23
+ ok: boolean;
24
+ notes: number;
25
+ findings: {
26
+ id: string;
27
+ severity: "warn" | "error" | "info";
28
+ message: string;
29
+ path: string;
30
+ fixHint: string;
31
+ }[];
32
+ }, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
33
+ export { renderKnowledgeValidate };
@@ -0,0 +1,11 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { runKnowledgeValidate } from '@pikku/knowledge';
3
+ import { renderKnowledgeValidate } from '../knowledge/render.js';
4
+ import { KnowledgeValidateInputSchema, KnowledgeValidateOutputSchema, } from '../knowledge/schemas.js';
5
+ export const knowledgeValidate = pikkuSessionlessFunc({
6
+ description: 'Check the knowledge base against the app-project profile: every note typed, every section indexed, every slice gated, and every resource: pointing at something that still exists.',
7
+ input: KnowledgeValidateInputSchema,
8
+ output: KnowledgeValidateOutputSchema,
9
+ func: async ({ config }) => runKnowledgeValidate(config.rootDir, config.outDir),
10
+ });
11
+ export { renderKnowledgeValidate };
@@ -9,6 +9,24 @@
9
9
  * the only option there.
10
10
  */
11
11
  export declare function resolveAddonDepProtocol(baseDir: string): string;
12
+ export interface AddonVars {
13
+ name: string;
14
+ camelName: string;
15
+ pascalName: string;
16
+ screamingName: string;
17
+ displayName: string;
18
+ description: string;
19
+ category: string;
20
+ addonDepProtocol: string;
21
+ }
22
+ export declare function getAddonFiles(vars: AddonVars, flags: {
23
+ secret: boolean;
24
+ variable: boolean;
25
+ oauth: boolean;
26
+ credential?: 'apikey' | 'bearer' | 'oauth2';
27
+ /** Delegated login: credential is the upstream token + expiry, checked per call. */
28
+ delegated?: boolean;
29
+ }): Record<string, string>;
12
30
  export declare const pikkuNewAddon: import("#pikku").PikkuFunctionConfig<{
13
31
  name: string;
14
32
  displayName?: string;
@@ -110,7 +110,23 @@ function sanitizeAddonName(raw) {
110
110
  name = `x${name}`;
111
111
  return name || raw;
112
112
  }
113
- function getAddonFiles(vars, flags) {
113
+ /**
114
+ * `--display-name` and `--description` are free-form prose, so an apostrophe in
115
+ * them is ordinary — "Bob's CRM". `JSON.stringify` emits a valid, escaped
116
+ * string literal; interpolating the raw text into a quoted one terminates the
117
+ * string and the scaffolded file no longer parses. Prose composed with other
118
+ * words is stringified as a whole, not swapped in place.
119
+ */
120
+ const literal = (value) => JSON.stringify(value);
121
+ /**
122
+ * The same prose inside a generated template literal, where a backtick or a
123
+ * `${` opens an interpolation rather than ending the string — so the emitted
124
+ * message keeps its own `\${response.status}` holes.
125
+ */
126
+ const inTemplate = (value) => value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
127
+ /** The same prose on a `//` comment line, where a newline ends the comment. */
128
+ const inComment = (value) => value.replace(/\s+/g, ' ').trim();
129
+ export function getAddonFiles(vars, flags) {
114
130
  const { name, camelName, pascalName, screamingName, displayName, description, category, } = vars;
115
131
  const files = {};
116
132
  // package.json
@@ -205,7 +221,7 @@ ${description}
205
221
  6. Build: \`yarn install && yarn pikku && yarn build\`
206
222
  `;
207
223
  // src/index.ts
208
- files['src/index.ts'] = `// ${displayName} functions
224
+ files['src/index.ts'] = `// ${inComment(displayName)} functions
209
225
  // export { ${camelName}Operation } from './functions/operation.function.js'
210
226
  `;
211
227
  // src/services.ts
@@ -224,10 +240,10 @@ export const createWireServices = pikkuAddonWireServices(
224
240
  }
225
241
  const cred = await wire.getCredential<{ token: string; expiresAt?: number }>('${camelName}')
226
242
  if (!cred?.token) {
227
- throw new UnauthorizedError('No ${displayName} session — sign in again')
243
+ throw new UnauthorizedError(${literal(`No ${displayName} session — sign in again`)})
228
244
  }
229
245
  if (cred.expiresAt && cred.expiresAt * 1000 < Date.now()) {
230
- throw new UnauthorizedError('${displayName} session expired — sign in again')
246
+ throw new UnauthorizedError(${literal(`${displayName} session expired — sign in again`)})
231
247
  }
232
248
  const ${camelName} = new ${pascalName}Service(cred, variables)
233
249
 
@@ -275,7 +291,7 @@ export const createWireServices = pikkuAddonWireServices(
275
291
  }
276
292
  const cred = await wire.getCredential<{ accessToken: string }>('${camelName}')
277
293
  if (!cred?.accessToken) {
278
- throw new UnauthorizedError('No ${displayName} connection — connect ${displayName} first')
294
+ throw new UnauthorizedError(${literal(`No ${displayName} connection — connect ${displayName} first`)})
279
295
  }
280
296
  const ${camelName} = new ${pascalName}Service(cred, variables)
281
297
 
@@ -363,7 +379,7 @@ export class ${pascalName}Service {
363
379
 
364
380
  if (!response.ok) {
365
381
  const errorText = await response.text()
366
- throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
382
+ throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
367
383
  }
368
384
 
369
385
  return response.json() as Promise<T>
@@ -416,7 +432,7 @@ export class ${pascalName}Service {
416
432
 
417
433
  if (!response.ok) {
418
434
  const errorText = await response.text()
419
- throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
435
+ throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
420
436
  }
421
437
 
422
438
  return response.json() as Promise<T>
@@ -464,7 +480,7 @@ export class ${pascalName}Service {
464
480
 
465
481
  if (!response.ok) {
466
482
  const errorText = await response.text()
467
- throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
483
+ throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
468
484
  }
469
485
 
470
486
  return response.json() as Promise<T>
@@ -507,7 +523,7 @@ export class ${pascalName}Service {
507
523
 
508
524
  if (!response.ok) {
509
525
  const errorText = await response.text()
510
- throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)
526
+ throw new Error(\`${inTemplate(displayName)} API error (\${response.status}): \${errorText}\`)
511
527
  }
512
528
 
513
529
  return response.json() as Promise<T>
@@ -553,13 +569,13 @@ export interface Services extends CoreServices<SingletonServices> {}
553
569
  import { wireCredential } from '@pikku/core/credential'
554
570
 
555
571
  export const ${camelName}CredentialSchema = z.object({
556
- apiKey: z.string().describe('${displayName} API key'),
572
+ apiKey: z.string().describe(${literal(`${displayName} API key`)}),
557
573
  })
558
574
 
559
575
  wireCredential({
560
576
  name: '${camelName}',
561
- displayName: '${displayName}',
562
- description: '${description}',
577
+ displayName: ${literal(displayName)},
578
+ description: ${literal(description)},
563
579
  type: 'wire',
564
580
  schema: ${camelName}CredentialSchema,
565
581
  })
@@ -567,10 +583,10 @@ wireCredential({
567
583
  }
568
584
  else if (flags.credential === 'bearer') {
569
585
  const credentialFields = flags.delegated
570
- ? ` token: z.string().describe('${displayName} session token captured at delegated sign-in'),
586
+ ? ` token: z.string().describe(${literal(`${displayName} session token captured at delegated sign-in`)}),
571
587
  expiresAt: z.number().optional().describe('Token expiry (epoch seconds)'),
572
588
  tenantId: z.string().optional().describe('Upstream tenant id'),`
573
- : ` token: z.string().describe('${displayName} bearer token'),`;
589
+ : ` token: z.string().describe(${literal(`${displayName} bearer token`)}),`;
574
590
  files[`src/${name}.credential.ts`] = `import { z } from 'zod'
575
591
  import { wireCredential } from '@pikku/core/credential'
576
592
 
@@ -580,8 +596,8 @@ ${credentialFields}
580
596
 
581
597
  wireCredential({
582
598
  name: '${camelName}',
583
- displayName: '${displayName}',
584
- description: '${description}',
599
+ displayName: ${literal(displayName)},
600
+ description: ${literal(description)},
585
601
  type: 'wire',
586
602
  schema: ${camelName}CredentialSchema,
587
603
  })
@@ -599,8 +615,8 @@ export const ${camelName}TokenSchema = z.object({
599
615
 
600
616
  wireCredential({
601
617
  name: '${camelName}',
602
- displayName: '${displayName}',
603
- description: '${description}',
618
+ displayName: ${literal(displayName)},
619
+ description: ${literal(description)},
604
620
  type: 'wire',
605
621
  schema: ${camelName}TokenSchema,
606
622
  oauth2: {
@@ -614,8 +630,8 @@ wireCredential({
614
630
 
615
631
  wireSecret({
616
632
  name: '${camelName}OAuthApp',
617
- displayName: '${displayName} OAuth App',
618
- description: 'OAuth2 app credentials for ${displayName}',
633
+ displayName: ${literal(`${displayName} OAuth App`)},
634
+ description: ${literal(`OAuth2 app credentials for ${displayName}`)},
619
635
  secretId: '${screamingName}_OAUTH_APP',
620
636
  })
621
637
  `;
@@ -625,7 +641,7 @@ wireSecret({
625
641
  import { wireSecret } from '@pikku/core/secret'
626
642
 
627
643
  export const ${camelName}SecretsSchema = z.object({
628
- apiKey: z.string().describe('${displayName} API key'),
644
+ apiKey: z.string().describe(${literal(`${displayName} API key`)}),
629
645
  // Add other secret fields as needed
630
646
  })
631
647
 
@@ -633,8 +649,8 @@ export type ${pascalName}Secrets = z.infer<typeof ${camelName}SecretsSchema>
633
649
 
634
650
  wireSecret({
635
651
  name: '${camelName}',
636
- displayName: '${displayName} API',
637
- description: '${description}',
652
+ displayName: ${literal(`${displayName} API`)},
653
+ description: ${literal(description)},
638
654
  secretId: '${screamingName}_CREDENTIALS',
639
655
  schema: ${camelName}SecretsSchema,
640
656
  })
@@ -649,7 +665,7 @@ export const ${camelName}VariableSchema = z.string().optional().describe('TODO:
649
665
 
650
666
  wireVariable({
651
667
  name: '${camelName}_variable',
652
- displayName: '${displayName} Variable',
668
+ displayName: ${literal(`${displayName} Variable`)},
653
669
  description: 'TODO: describe this variable',
654
670
  variableId: '${screamingName}_VARIABLE',
655
671
  schema: ${camelName}VariableSchema,
@@ -70,6 +70,7 @@ export const pikkuBootstrap = pikkuSessionlessFunc({
70
70
  // in place however this module is reached.
71
71
  const scenarioBootstrapImports = [
72
72
  config.bootstrapFile,
73
+ join(config.scenarioSchemaDirectory, 'register.gen.ts'),
73
74
  config.scenarioStepsMetaFile,
74
75
  config.scenarioStepsFile,
75
76
  config.scenarioWiringsMetaFile,
@@ -29,9 +29,17 @@ export interface ScenarioBrowserDriver {
29
29
  createScenarioBrowserProvider?: (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
30
30
  /** The class form the bundled driver exports. */
31
31
  PlaywrightScenarioBrowserProvider?: new (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
32
- /** Driver-specific config (headed, slowMo, timeouts) resolved from the env. */
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
+ */
33
41
  browserConfigFromEnv?: (overrides: {
34
- appUrl: string;
42
+ appUrl?: string;
35
43
  apiUrl: string;
36
44
  }) => unknown;
37
45
  }
@@ -55,6 +63,13 @@ export interface ResolveScenarioBrowserProviderOptions {
55
63
  * Both failure modes here are discovered before the first scenario starts. A
56
64
  * missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
57
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.
58
73
  */
59
74
  export declare const resolveScenarioBrowserProvider: ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver, importDriver, }: ResolveScenarioBrowserProviderOptions) => Promise<ScenarioBrowserProvider>;
60
75
  /**
@@ -4,12 +4,15 @@ export const DEFAULT_BROWSER_DRIVER = '@pikku/playwright';
4
4
  * Both failure modes here are discovered before the first scenario starts. A
5
5
  * missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
6
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.
7
14
  */
8
15
  export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver = DEFAULT_BROWSER_DRIVER, importDriver = (specifier) => import(specifier), }) => {
9
- if (!appUrl) {
10
- throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl'. ` +
11
- `Add it to scenarios.environments.${environment} in pikku.config.json, or run with --no-browser to skip them.`);
12
- }
13
16
  const module = await importDriver(driver).catch(() => {
14
17
  const install = driver === DEFAULT_BROWSER_DRIVER
15
18
  ? `Run 'yarn add -D ${driver} @playwright/test'`
@@ -17,15 +20,20 @@ export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appU
17
20
  throw new Error(`Scenarios ${browserScenarios.join(', ')} declare browser steps but '${driver}' could not be loaded. ` +
18
21
  `${install}, or run with --no-browser to skip them.`);
19
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
+ }
20
31
  const options = {
21
32
  secret,
22
33
  actors,
23
34
  signInPath,
24
35
  failureDir,
25
- config: module.browserConfigFromEnv?.({ appUrl, apiUrl }) ?? {
26
- appUrl,
27
- apiUrl,
28
- },
36
+ config,
29
37
  };
30
38
  if (module.createScenarioBrowserProvider) {
31
39
  return module.createScenarioBrowserProvider(options);
@@ -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
+ };
@@ -10,6 +10,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
10
10
  spawn?: boolean;
11
11
  keepAlive?: boolean;
12
12
  trace?: boolean;
13
+ apiUrl?: string;
14
+ appUrl?: string;
13
15
  }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
14
16
  environment: string;
15
17
  flows?: string;
@@ -21,6 +23,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
21
23
  spawn?: boolean;
22
24
  keepAlive?: boolean;
23
25
  trace?: boolean;
26
+ apiUrl?: string;
27
+ appUrl?: string;
24
28
  }, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
25
29
  environment: string;
26
30
  flows?: string;
@@ -32,4 +36,6 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
32
36
  spawn?: boolean;
33
37
  keepAlive?: boolean;
34
38
  trace?: boolean;
39
+ apiUrl?: string;
40
+ appUrl?: string;
35
41
  }, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
@@ -12,6 +12,7 @@ import { resolveScenarioBrowserProvider, scenarioBrowserLifecycle, } from './sce
12
12
  import { resolveScenarioActors } from '../../utils/resolve-scenario-actors.js';
13
13
  import { spawnDevServer } from '../../server/spawn-dev-server.js';
14
14
  import { buildScenarioPlan } from './scenario-plan.js';
15
+ import { resolveScenarioEnvironment } from './scenario-environment.js';
15
16
  const isScenario = (wf) => wf?.scenario === true;
16
17
  const listScenarios = (state) => Object.entries(state.workflows?.meta ?? {})
17
18
  .filter(([, wf]) => isScenario(wf))
@@ -83,16 +84,20 @@ export const scenarioList = pikkuSessionlessFunc({
83
84
  },
84
85
  });
85
86
  export const scenarioRun = pikkuSessionlessFunc({
86
- func: async ({ logger, config, getInspectorState, variables }, { environment, flows, features, tags, excludeTags, coverage, browser = true, spawn = false, keepAlive = false, trace = false, }) => {
87
+ func: async ({ logger, config, getInspectorState, variables }, { environment, flows, features, tags, excludeTags, coverage, browser = true, spawn = false, keepAlive = false, trace = false, apiUrl, appUrl, }) => {
87
88
  const state = await getInspectorState(true, false, false, true);
88
- const environments = config.scenarios?.environments ?? {};
89
- const env = environments[environment];
90
- if (!env) {
91
- const known = Object.keys(environments);
92
- throw new Error(`Unknown scenario environment '${environment}'. ` +
93
- (known.length
94
- ? `Configured environments: ${known.join(', ')}`
95
- : `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
89
+ // Resolved once, so actors, step env, the browser driver and any spawned
90
+ // server all target the same place — including when the target only exists
91
+ // at run time and arrives through --api-url/--app-url.
92
+ const env = resolveScenarioEnvironment({
93
+ environment,
94
+ environments: config.scenarios?.environments ?? {},
95
+ apiUrl,
96
+ appUrl,
97
+ spawn,
98
+ });
99
+ if (apiUrl || appUrl) {
100
+ logger.info(`Overriding '${environment}': apiUrl ${env.apiUrl}${env.appUrl ? `, appUrl ${env.appUrl}` : ''}`);
96
101
  }
97
102
  if (spawn) {
98
103
  const { hostname, port } = new URL(env.apiUrl);