@pikku/cli 0.12.89 → 0.12.91

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 (162) hide show
  1. package/README.md +25 -2
  2. package/console-app/assets/{index-BaOGIr2O.js → index-CpkvmAGb.js} +148 -148
  3. package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
  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 +21 -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 +6 -1
  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 +52 -1
  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 +4 -4
  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 -13
  27. package/dist/.pikku/function/pikku-functions.gen.js +9 -1
  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 +8 -0
  33. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +8 -0
  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 +4 -0
  48. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
  49. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
  50. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
  51. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
  52. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
  53. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
  54. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
  55. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
  56. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
  57. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  58. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  59. package/dist/.pikku/schemas/register.gen.js +9 -1
  60. package/dist/.pikku/schemas/schemas/DbBaselineInput.schema.json +1 -0
  61. package/dist/.pikku/schemas/schemas/DbCheckInput.schema.json +1 -0
  62. package/dist/.pikku/schemas/schemas/DbCodegenInput.schema.json +1 -0
  63. package/dist/.pikku/schemas/schemas/DbExportInput.schema.json +1 -0
  64. package/dist/.pikku/schemas/schemas/FabricValidateInput.schema.json +1 -1
  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/pikku-scenario-actors.gen.d.ts +19 -0
  81. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
  82. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
  83. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -1
  84. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  85. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  86. package/dist/bin/pikku-bin.mjs +2 -2
  87. package/dist/src/cli.wiring.js +42 -0
  88. package/dist/src/fabric/fabric-commands.d.ts +9 -3
  89. package/dist/src/fabric/fabric-commands.js +6 -0
  90. package/dist/src/fabric/functions/validate.function.d.ts +15 -5
  91. package/dist/src/fabric/functions/validate.function.js +69 -16
  92. package/dist/src/fabric/lib/config.d.ts +11 -1
  93. package/dist/src/fabric/lib/config.js +39 -1
  94. package/dist/src/fabric/lib/frontend-typecheck.d.ts +46 -0
  95. package/dist/src/fabric/lib/frontend-typecheck.js +139 -0
  96. package/dist/src/functions/commands/db-baseline.d.ts +13 -0
  97. package/dist/src/functions/commands/db-baseline.js +48 -0
  98. package/dist/src/functions/commands/db-check.d.ts +12 -0
  99. package/dist/src/functions/commands/db-check.js +64 -0
  100. package/dist/src/functions/commands/db-codegen.d.ts +11 -0
  101. package/dist/src/functions/commands/db-codegen.js +39 -0
  102. package/dist/src/functions/commands/db-export.d.ts +12 -0
  103. package/dist/src/functions/commands/db-export.js +31 -0
  104. package/dist/src/functions/commands/db-generate.js +19 -26
  105. package/dist/src/functions/commands/dev.js +2 -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 +12 -0
  109. package/dist/src/functions/commands/scenario-browser.d.ts +69 -0
  110. package/dist/src/functions/commands/scenario-browser.js +60 -0
  111. package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
  112. package/dist/src/functions/commands/scenario-formatter.js +120 -0
  113. package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
  114. package/dist/src/functions/commands/scenario-ladder.js +132 -0
  115. package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
  116. package/dist/src/functions/commands/scenario-plan.js +106 -0
  117. package/dist/src/functions/commands/scenario.d.ts +18 -0
  118. package/dist/src/functions/commands/scenario.js +305 -38
  119. package/dist/src/functions/commands/serve.js +2 -0
  120. package/dist/src/functions/db/db-migrator.d.ts +35 -4
  121. package/dist/src/functions/db/db-migrator.js +67 -10
  122. package/dist/src/functions/db/local-db.d.ts +235 -13
  123. package/dist/src/functions/db/local-db.js +430 -45
  124. package/dist/src/functions/db/postgres/postgres-introspector.js +19 -1
  125. package/dist/src/functions/db/postgres/postgres-migrator.d.ts +1 -0
  126. package/dist/src/functions/db/postgres/postgres-migrator.js +4 -1
  127. package/dist/src/functions/db/sqlite/sqlite-introspector.js +2 -1
  128. package/dist/src/functions/db/sqlite/sqlite-migrator.d.ts +1 -0
  129. package/dist/src/functions/db/sqlite/sqlite-migrator.js +6 -1
  130. package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
  131. package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
  132. package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +29 -0
  133. package/dist/src/functions/wirings/scenarios/scenario-partition.js +58 -0
  134. package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
  135. package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
  136. package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
  137. package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
  138. package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
  139. package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
  140. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
  141. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
  142. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
  143. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
  144. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
  145. package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
  146. package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
  147. package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
  148. package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
  149. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
  150. package/dist/src/server/server-ready.d.ts +12 -0
  151. package/dist/src/server/server-ready.js +12 -0
  152. package/dist/src/server/spawn-dev-server.d.ts +50 -0
  153. package/dist/src/server/spawn-dev-server.js +112 -0
  154. package/dist/src/services.js +12 -1
  155. package/dist/src/utils/meta-diff.js +5 -2
  156. package/dist/src/utils/pikku-cli-config.js +26 -0
  157. package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
  158. package/dist/src/utils/resolve-scenario-actors.js +74 -0
  159. package/dist/tsconfig.tsbuildinfo +1 -1
  160. package/package.json +16 -6
  161. package/skills/pikku-i18n/SKILL.md +53 -6
  162. package/skills/pikku-scenario/SKILL.md +196 -29
