@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
@@ -1,22 +1,36 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.89
2
+ * This file was generated by @pikku/cli@0.12.91
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
6
6
  import { type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult } from '@pikku/core/workflow';
7
- import type { PikkuWorkflowWire, PikkuScenarioWire, WorkflowStepOptions } from '@pikku/core/workflow';
7
+ import type { PikkuWorkflowWire, PikkuScenarioWire, WorkflowStepOptions, ScenarioStepOptions } from '@pikku/core/workflow';
8
8
  export { WorkflowCancelledException };
9
9
  import type { PikkuFunctionSessionless, PikkuFunctionConfig } from '../function/pikku-function-types.gen.js';
10
10
  import type { FlattenedRPCMap } from '../rpc/pikku-rpc-wirings-map.internal.gen.d.js';
11
11
  import type { FlattenedWorkflowMap } from './pikku-workflow-map.gen.d.js';
12
12
  import type { AgentMap as FlattenedAgentMap } from '../agent/pikku-agent-map.gen.d.js';
13
+ import type { FlattenedScenarioStepMap } from './pikku-scenario-step-map.gen.d.js';
14
+ import type { TypedScenarioActors } from './pikku-scenario-actors.gen.js';
15
+ export type { TypedScenarioActors };
13
16
  export { template };
14
17
  export interface TypedWorkflow extends PikkuWorkflowWire {
15
18
  do<K extends keyof FlattenedRPCMap>(stepName: string, rpcName: K, data: FlattenedRPCMap[K]['input'], options?: WorkflowStepOptions): Promise<FlattenedRPCMap[K]['output']>;
16
19
  do<K extends keyof FlattenedWorkflowMap>(stepName: string, workflowName: K, data: FlattenedWorkflowMap[K]['input'], options?: WorkflowStepOptions): Promise<FlattenedWorkflowMap[K]['output']>;
17
20
  do<T>(stepName: string, fn: () => T | Promise<T>, options?: WorkflowStepOptions): Promise<T>;
18
21
  }
19
- export type TypedScenario = TypedWorkflow & Omit<PikkuScenarioWire, keyof PikkuWorkflowWire>;
22
+ /**
23
+ * The typed half of a scenario wire: `step` and its `given`/`when`/`then`
24
+ * sugar, narrowed to the names declared by `pikkuScenarioStep` in this project.
25
+ * The phase only changes the prose the reporter renders.
26
+ */
27
+ export interface TypedScenarioSteps {
28
+ step<K extends keyof FlattenedScenarioStepMap>(stepName: string, stepFunc: K, data?: FlattenedScenarioStepMap[K]['input'], options?: ScenarioStepOptions): Promise<FlattenedScenarioStepMap[K]['output']>;
29
+ given<K extends keyof FlattenedScenarioStepMap>(stepName: string, stepFunc: K, data?: FlattenedScenarioStepMap[K]['input'], options?: ScenarioStepOptions): Promise<FlattenedScenarioStepMap[K]['output']>;
30
+ when<K extends keyof FlattenedScenarioStepMap>(stepName: string, stepFunc: K, data?: FlattenedScenarioStepMap[K]['input'], options?: ScenarioStepOptions): Promise<FlattenedScenarioStepMap[K]['output']>;
31
+ then<K extends keyof FlattenedScenarioStepMap>(stepName: string, stepFunc: K, data?: FlattenedScenarioStepMap[K]['input'], options?: ScenarioStepOptions): Promise<FlattenedScenarioStepMap[K]['output']>;
32
+ }
33
+ export type TypedScenario = TypedWorkflow & Omit<PikkuScenarioWire, keyof PikkuWorkflowWire | keyof TypedScenarioSteps> & TypedScenarioSteps;
20
34
  import type { StandardSchemaV1 } from '@standard-schema/spec';
21
35
  import type { InferSchemaOutput, PikkuPermission, PikkuMiddleware, NodeConfig, PikkuApprovalDescription } from '../function/pikku-function-types.gen.js';
22
36
  import type { ScopeId } from '../scopes/pikku-scopes.gen.js';
@@ -58,7 +72,39 @@ export declare function pikkuWorkflowComplexFunc<InputSchema extends StandardSch
58
72
  export declare function pikkuWorkflowComplexFunc<In, Out = unknown>(func: PikkuFunctionWorkflow<In, Out> | PikkuFunctionConfig<In, Out, 'workflow', PikkuFunctionWorkflow<In, Out>>): PikkuFunctionConfig<In, Out, 'workflow'>;
59
73
  export type PikkuScenarioConfigWithSchema<InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined> = Omit<PikkuWorkflowConfigWithSchema<InputSchema, OutputSchema>, 'func' | 'auth' | 'scopes' | 'permissions'> & {
60
74
  func: PikkuFunctionScenario<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown>;
75
+ /**
76
+ * Runs before the scenario body. Same signature as `func`, but its return
77
+ * value is discarded — a hook is setup, not a step, so it is never recorded
78
+ * on the ladder. Throwing skips the body and fails the run; `after` still
79
+ * runs.
80
+ */
81
+ before?: PikkuScenarioHook<InputSchema>;
82
+ /**
83
+ * Always runs after the scenario body, in a `finally`, whether it passed or
84
+ * failed. Throwing fails a run that would otherwise have passed; on an
85
+ * already-failed run it attaches as the `cause` and never replaces the
86
+ * original error.
87
+ */
88
+ after?: PikkuScenarioHook<InputSchema>;
89
+ /**
90
+ * Why this scenario is held out of a default run, stated where the scenario
91
+ * is. The scenario still appears in the plan and is reported as skipped
92
+ * rather than quietly omitted; naming it directly with `--flows` runs it
93
+ * anyway.
94
+ */
95
+ skip?: string;
61
96
  };
97
+ /**
98
+ * A scenario lifecycle hook: the scenario's own `(services, data, wire)`
99
+ * signature with its result discarded.
100
+ */
101
+ export type PikkuScenarioHook<InputSchema extends StandardSchemaV1 | undefined = undefined> = PikkuFunctionScenario<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, void>;
102
+ /**
103
+ * Declares a scenario hook. Returns the function verbatim — a hook is never
104
+ * registered, so this exists purely to give an inline hook a call site to be
105
+ * contextually typed from, the way every other pikku primitive is.
106
+ */
107
+ export declare function pikkuScenarioHook<In = unknown>(hook: PikkuFunctionScenario<In, void>): PikkuFunctionScenario<In, void>;
62
108
  /**
63
109
  * A scenario: a complex workflow that drives the app the way users do.
64
110
  * Steps run as actors over the REAL transport — `scenario.do(step, rpc,
@@ -67,6 +113,109 @@ export type PikkuScenarioConfigWithSchema<InputSchema extends StandardSchemaV1 |
67
113
  */
68
114
  export declare function pikkuScenario<InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined>(config: PikkuScenarioConfigWithSchema<InputSchema, OutputSchema>): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'scenario' | 'actors', PikkuFunctionScenario<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown>, InputSchema, OutputSchema>;
69
115
  export declare function pikkuScenario<In, Out = unknown>(func: PikkuFunctionScenario<In, Out> | PikkuFunctionConfig<In, Out, 'scenario' | 'actors', PikkuFunctionScenario<In, Out>>): PikkuFunctionConfig<In, Out, 'scenario' | 'actors'>;