@@ -8,9 +8,9 @@ export const serializeWorkflowMeta = (outputPath, metaDir, workflowNames, packag
8
8
  const pkg = packageName ? `'${packageName}'` : 'null';
9
9
  if (workflowNames.length === 0) {
10
10
  return `import { pikkuState } from '@pikku/core/internal'
11
- import type { SerializedWorkflowGraphs } from '@pikku/inspector/workflow-graph'
11
+ import type { WorkflowsRuntimeMeta } from '@pikku/core/workflow/types'
12
12
 
13
- const workflowsMeta: SerializedWorkflowGraphs = {}
13
+ const workflowsMeta: WorkflowsRuntimeMeta = {}
14
14
 
15
15
  pikkuState(${pkg}, 'workflows', 'meta', workflowsMeta)`;
16
16
  }
@@ -32,13 +32,13 @@ pikkuState(${pkg}, 'workflows', 'meta', workflowsMeta)`;
32
32
  })
33
33
  .join('\n');
34
34
  return `import { pikkuState } from '@pikku/core/internal'
35
- import type { SerializedWorkflowGraphs } from '@pikku/inspector/workflow-graph'
35
+ import type { WorkflowsRuntimeMeta } from '@pikku/core/workflow/types'
36
36
 
37
37
  ${imports}
38
38
 
39
39
  const workflowsMeta = {
40
40
  ${metaEntries}
41
- } as SerializedWorkflowGraphs
41
+ } as WorkflowsRuntimeMeta
42
42
 
43
43
  pikkuState(${pkg}, 'workflows', 'meta', workflowsMeta)`;
44
44
  };
@@ -1 +1 @@
1
- export declare const serializeWorkflowTypes: (functionTypesImportPath: string, rpcMapImportPath: string, workflowMapImportPath: string, agentMapImportPath: string, scopesImportPath: string) => string;
1
+ export declare const serializeWorkflowTypes: (functionTypesImportPath: string, rpcMapImportPath: string, workflowMapImportPath: string, agentMapImportPath: string, scopesImportPath: string, scenarioStepMapImportPath?: string, scenarioActorsImportPath?: string) => string;
@@ -1,4 +1,4 @@
1
- export const serializeWorkflowTypes = (functionTypesImportPath, rpcMapImportPath, workflowMapImportPath, agentMapImportPath, scopesImportPath) => {
1
+ export const serializeWorkflowTypes = (functionTypesImportPath, rpcMapImportPath, workflowMapImportPath, agentMapImportPath, scopesImportPath, scenarioStepMapImportPath = './pikku-scenario-step-map.gen.js', scenarioActorsImportPath = './pikku-scenario-actors.gen.js') => {
2
2
  return `import { WorkflowCancelledException } from '@pikku/core/workflow'
3
3
  import { template } from '@pikku/core/workflow'
4
4
  import {
@@ -6,13 +6,17 @@ import {
6
6
  type PikkuWorkflowGraphConfig,
7
7
  type PikkuWorkflowGraphResult,
8
8
  } from '@pikku/core/workflow'
9
- import type { PikkuWorkflowWire, PikkuScenarioWire, WorkflowStepOptions } from '@pikku/core/workflow'
9
+ import type { PikkuWorkflowWire, PikkuScenarioWire, WorkflowStepOptions, ScenarioStepOptions } from '@pikku/core/workflow'
10
10
 
11
11
  export { WorkflowCancelledException }
12
12
  import type { PikkuFunctionSessionless, PikkuFunctionConfig } from '${functionTypesImportPath}'
13
13
  import type { FlattenedRPCMap } from '${rpcMapImportPath}'
14
14
  import type { FlattenedWorkflowMap } from '${workflowMapImportPath}'
15
15
  import type { AgentMap as FlattenedAgentMap } from '${agentMapImportPath}'
16
+ import type { FlattenedScenarioStepMap } from '${scenarioStepMapImportPath}'
17
+ import type { TypedScenarioActors } from '${scenarioActorsImportPath}'
18
+
19
+ export type { TypedScenarioActors }
16
20
 
17
21
  export { template }
18
22
 
@@ -38,7 +42,44 @@ export interface TypedWorkflow extends PikkuWorkflowWire {
38
42
  ): Promise<T>
39
43
  }
40
44
 
41
- export type TypedScenario = TypedWorkflow & Omit<PikkuScenarioWire, keyof PikkuWorkflowWire>
45
+ /**
46
+ * The typed half of a scenario wire: \`step\` and its \`given\`/\`when\`/\`then\`
47
+ * sugar, narrowed to the names declared by \`pikkuScenarioStep\` in this project.
48
+ * The phase only changes the prose the reporter renders.
49
+ */
50
+ export interface TypedScenarioSteps {
51
+ step<K extends keyof FlattenedScenarioStepMap>(
52
+ stepName: string,
53
+ stepFunc: K,
54
+ data?: FlattenedScenarioStepMap[K]['input'],
55
+ options?: ScenarioStepOptions
56
+ ): Promise<FlattenedScenarioStepMap[K]['output']>
57
+
58
+ given<K extends keyof FlattenedScenarioStepMap>(
59
+ stepName: string,
60
+ stepFunc: K,
61
+ data?: FlattenedScenarioStepMap[K]['input'],
62
+ options?: ScenarioStepOptions
63
+ ): Promise<FlattenedScenarioStepMap[K]['output']>
64
+
65
+ when<K extends keyof FlattenedScenarioStepMap>(
66
+ stepName: string,
67
+ stepFunc: K,
68
+ data?: FlattenedScenarioStepMap[K]['input'],
69
+ options?: ScenarioStepOptions
70
+ ): Promise<FlattenedScenarioStepMap[K]['output']>
71
+
72
+ then<K extends keyof FlattenedScenarioStepMap>(
73
+ stepName: string,
74
+ stepFunc: K,
75
+ data?: FlattenedScenarioStepMap[K]['input'],
76
+ options?: ScenarioStepOptions
77
+ ): Promise<FlattenedScenarioStepMap[K]['output']>
78
+ }
79
+
80
+ export type TypedScenario = TypedWorkflow &
81
+ Omit<PikkuScenarioWire, keyof PikkuWorkflowWire | keyof TypedScenarioSteps> &
82
+ TypedScenarioSteps
42
83
 
43
84
  import type { StandardSchemaV1 } from '@standard-schema/spec'
44
85
  import type { InferSchemaOutput, PikkuPermission, PikkuMiddleware, NodeConfig, PikkuApprovalDescription } from '${functionTypesImportPath}'
@@ -129,6 +170,49 @@ export type PikkuScenarioConfigWithSchema<
129
170
  InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
130
171
  OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown
131
172
  >
173
+ /**
174
+ * Runs before the scenario body. Same signature as \`func\`, but its return
175
+ * value is discarded — a hook is setup, not a step, so it is never recorded
176
+ * on the ladder. Throwing skips the body and fails the run; \`after\` still
177
+ * runs.
178
+ */
179
+ before?: PikkuScenarioHook<InputSchema>
180
+ /**
181
+ * Always runs after the scenario body, in a \`finally\`, whether it passed or
182
+ * failed. Throwing fails a run that would otherwise have passed; on an
183
+ * already-failed run it attaches as the \`cause\` and never replaces the
184
+ * original error.
185
+ */
186
+ after?: PikkuScenarioHook<InputSchema>
187
+ /**
188
+ * Why this scenario is held out of a default run, stated where the scenario
189
+ * is. The scenario still appears in the plan and is reported as skipped
190
+ * rather than quietly omitted; naming it directly with \`--flows\` runs it
191
+ * anyway.
192
+ */
193
+ skip?: string
194
+ }
195
+
196
+ /**
197
+ * A scenario lifecycle hook: the scenario's own \`(services, data, wire)\`
198
+ * signature with its result discarded.
199
+ */
200
+ export type PikkuScenarioHook<
201
+ InputSchema extends StandardSchemaV1 | undefined = undefined
202
+ > = PikkuFunctionScenario<
203
+ InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
204
+ void
205
+ >
206
+
207
+ /**
208
+ * Declares a scenario hook. Returns the function verbatim — a hook is never
209
+ * registered, so this exists purely to give an inline hook a call site to be
210
+ * contextually typed from, the way every other pikku primitive is.
211
+ */
212
+ export function pikkuScenarioHook<In = unknown>(
213
+ hook: PikkuFunctionScenario<In, void>
214
+ ): PikkuFunctionScenario<In, void> {
215
+ return hook
132
216
  }