116
+ /**
117
+ * A scenario as a feature references it. Any `pikkuScenario` export is
118
+ * assignable; `In` is recovered from it so the paired form's `data` is
119
+ * checked against that scenario's own input.
120
+ */
121
+ export type PikkuScenarioRef<In = any, Out = any> = PikkuFunctionConfig<In, Out, 'scenario' | 'actors', PikkuFunctionScenario<In, Out>, any, any>;
122
+ /**
123
+ * One entry in a feature's `scenarios` list, validated against itself: a bare
124
+ * scenario, or a scenario paired with the input to run it with. The paired form
125
+ * is gherkin's `Examples:` written as an ordinary loop.
126
+ */
127
+ export type PikkuFeatureEntry<Entry> = Entry extends {
128
+ scenario: PikkuScenarioRef<infer In>;
129
+ } ? {
130
+ scenario: PikkuScenarioRef<In>;
131
+ data: In;
132
+ } : Entry extends PikkuScenarioRef ? Entry : never;
133
+ export type PikkuFeatureConfig<Scenarios extends readonly unknown[]> = {
134
+ /** Human-readable name. The export identifier is the id. */
135
+ name: string;
136
+ description?: string;
137
+ tags?: string[];
138
+ scenarios: {
139
+ [K in keyof Scenarios]: PikkuFeatureEntry<Scenarios[K]>;
140
+ };
141
+ /**
142
+ * Runs ONCE before the whole group — not before each scenario. Per-scenario
143
+ * setup is the scenario's own `before`; gherkin's `Background:` is
144
+ * deliberately not expressible here.
145
+ */
146
+ before?: PikkuScenarioHook;
147
+ after?: PikkuScenarioHook;
148
+ };
149
+ /**
150
+ * A feature: an ordered group of scenarios, mirroring gherkin's Feature ↔
151
+ * Scenario structure. Scenarios are referenced by imported identifier, so a
152
+ * renamed or deleted scenario is a compile error rather than a silent skip.
153
+ *
154
+ * A scenario does not have to belong to a feature — a void-input scenario
155
+ * still runs standalone.
156
+ *
157
+ * ```ts
158
+ * export const credentialFeature = pikkuFeature({
159
+ * name: 'Credential API',
160
+ * tags: ['credential'],
161
+ * before: startsMockOAuthServer,
162
+ * after: stopsMockOAuthServer,
163
+ * scenarios: [
164
+ * credentialLazyLoadScenario,
165
+ * ...['stripe', 'google'].map((name) => ({
166
+ * scenario: credentialRoundTripScenario,
167
+ * data: { name },
168
+ * })),
169
+ * ],
170
+ * })
171
+ * ```
172
+ */
173
+ export declare function pikkuFeature<const Scenarios extends readonly unknown[]>(config: PikkuFeatureConfig<Scenarios>): PikkuFeatureConfig<Scenarios>;
174
+ export type PikkuFunctionScenarioStep<In = unknown, Out = never, B extends boolean = false> = PikkuFunctionSessionless<In, Out, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep'>;
175
+ export type PikkuScenarioStepConfigWithSchema<InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined, B extends boolean = false> = {
176
+ /** Registered name — this is the string `scenario.step()` references. */
177
+ name: string;
178
+ /**
179
+ * What this step does, for the console and for whoever reads the source. It
180
+ * is also the fallback prose when no `template` is declared, in which case a
181
+ * reporter renders "Given the shopper buys an apple". Defaults to the call
182
+ * site's step name.
183
+ */
184
+ description?: string;
185
+ /**
186
+ * The prose a reporter renders for this step, with `{placeholders}` filled
187
+ * from the input the step was called with — `'sees {packageName}'` reports as
188
+ * "Then the admin sees @pikku/addon-todos". Every input field should appear,
189
+ * so the report names the values under test rather than repeating one
190
+ * sentence per call site.
191
+ */
192
+ template?: string;
193
+ /**
194
+ * This step drives a browser. The runner provisions one for the step's actor
195
+ * before calling it — which also makes an actor mandatory — and `wire.browser`
196
+ * is non-optional inside `func`.
197
+ */
198
+ browser?: B;
199
+ title?: string;
200
+ tags?: string[];
201
+ input?: InputSchema;
202
+ output?: OutputSchema;
203
+ errors?: Array<typeof PikkuError>;
204
+ func: PikkuFunctionScenarioStep<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, B>;
205
+ };
206
+ export type PikkuScenarioStepConfig<In, Out, B extends boolean = false> = Omit<PikkuScenarioStepConfigWithSchema<undefined, undefined, B>, 'func' | 'input' | 'output'> & {
207
+ func: PikkuFunctionScenarioStep<In, Out, B>;
208
+ };
209
+ /**
210
+ * A named, reusable scenario step. Unlike `scenario.do`, which can only name an
211
+ * RPC, a step's body is an ordinary pikku function — so it may drive a browser,
212
+ * invoke RPCs as its actor, or run a workflow.
213
+ *
214
+ * Steps are deliberately NOT registered as RPCs: a browser-driving step must
215
+ * never be network-callable.
216
+ */
217
+ export declare function pikkuScenarioStep<InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined, B extends boolean = false>(config: PikkuScenarioStepConfigWithSchema<InputSchema, OutputSchema, B>): 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>;
218
+ export declare function pikkuScenarioStep<In, Out = unknown, B extends boolean = false>(config: PikkuScenarioStepConfig<In, Out, B>): PikkuFunctionConfig<In, Out, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep'>;
70
219
  type TypedRef<T> = {
71
220
  $ref: string;
72
221
  path?: string;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.89
2
+ * This file was generated by @pikku/cli@0.12.91
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
@@ -12,9 +12,47 @@ export function pikkuWorkflowFunc(func) {
12
12
  export function pikkuWorkflowComplexFunc(func) {
13
13
  return typeof func === 'function' ? { func } : func;
14
14
  }
15
+ /**
16
+ * Declares a scenario hook. Returns the function verbatim — a hook is never
17
+ * registered, so this exists purely to give an inline hook a call site to be
18
+ * contextually typed from, the way every other pikku primitive is.
19
+ */
20
+ export function pikkuScenarioHook(hook) {
21
+ return hook;
22
+ }
15
23
  export function pikkuScenario(func) {
16
24
  return typeof func === 'function' ? { func } : func;
17
25
  }
26
+ /**
27
+ * A feature: an ordered group of scenarios, mirroring gherkin's Feature ↔
28
+ * Scenario structure. Scenarios are referenced by imported identifier, so a
29
+ * renamed or deleted scenario is a compile error rather than a silent skip.
30
+ *
31
+ * A scenario does not have to belong to a feature — a void-input scenario
32
+ * still runs standalone.
33
+ *
34
+ * ```ts
35
+ * export const credentialFeature = pikkuFeature({
36
+ * name: 'Credential API',
37
+ * tags: ['credential'],
38
+ * before: startsMockOAuthServer,
39
+ * after: stopsMockOAuthServer,
40
+ * scenarios: [
41
+ * credentialLazyLoadScenario,
42
+ * ...['stripe', 'google'].map((name) => ({
43
+ * scenario: credentialRoundTripScenario,
44
+ * data: { name },
45
+ * })),
46
+ * ],
47
+ * })
48
+ * ```
49
+ */
50
+ export function pikkuFeature(config) {
51
+ return config;
52
+ }
53
+ export function pikkuScenarioStep(config) {
54
+ return config;
55
+ }
18
56
  export function pikkuWorkflowGraph(config) {
19
57
  return corePikkuWorkflowGraph(config);
20
58
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.89
2
+ * This file was generated by @pikku/cli@0.12.91
3
3
  */
4
4
  import { pikkuState } from '@pikku/core/internal';
5
5
  import allWorkflowMeta from './meta/allWorkflow.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.89
2
+ * This file was generated by @pikku/cli@0.12.91
3
3
  */
4
4
  import { addWorkflow } from '@pikku/core/workflow';
5
5
  import './pikku-workflow-wirings-meta.gen.js';
@@ -11,8 +11,8 @@ async function checkForUpdate() {
11
11
  })
12
12
  if (!res.ok) return
13
13
  const { version: latest } = await res.json()
14
- if (latest !== '0.12.89') {
15
- process.stderr.write(`\n Update available 0.12.89 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.91') {
15
+ process.stderr.write(`\n Update available 0.12.91 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
16
16
  }
17
17
  } catch {}
18
18
  }
@@ -18,6 +18,10 @@ import { dev } from './functions/commands/dev.js';
18
18
  import { serve } from './functions/commands/serve.js';
19
19
  import { dbMigrate } from './functions/commands/db-migrate.js';
20
20
  import { dbGenerate } from './functions/commands/db-generate.js';
21
+ import { dbCodegen } from './functions/commands/db-codegen.js';
22
+ import { dbCheck } from './functions/commands/db-check.js';
23
+ import { dbBaseline } from './functions/commands/db-baseline.js';
24
+ import { dbExport } from './functions/commands/db-export.js';
21
25
  import { dbSeed } from './functions/commands/db-seed.js';
22
26
  import { dbReset } from './functions/commands/db-reset.js';
23
27
  import { dbAudit } from './functions/commands/db-audit.js';
@@ -321,6 +325,22 @@ wireCLI({
321
325
  func: dbGenerate,
322
326
  description: 'Generate SQL migrations from detected schema changes',
323
327
  }),
328
+ check: pikkuCLICommand({
329
+ func: dbCheck,
330
+ description: 'Report how the configured database differs from the schema its migrations define',
331
+ }),
332
+ baseline: pikkuCLICommand({
333
+ func: dbBaseline,
334
+ description: 'Record pending migrations as applied, for a database that already has what they describe',
335
+ }),
336
+ codegen: pikkuCLICommand({
337
+ func: dbCodegen,
338
+ description: 'Regenerate db/schema.gen.ts and db/zod.gen.ts from the migration files, without connecting to a database',
339
+ }),
340
+ export: pikkuCLICommand({
341
+ func: dbExport,
342
+ description: "Publish this package's schema so projects consuming it as an addon can create its tables",
343
+ }),
324
344
  seed: pikkuCLICommand({
325
345
  func: dbSeed,
326
346
  description: 'Apply db/seed.sql to the dev database',
@@ -377,14 +397,36 @@ wireCLI({
377
397
  description: 'Comma-separated flow names to run (default: all)',
378
398
  short: 'f',
379
399
  },
400
+ features: {
401
+ description: 'Comma-separated feature names to run (pikkuFeature exports) — the feature is the run unit, so its hooks run once around the group',
402
+ },
380
403
  tags: {
381
404
  description: 'Comma-separated tags — run flows matching any',
382
405
  short: 't',
383
406
  },
407
+ excludeTags: {
408
+ description: 'Comma-separated tags to hold back — flows matching any are not run, unless named directly with --flows',
409
+ },
384
410
  coverage: {
385
411
  description: 'Reset/snapshot server coverage per scenario (target must run with --coverage); writes coverage/scenario-coverage.json',
386
412
  default: false,
387
413
  },
414
+ browser: {
415
+ description: 'Run scenarios with browser steps. --no-browser skips them (they are reported as skipped, not failed)',
416
+ default: true,
417
+ },
418
+ spawn: {
419
+ description: "Start `pikku dev` on the environment's apiUrl for the run and stop it afterwards. Without it, the environment must already be serving",
420
+ default: false,
421
+ },
422
+ keepAlive: {
423
+ description: 'With --spawn, leave the server running after the run (dev loop)',
424
+ default: false,
425
+ },
426
+ trace: {
427
+ description: 'Keep every stack frame on a failure. Without it, only the project’s own frames are shown',
428
+ default: false,
429
+ },
388
430
  },
389
431
  }),
390
432
  list: pikkuCLICommand({
@@ -1,5 +1,7 @@
1
1
  export declare const fabricCommands: {
2
- validate: import("@pikku/core/cli").CoreCLICommandConfig<import("../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<Record<string, never>, {
2
+ validate: import("@pikku/core/cli").CoreCLICommandConfig<import("../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
3
+ skipTypecheck?: boolean | undefined;
4
+ }, {
3
5
  ok: boolean;
4
6
  root: string;
5
7
  findings: {
@@ -9,7 +11,9 @@ export declare const fabricCommands: {
9
11
  path: string;
10
12
  fixHint: string;
11
13
  }[];
12
- }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<Record<string, never>, {
14
+ }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
15
+ skipTypecheck?: boolean | undefined;
16
+ }, {
13
17
  ok: boolean;
14
18
  root: string;
15
19
  findings: {
@@ -19,7 +23,9 @@ export declare const fabricCommands: {
19
23
  path: string;
20
24
  fixHint: string;
21
25
  }[];
22
- }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").WiredServices> | import("../../.pikku/pikku-types.gen.js").PikkuFunction<Record<string, never>, {
26
+ }, "rpc" | "session", import("../../.pikku/pikku-types.gen.js").WiredServices> | import("../../.pikku/pikku-types.gen.js").PikkuFunction<{
27
+ skipTypecheck?: boolean | undefined;
28
+ }, {
23
29
  ok: boolean;
24
30
  root: string;
25
31
  findings: {
@@ -48,6 +48,12 @@ export const fabricCommands = defineCLICommands({
48
48
  func: FabricValidate,
49
49
  render: renderValidate,
50
50
  description: 'Check the project structure for fabric compatibility — prints all missing or misconfigured items with fix hints',
51
+ options: {
52
+ skipTypecheck: {
53
+ description: 'Skip the frontend type-check the build container runs (structural checks only)',
54
+ default: false,
55
+ },
56
+ },
51
57
  }),
52
58
  smoke: pikkuCLICommand({
53
59
  func: FabricSmoke,
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
- export declare const FabricValidateInput: z.ZodObject<{}, z.core.$strip>;
2
+ export declare const FabricValidateInput: z.ZodObject<{
3
+ skipTypecheck: z.ZodOptional<z.ZodBoolean>;
4
+ }, z.core.$strip>;
3
5
  export declare const FabricValidateOutput: z.ZodObject<{
4
6
  ok: z.ZodBoolean;
5
7
  root: z.ZodString;
@@ -15,8 +17,12 @@ export declare const FabricValidateOutput: z.ZodObject<{
15
17
  fixHint: z.ZodString;
16
18
  }, z.core.$strip>>;
17
19
  }, z.core.$strip>;
18
- export declare function runValidate(startDir?: string): Promise<z.infer<typeof FabricValidateOutput>>;
19
- export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<Record<string, never>, {
20
+ export declare function runValidate(startDir?: string, opts?: {
21
+ skipTypecheck?: boolean;
22
+ }): Promise<z.infer<typeof FabricValidateOutput>>;
23
+ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
24
+ skipTypecheck?: boolean | undefined;
25
+ }, {
20
26
  ok: boolean;
21
27
  root: string;
22
28
  findings: {
@@ -26,7 +32,9 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
26
32
  path: string;
27
33
  fixHint: string;
28
34
  }[];
29
- }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<Record<string, never>, {
35
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
36
+ skipTypecheck?: boolean | undefined;
37
+ }, {
30
38
  ok: boolean;
31
39
  root: string;
32
40
  findings: {
@@ -36,7 +44,9 @@ export declare const FabricValidate: import("../../../.pikku/pikku-types.gen.js"
36
44
  path: string;
37
45
  fixHint: string;
38
46
  }[];
39
- }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<Record<string, never>, {
47
+ }, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
48
+ skipTypecheck?: boolean | undefined;
49
+ }, {
40
50
  ok: boolean;
41
51
  root: string;
42
52
  findings: {
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path';
5
5
  import { createRequire } from 'node:module';
6
6
  import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
7
7
  import { added, changed, removed, dim } from '../lib/output.js';
8
+ import { typeCheckFrontends } from '../lib/frontend-typecheck.js';
8
9
  const FindingSchema = z.object({
9
10
  id: z.string(),
10
11
  severity: z.enum(['error', 'warn', 'info']),
@@ -12,7 +13,12 @@ const FindingSchema = z.object({
12
13
  path: z.string(),
13
14
  fixHint: z.string(),
14
15
  });
15
- export const FabricValidateInput = z.object({});
16
+ export const FabricValidateInput = z.object({
17
+ skipTypecheck: z
18
+ .boolean()
19
+ .optional()
20
+ .describe('Skip the frontend type-check stage (structural checks only, much faster)'),
21
+ });
16
22
  export const FabricValidateOutput = z.object({
17
23
  ok: z.boolean(),
18
24
  root: z.string(),
@@ -182,7 +188,7 @@ const POSTGRES_SQL_PATTERNS = [
182
188
  { re: /\bTSVECTOR\b/i, label: 'TSVECTOR' },
183
189
  { re: /\bARRAY\s*\[/i, label: 'ARRAY[…]' },
184
190
  ];
185
- export async function runValidate(startDir = process.cwd()) {
191
+ export async function runValidate(startDir = process.cwd(), opts = {}) {
186
192
  const root = await findProjectRoot(startDir);
187
193
  const findings = [];
188
194
  const e = (id, message, path, fixHint) => {
@@ -838,19 +844,47 @@ export async function runValidate(startDir = process.cwd()) {
838
844
  info('config-missing', 'packages/functions/src/config.ts not found', join(fnDir, 'src', 'config.ts'), "Create src/config.ts: export const createConfig = pikkuConfig(async () => ({ sqliteDb: '.pikku-runtime/dev.db' }))");
839
845
  }
840
846
  }
847
+ // ── declared frontends ────────────────────────────────────────────────
848
+ // pikkufabric.config.json is unvalidated JSON, so every field below is a
849
+ // claim, not a guarantee: a null entry or a non-string cwd used to throw on
850
+ // property access and take down the whole validation run — the one thing that
851
+ // was supposed to report the broken config. Shape-check the entries once here
852
+ // and let the apps/ and type-check passes consume the narrowed list.
853
+ const declaredFrontends = [];
854
+ /** every syntactically valid cwd, including ones whose directory is missing */
855
+ const declaredCwdList = [];
856
+ const rawFrontends = fabricConfig?.frontends;
857
+ if (rawFrontends !== undefined &&
858
+ (!rawFrontends || typeof rawFrontends !== 'object')) {
859
+ e('frontends-invalid', 'pikkufabric.config.json "frontends" is not an object — no frontend will be built or type-checked', fabricConfigPath, `Set "frontends" to an object keyed by slug: { "app": { "cwd": "apps/app", "kind": "ssr" } }`);
860
+ }
861
+ else if (rawFrontends) {
862
+ for (const [slug, entry] of Object.entries(rawFrontends)) {
863
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
864
+ e(`frontend-entry-invalid-${slug}`, `pikkufabric.config.json frontend "${slug}" is not an object`, fabricConfigPath, `Give "${slug}" an object value: { "cwd": "apps/${slug}", "kind": "ssr" }`);
865
+ continue;
866
+ }
867
+ const { cwd, deploy } = entry;
868
+ if (typeof cwd !== 'string' || cwd.trim() === '') {
869
+ e(`frontend-cwd-invalid-${slug}`, `pikkufabric.config.json frontend "${slug}" has no string "cwd" — the build container has nothing to build`, fabricConfigPath, `Set "cwd" to the app directory, e.g. { "${slug}": { "cwd": "apps/${slug}" } }`);
870
+ continue;
871
+ }
872
+ const rel = cwd.replace(/^\.\//, '');
873
+ declaredCwdList.push(rel);
874
+ const dir = join(root, rel);
875
+ if (!existsSync(dir)) {
876
+ // Reported here rather than inside the apps/ block below: a project
877
+ // without an apps/ directory used to validate cleanly while declaring a
878
+ // deployable frontend that does not exist.
879
+ e(`frontend-cwd-missing-${slug}`, `fabric.config.json frontend "${slug}" declares cwd "${rel}" but that directory does not exist`, dir, `Create the directory or update the cwd in fabric.config.json`);
880
+ continue;
881
+ }
882
+ declaredFrontends.push({ slug, cwd: rel, dir, deploy: deploy !== false });
883
+ }
884
+ }
841
885
  // ── apps/ vs fabric.config.json frontends ─────────────────────────────
842
886
  const appsDir = join(root, 'apps');
843
887
  if (existsSync(appsDir)) {
844
- // Check declared frontends have a real directory on disk
845
- if (fabricConfig) {
846
- const frontends = (fabricConfig.frontends ?? {});
847
- for (const [slug, fe] of Object.entries(frontends)) {
848
- const cwd = fe.cwd?.replace(/^\.\//, '');
849
- if (cwd && !existsSync(join(root, cwd))) {
850
- e(`frontend-cwd-missing-${slug}`, `fabric.config.json frontend "${slug}" declares cwd "${cwd}" but that directory does not exist`, join(root, cwd), `Create the directory or update the cwd in fabric.config.json`);
851
- }
852
- }
853
- }
854
888
  // Check each app/ subdir is declared and has correct local deps
855
889
  let appEntries = [];
856
890
  try {
@@ -861,9 +895,7 @@ export async function runValidate(startDir = process.cwd()) {
861
895
  catch {
862
896
  /* ignore */
863
897
  }
864
- const declaredCwds = fabricConfig
865
- ? new Set(Object.values((fabricConfig.frontends ?? {})).map((f) => f.cwd?.replace(/^\.\//, '') ?? ''))
866
- : null;
898
+ const declaredCwds = fabricConfig ? new Set(declaredCwdList) : null;
867
899
  for (const name of appEntries) {
868
900
  const appPath = join(appsDir, name);
869
901
  const cwd = `apps/${name}`;
@@ -1119,6 +1151,27 @@ export async function runValidate(startDir = process.cwd()) {
1119
1151
  if (!existsSync(sdkDir)) {
1120
1152
  info('functions-sdk-missing', 'packages/functions-sdk/ not found — generated RPC client and React Query hooks will not be available', sdkDir, lines('Create a workspace at `packages/functions-sdk/`.', 'Minimum structure:', '- packages/functions-sdk/package.json', '- packages/functions-sdk/src/pikku/', 'Point `pikku.config.json` clientFiles to:', '- packages/functions-sdk/src/pikku/rpc-map.gen.d.ts', '- packages/functions-sdk/src/pikku/api.gen.ts', 'This package is the home for generated client artifacts, not handwritten server code.'));
1121
1153
  }
1154
+ // ── frontend type-check (what the build container actually runs) ───────
1155
+ // Every check above is a heuristic standing in for this compile. The build
1156
+ // container type-checks each deployable frontend and aborts the deploy on a
1157
+ // non-zero exit, so running the same compile here is the difference between
1158
+ // a 20-second local failure and a burned deploy out of the daily 10.
1159
+ if (!opts.skipTypecheck) {
1160
+ const frontends = declaredFrontends
1161
+ .filter((fe) => fe.deploy)
1162
+ .map((fe) => ({ name: fe.slug, dir: fe.dir }));
1163
+ for (const result of await typeCheckFrontends(root, frontends)) {
1164
+ if (result.skipped) {
1165
+ w(`frontend-typecheck-skipped-${result.name}`, `could not type-check frontend "${result.name}" — ${result.skipped}`, result.dir, lines('The build container type-checks every deployable frontend and aborts the deploy if it fails.', 'Give the app a tsconfig.json and a "tsc" script so the same check runs locally.'));
1166
+ continue;
1167
+ }
1168
+ if (result.errors.length === 0)
1169
+ continue;
1170
+ const shown = result.errors.slice(0, 20);
1171
+ const extra = result.errors.length - shown.length;
1172
+ e(`frontend-typecheck-${result.name}`, `frontend "${result.name}" does not type-check — the build container aborts the deploy on this`, result.dir, lines(...shown, ...(extra > 0 ? [`… and ${extra} more`] : []), '', `Reproduce with: cd ${result.dir.slice(root.length + 1)} && ${result.command}`));
1173
+ }
1174
+ }
1122
1175
  const ok = !findings.some((f) => f.severity === 'error');
1123
1176
  return { ok, root, findings };
1124
1177
  }
@@ -1126,7 +1179,7 @@ export const FabricValidate = pikkuSessionlessFunc({
1126
1179
  description: 'Check the current project structure for fabric compatibility. Prints all missing or misconfigured items with fix hints so an AI agent or developer can resolve them.',
1127
1180
  input: FabricValidateInput,
1128
1181
  output: FabricValidateOutput,
1129
- func: async (_services) => runValidate(),
1182
+ func: async (_services, { skipTypecheck }) => runValidate(process.cwd(), { skipTypecheck }),
1130
1183
  });
1131
1184
  export const renderValidate = (_s, { ok, root, findings }) => {
1132
1185
  if (findings.length === 0) {
@@ -50,7 +50,17 @@ export declare function findProjectConfig(startDir?: string): Promise<{
50
50
  * placeholder to the API as if it were a real id.
51
51
  */
52
52
  export declare function isLinkedProjectId(projectId?: string | null): boolean;
53
- export declare function writeProjectConfig(cwd: string, config: ProjectConfig): Promise<string>;
53
+ /**
54
+ * Merge into the existing config rather than replacing it. `link` and `init`
55
+ * only know the projectId, so a plain write silently deleted every other key —
56
+ * including `frontends`, which is what tells the build container an app exists.
57
+ * The symptom is a deploy that succeeds having built no frontend at all, long
58
+ * after the link that caused it. Unknown keys are preserved too; nothing here
59
+ * has any business dropping config it does not understand.
60
+ */
61
+ export declare function writeProjectConfig(cwd: string, config: Partial<ProjectConfig> & {
62
+ projectId: string;
63
+ }): Promise<string>;
54
64
  export declare function readAuthFile(): Promise<AuthFile>;
55
65
  export declare function writeAuthFile(file: AuthFile): Promise<void>;
56
66
  export interface ResolvedApiContext {
@@ -31,10 +31,48 @@ export function isLinkedProjectId(projectId) {
31
31
  return false;
32
32
  return !/^__.*__$/.test(projectId);
33
33
  }
34
+ /**
35
+ * Merge into the existing config rather than replacing it. `link` and `init`
36
+ * only know the projectId, so a plain write silently deleted every other key —
37
+ * including `frontends`, which is what tells the build container an app exists.
38
+ * The symptom is a deploy that succeeds having built no frontend at all, long
39
+ * after the link that caused it. Unknown keys are preserved too; nothing here
40
+ * has any business dropping config it does not understand.
41
+ */
34
42
  export async function writeProjectConfig(cwd, config) {
35
43
  const path = join(cwd, projectConfigName);
44
+ let existing = {};
45
+ if (existsSync(path)) {
46
+ // A read that fails for any reason other than the file's contents —
47
+ // permissions, a transient I/O error — tells us nothing about what the file
48
+ // holds. Rewriting it then is the exact clobber this function exists to
49
+ // prevent, so refuse the write instead.
50
+ let raw;
51
+ try {
52
+ raw = await readFile(path, 'utf8');
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Cannot read ${projectConfigName} at ${path} (${error.message}) — refusing to overwrite it`);
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
60
+ existing = parsed;
61
+ }
62
+ else {
63
+ // Valid JSON that is not an object holds nothing we can merge, but it is
64
+ // still the user's file — say so rather than dropping it silently.
65
+ console.warn(`[fabric] ${projectConfigName} is not a JSON object — rewriting it`);
66
+ }
67
+ }
68
+ catch (error) {
69
+ // An unparseable config is not a reason to lose the link; warn and start
70
+ // fresh rather than throwing away the command the user just ran.
71
+ console.warn(`[fabric] ${projectConfigName} is not valid JSON (${error.message}) — rewriting it`);
72
+ }
73
+ }
36
74
  await mkdir(dirname(path), { recursive: true });
37
- await writeFile(path, JSON.stringify(config, null, 2) + '\n', 'utf8');
75
+ await writeFile(path, JSON.stringify({ ...existing, ...config }, null, 2) + '\n', 'utf8');
38
76
  return path;
39
77
  }
40
78
  export async function readAuthFile() {