133
217
 
134
218
  /**
@@ -152,6 +236,151 @@ export function pikkuScenario(func: any) {
152
236
  return typeof func === 'function' ? { func } : func
153
237
  }
154
238
 
239
+ /**
240
+ * A scenario as a feature references it. Any \`pikkuScenario\` export is
241
+ * assignable; \`In\` is recovered from it so the paired form's \`data\` is
242
+ * checked against that scenario's own input.
243
+ */
244
+ export type PikkuScenarioRef<In = any, Out = any> = PikkuFunctionConfig<
245
+ In,
246
+ Out,
247
+ 'scenario' | 'actors',
248
+ PikkuFunctionScenario<In, Out>,
249
+ any,
250
+ any
251
+ >
252
+
253
+ /**
254
+ * One entry in a feature's \`scenarios\` list, validated against itself: a bare
255
+ * scenario, or a scenario paired with the input to run it with. The paired form
256
+ * is gherkin's \`Examples:\` written as an ordinary loop.
257
+ */
258
+ export type PikkuFeatureEntry<Entry> = Entry extends {
259
+ scenario: PikkuScenarioRef<infer In>
260
+ }
261
+ ? { scenario: PikkuScenarioRef<In>; data: In }
262
+ : Entry extends PikkuScenarioRef
263
+ ? Entry
264
+ : never
265
+
266
+ export type PikkuFeatureConfig<Scenarios extends readonly unknown[]> = {
267
+ /** Human-readable name. The export identifier is the id. */
268
+ name: string
269
+ description?: string
270
+ tags?: string[]
271
+ scenarios: { [K in keyof Scenarios]: PikkuFeatureEntry<Scenarios[K]> }
272
+ /**
273
+ * Runs ONCE before the whole group — not before each scenario. Per-scenario
274
+ * setup is the scenario's own \`before\`; gherkin's \`Background:\` is
275
+ * deliberately not expressible here.
276
+ */
277
+ before?: PikkuScenarioHook
278
+ after?: PikkuScenarioHook
279
+ }
280
+
281
+ /**
282
+ * A feature: an ordered group of scenarios, mirroring gherkin's Feature ↔
283
+ * Scenario structure. Scenarios are referenced by imported identifier, so a
284
+ * renamed or deleted scenario is a compile error rather than a silent skip.
285
+ *
286
+ * A scenario does not have to belong to a feature — a void-input scenario
287
+ * still runs standalone.
288
+ *
289
+ * \`\`\`ts
290
+ * export const credentialFeature = pikkuFeature({
291
+ * name: 'Credential API',
292
+ * tags: ['credential'],
293
+ * before: startsMockOAuthServer,
294
+ * after: stopsMockOAuthServer,
295
+ * scenarios: [
296
+ * credentialLazyLoadScenario,
297
+ * ...['stripe', 'google'].map((name) => ({
298
+ * scenario: credentialRoundTripScenario,
299
+ * data: { name },
300
+ * })),
301
+ * ],
302
+ * })
303
+ * \`\`\`
304
+ */
305
+ export function pikkuFeature<const Scenarios extends readonly unknown[]>(
306
+ config: PikkuFeatureConfig<Scenarios>
307
+ ): PikkuFeatureConfig<Scenarios> {
308
+ return config
309
+ }
310
+
311
+ export type PikkuFunctionScenarioStep<
312
+ In = unknown,
313
+ Out = never,
314
+ B extends boolean = false
315
+ > = PikkuFunctionSessionless<In, Out, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep'>
316
+
317
+ export type PikkuScenarioStepConfigWithSchema<
318
+ InputSchema extends StandardSchemaV1 | undefined = undefined,
319
+ OutputSchema extends StandardSchemaV1 | undefined = undefined,
320
+ B extends boolean = false
321
+ > = {
322
+ /** Registered name — this is the string \`scenario.step()\` references. */
323
+ name: string
324
+ /**
325
+ * What this step does, for the console and for whoever reads the source. It
326
+ * is also the fallback prose when no \`template\` is declared, in which case a
327
+ * reporter renders "Given the shopper buys an apple". Defaults to the call
328
+ * site's step name.
329
+ */
330
+ description?: string
331
+ /**
332
+ * The prose a reporter renders for this step, with \`{placeholders}\` filled
333
+ * from the input the step was called with — \`'sees {packageName}'\` reports as
334
+ * "Then the admin sees @pikku/addon-todos". Every input field should appear,
335
+ * so the report names the values under test rather than repeating one
336
+ * sentence per call site.
337
+ */
338
+ template?: string
339
+ /**
340
+ * This step drives a browser. The runner provisions one for the step's actor
341
+ * before calling it — which also makes an actor mandatory — and \`wire.browser\`
342
+ * is non-optional inside \`func\`.
343
+ */
344
+ browser?: B
345
+ title?: string
346
+ tags?: string[]
347
+ input?: InputSchema
348
+ output?: OutputSchema
349
+ errors?: Array<typeof PikkuError>
350
+ func: PikkuFunctionScenarioStep<
351
+ InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
352
+ OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
353
+ B
354
+ >
355
+ }
356
+
357
+ export type PikkuScenarioStepConfig<In, Out, B extends boolean = false> =
358
+ Omit<PikkuScenarioStepConfigWithSchema<undefined, undefined, B>, 'func' | 'input' | 'output'> & {
359
+ func: PikkuFunctionScenarioStep<In, Out, B>
360
+ }
361
+
362
+ /**
363
+ * A named, reusable scenario step. Unlike \`scenario.do\`, which can only name an
364
+ * RPC, a step's body is an ordinary pikku function — so it may drive a browser,
365
+ * invoke RPCs as its actor, or run a workflow.
366
+ *
367
+ * Steps are deliberately NOT registered as RPCs: a browser-driving step must
368
+ * never be network-callable.
369
+ */
370
+ export function pikkuScenarioStep<
371
+ InputSchema extends StandardSchemaV1 | undefined = undefined,
372
+ OutputSchema extends StandardSchemaV1 | undefined = undefined,
373
+ B extends boolean = false
374
+ >(
375
+ config: PikkuScenarioStepConfigWithSchema<InputSchema, OutputSchema, B>
376
+ ): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep', PikkuFunctionScenarioStep<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, B>, InputSchema, OutputSchema>
377
+ export function pikkuScenarioStep<In, Out = unknown, B extends boolean = false>(
378
+ config: PikkuScenarioStepConfig<In, Out, B>
379
+ ): PikkuFunctionConfig<In, Out, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep'>
380
+ export function pikkuScenarioStep(config: any) {
381
+ return config
382
+ }
383
+
155
384
  type TypedRef<T> = { $ref: string; path?: string } & { __phantomType?: T }
156
385
 
157
386
  type TemplateString = {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Printed once the server is listening AND the project's `afterStart`
3
+ * lifecycle has resolved.
4
+ *
5
+ * The runtime's own `listening on …` line is emitted inside `server.start()`,
6
+ * which runs *before* `afterStart` — so anything a project seeds there (users,
7
+ * scopes, fixtures) is still pending when it appears, and a parent process
8
+ * that treats it as readiness races the seed. This marker is the point at
9
+ * which the server is actually usable, and it is what `--spawn` waits for.
10
+ */
11
+ export declare const SERVER_READY_MARKER = "pikku: ready";
12
+ export declare const serverReadyLine: (hostname: string, port: number) => string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Printed once the server is listening AND the project's `afterStart`
3
+ * lifecycle has resolved.
4
+ *
5
+ * The runtime's own `listening on …` line is emitted inside `server.start()`,
6
+ * which runs *before* `afterStart` — so anything a project seeds there (users,
7
+ * scopes, fixtures) is still pending when it appears, and a parent process
8
+ * that treats it as readiness races the seed. This marker is the point at
9
+ * which the server is actually usable, and it is what `--spawn` waits for.
10
+ */
11
+ export const SERVER_READY_MARKER = 'pikku: ready';
12
+ export const serverReadyLine = (hostname, port) => `${SERVER_READY_MARKER} on http://${hostname}:${port}`;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Rejects if anything is already listening on `port`.
3
+ *
4
+ * A readiness check cannot tell "my server" from "someone else's": both answer
5
+ * on the same address once spawned, so a stale server left on the target port
6
+ * silently absorbs a whole run and reports failures that belong to code nobody
7
+ * is looking at. A TCP-level check before spawning is the only point at which
8
+ * the two are distinguishable.
9
+ */
10
+ export declare const assertPortFree: (port: number, host?: string, timeoutMs?: number) => Promise<void>;
11
+ export interface ReadyBarrier {
12
+ /** Feed a chunk of the child's output. */
13
+ observe: (text: string) => void;
14
+ /** Record that the child exited, so the wait fails fast instead of timing out. */
15
+ markExited: (code: number | null) => void;
16
+ wait: (options?: {
17
+ timeoutMs?: number;
18
+ pollMs?: number;
19
+ }) => Promise<void>;
20
+ }
21
+ /**
22
+ * Watches a spawned server's output for the ready marker. Separated from the
23
+ * spawn so the three outcomes that matter — ready, the child died, nothing
24
+ * happened for long enough — are testable without a real process.
25
+ */
26
+ export declare const createReadyBarrier: (label: string) => ReadyBarrier;
27
+ export interface SpawnedServer {
28
+ /** Kills the whole process group, so the server's own children go with it. */
29
+ stop: () => void;
30
+ /** Resolves on the ready marker; rejects if the server dies or never reports. */
31
+ waitUntilReady: (options?: {
32
+ timeoutMs?: number;
33
+ }) => Promise<void>;
34
+ }
35
+ export interface SpawnDevServerOptions {
36
+ cwd: string;
37
+ port: number;
38
+ hostname?: string;
39
+ coverage?: boolean;
40
+ /** Passed through as `--test`, which sets PIKKU_TEST_RUN for isTestRun(). */
41
+ test?: boolean;
42
+ env?: NodeJS.ProcessEnv;
43
+ /** Where the server's own output goes. Defaults to this process's streams. */
44
+ onOutput?: (text: string) => void;
45
+ }
46
+ /**
47
+ * Start `pikku dev` as its own process group and stream its output under a
48
+ * `[server]` prefix. Does not wait — call `waitUntilReady`.
49
+ */
50
+ export declare const spawnDevServer: (options: SpawnDevServerOptions) => Promise<SpawnedServer>;
@@ -0,0 +1,112 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { connect } from 'node:net';
3
+ import { SERVER_READY_MARKER } from './server-ready.js';
4
+ /**
5
+ * Rejects if anything is already listening on `port`.
6
+ *
7
+ * A readiness check cannot tell "my server" from "someone else's": both answer
8
+ * on the same address once spawned, so a stale server left on the target port
9
+ * silently absorbs a whole run and reports failures that belong to code nobody
10
+ * is looking at. A TCP-level check before spawning is the only point at which
11
+ * the two are distinguishable.
12
+ */
13
+ export const assertPortFree = (port, host = '127.0.0.1', timeoutMs = 1000) => new Promise((resolve, reject) => {
14
+ const socket = connect({ port, host });
15
+ const settle = (error) => {
16
+ socket.removeAllListeners();
17
+ socket.destroy();
18
+ error ? reject(error) : resolve();
19
+ };
20
+ socket.setTimeout(timeoutMs);
21
+ socket.on('connect', () => settle(new Error(`Port ${port} is already in use — something is already listening on ${host}:${port}. ` +
22
+ `Stop it, or point this environment's apiUrl at a free port.`)));
23
+ // Nothing listening, or unreachable: either way this process is free to bind.
24
+ socket.on('error', () => settle());
25
+ socket.on('timeout', () => settle());
26
+ });
27
+ /**
28
+ * Watches a spawned server's output for the ready marker. Separated from the
29
+ * spawn so the three outcomes that matter — ready, the child died, nothing
30
+ * happened for long enough — are testable without a real process.
31
+ */
32
+ export const createReadyBarrier = (label) => {
33
+ let ready = false;
34
+ let exit;
35
+ // Stdout arrives in arbitrary chunks, so the marker can straddle two of them.
36
+ // Keep just enough of the tail to rejoin a split marker.
37
+ let tail = '';
38
+ return {
39
+ observe: (text) => {
40
+ if (ready)
41
+ return;
42
+ const window = tail + text;
43
+ if (window.includes(SERVER_READY_MARKER)) {
44
+ ready = true;
45
+ tail = '';
46
+ return;
47
+ }
48
+ tail = window.slice(-SERVER_READY_MARKER.length);
49
+ },
50
+ markExited: (code) => {
51
+ exit = { code };
52
+ },
53
+ wait: async ({ timeoutMs = 120_000, pollMs = 100 } = {}) => {
54
+ const deadline = Date.now() + timeoutMs;
55
+ while (Date.now() < deadline) {
56
+ if (ready)
57
+ return;
58
+ if (exit) {
59
+ throw new Error(`${label} exited with code ${exit.code} before it was ready`);
60
+ }
61
+ await new Promise((r) => setTimeout(r, pollMs));
62
+ }
63
+ throw new Error(`${label} did not report ready within ${timeoutMs / 1000} seconds`);
64
+ },
65
+ };
66
+ };
67
+ /**
68
+ * Start `pikku dev` as its own process group and stream its output under a
69
+ * `[server]` prefix. Does not wait — call `waitUntilReady`.
70
+ */
71
+ export const spawnDevServer = async (options) => {
72
+ const { cwd, port, hostname = '127.0.0.1', coverage, test = true, env, onOutput = (text) => process.stdout.write(text), } = options;
73
+ await assertPortFree(port, hostname);
74
+ const args = ['pikku', 'dev', '--port', String(port)];
75
+ if (coverage)
76
+ args.push('--coverage');
77
+ if (test)
78
+ args.push('--test');
79
+ const child = spawn('npx', args, {
80
+ cwd,
81
+ env: { ...process.env, ...env },
82
+ stdio: 'pipe',
83
+ detached: true,
84
+ });
85
+ const barrier = createReadyBarrier('Server');
86
+ const forward = (chunk) => {
87
+ const text = chunk.toString();
88
+ barrier.observe(text);
89
+ onOutput(text
90
+ .split('\n')
91
+ .map((line) => (line ? `[server] ${line}` : line))
92
+ .join('\n'));
93
+ };
94
+ child.stdout?.on('data', forward);
95
+ child.stderr?.on('data', forward);
96
+ child.on('exit', (code) => barrier.markExited(code));
97
+ return {
98
+ waitUntilReady: barrier.wait,
99
+ stop: () => {
100
+ child.stdout?.destroy();
101
+ child.stderr?.destroy();
102
+ if (child.pid) {
103
+ try {
104
+ process.kill(-child.pid, 'SIGTERM');
105
+ }
106
+ catch {
107
+ // Process group may already be gone
108
+ }
109
+ }
110
+ },
111
+ };
112
+ };
@@ -149,7 +149,14 @@ export const createSingletonServices = async (config) => {
149
149
  let unfilteredStateIsSetupOnly = false;
150
150
  let inspectedTsGeneration;
151
151
  let inspectorInvalidated = false;
152
- const getInspectorState = async (refresh = false, setupOnly = false, bootstrapMode = false) => {
152
+ /**
153
+ * `unfiltered` is for commands that RUN the project rather than generate from
154
+ * it. The CLI filters narrow what gets written out, which is meaningless to a
155
+ * runner and actively wrong for one: `--tags` on `pikku scenario run` selects
156
+ * which scenarios to run, and if it also narrowed the state the runner would
157
+ * lose the very step functions it is about to call.
158
+ */
159
+ const getInspectorState = async (refresh = false, setupOnly = false, bootstrapMode = false, unfiltered = false) => {
153
160
  // In bootstrap mode, return a minimal "zero state" with core types
154
161
  // This allows bootstrap to run immediately without inspecting the codebase
155
162
  if (bootstrapMode) {
@@ -240,6 +247,7 @@ export const createSingletonServices = async (config) => {
240
247
  rootDir,
241
248
  isAddon: !!config.addon,
242
249
  oldProgram,
250
+ tsconfig: config.tsconfig,
243
251
  types: {
244
252
  configFileType: config.configFile,
245
253
  userSessionType: config.userSessionType,
@@ -287,6 +295,9 @@ export const createSingletonServices = async (config) => {
287
295
  }
288
296
  }
289
297
  }
298
+ if (unfiltered) {
299
+ return unfilteredState;
300
+ }
290
301
  // Apply filters as a post-processing step
291
302
  const filteredState = filterInspectorState(unfilteredState, filters, logger);
292
303
  return filteredState;
@@ -19,7 +19,7 @@ const UNWRAP = {
19
19
  email: 'templates',
20
20
  agent: 'agentsMeta',
21
21
  };
22
- // Workflows (incl. userflows/scenarios) are written one file per workflow under
22
+ // Workflows (incl. scenarios) are written one file per workflow under
23
23
  // this dir; the file basename (sans `.gen.json`) is the id, `-verbose` excluded.
24
24
  const WORKFLOW_META_DIR = 'workflow/meta';
25
25
  function readJson(path) {
@@ -40,7 +40,10 @@ function readSingle(outDir, category, relPath) {
40
40
  const unwrapKey = UNWRAP[category];
41
41
  if (unwrapKey) {
42
42
  const inner = raw[unwrapKey];
43
- raw = inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : {};
43
+ raw =
44
+ inner && typeof inner === 'object' && !Array.isArray(inner)
45
+ ? inner
46
+ : {};
44
47
  }
45
48
  if (category !== 'http')
46
49
  return raw;
@@ -341,6 +341,9 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
341
341
  if (!result.workflowMapDeclarationFile) {
342
342
  result.workflowMapDeclarationFile = join(workflowDir, 'pikku-workflow-map.gen.d.ts');
343
343
  }
344
+ if (!result.scenarioStepMapDeclarationFile) {
345
+ result.scenarioStepMapDeclarationFile = join(workflowDir, 'pikku-scenario-step-map.gen.d.ts');
346
+ }
344
347
  if (!result.workflowTypesFile) {
345
348
  result.workflowTypesFile = join(workflowDir, 'pikku-workflow-types.gen.ts');
346
349
  }
@@ -351,6 +354,29 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
351
354
  if (!result.scenarioActorsFile) {
352
355
  result.scenarioActorsFile = join(workflowDir, 'pikku-scenario-actors.gen.ts');
353
356
  }
357
+ // Scenarios
358
+ const scenarioDir = join(result.outDir, 'scenarios');
359
+ if (!result.scenarioStepsFile) {
360
+ result.scenarioStepsFile = join(scenarioDir, 'pikku-scenario-functions.gen.ts');
361
+ }
362
+ if (!result.scenarioStepsMetaFile) {
363
+ result.scenarioStepsMetaFile = join(scenarioDir, 'pikku-scenario-functions-meta.gen.ts');
364
+ }
365
+ if (!result.scenarioStepsMetaJsonFile) {
366
+ result.scenarioStepsMetaJsonFile = join(scenarioDir, 'pikku-scenario-functions-meta.gen.json');
367
+ }
368
+ if (!result.scenarioWiringsFile) {
369
+ result.scenarioWiringsFile = join(scenarioDir, 'pikku-scenario-wirings.gen.ts');
370
+ }
371
+ if (!result.scenarioWiringsMetaFile) {
372
+ result.scenarioWiringsMetaFile = join(scenarioDir, 'pikku-scenario-wirings-meta.gen.ts');
373
+ }
374
+ if (!result.scenarioMetaDir) {
375
+ result.scenarioMetaDir = join(scenarioDir, 'meta');
376
+ }
377
+ if (!result.scenarioBootstrapFile) {
378
+ result.scenarioBootstrapFile = join(result.outDir, 'pikku-bootstrap-scenarios.gen.ts');
379
+ }
354
380
  // Services
355
381
  if (!result.servicesFile) {
356
382
  result.servicesFile = join(result.outDir, 'pikku-services.gen.ts');
@@ -0,0 +1,23 @@
1
+ import type { PikkuCLIConfig } from '../../types/config.js';
2
+ type ScenariosConfig = NonNullable<PikkuCLIConfig['scenarios']>;
3
+ type ActorsConfig = NonNullable<ScenariosConfig['actors']>;
4
+ type ActorConfig = ActorsConfig[string];
5
+ /**
6
+ * The actor registry a run actually sees: every declared actor, plus one
7
+ * materialised actor for each persona nobody declared an actor for.
8
+ *
9
+ * A persona is the KIND of person (its `description`); an actor is one BODY
10
+ * that signs in, with an email of its own. Most apps want exactly one body per
11
+ * kind, so declaring the persona is enough — this fills in the actor. You only
12
+ * write an actor by hand when a scenario needs TWO of the same kind, which is
13
+ * what tenant-isolation and peer-sharing scenarios are made of.
14
+ *
15
+ * Every consumer must resolve through here rather than reading
16
+ * `config.scenarios.actors` directly: codegen emits `scenarioActorConfigs` from
17
+ * it (and `ScenarioActorName` is a `keyof` over that literal, so an actor missing
18
+ * at codegen time can never be referenced in a typed step), while `scenario run`
19
+ * builds the HTTP and Playwright actor maps from it. Those were already three
20
+ * separate reads of the same field before personas existed.
21
+ */
22
+ export declare const resolveScenarioActors: (scenarios: ScenariosConfig | undefined) => ActorsConfig;
23
+ export type { ActorConfig as ResolvedScenarioActorConfig };