@prisma/composer 0.1.0-dev.9 → 0.2.0-dev.1

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 (41) hide show
  1. package/dist/{app-config-Bhu93gjR-BFLsU-gF.d.mts → app-config-BPb3LCdH-CH7b8Khi.d.mts} +105 -19
  2. package/dist/assertions.d.mts +1 -1
  3. package/dist/assertions.mjs.map +1 -1
  4. package/dist/bin.mjs +81 -266
  5. package/dist/bin.mjs.map +1 -1
  6. package/dist/casts-Ci5rYYaR.mjs.map +1 -1
  7. package/dist/casts.d.mts +1 -1
  8. package/dist/config-Cqu_-Hna.d.mts +1 -0
  9. package/dist/config.d.mts +3 -3
  10. package/dist/config.mjs +2 -1
  11. package/dist/config.mjs.map +1 -1
  12. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs +45 -0
  13. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs.map +1 -0
  14. package/dist/deploy-Cqu_-Hna.d.mts +1 -0
  15. package/dist/deploy.d.mts +2 -2
  16. package/dist/deploy.mjs +12 -5
  17. package/dist/deploy.mjs.map +1 -1
  18. package/dist/dist-B0axxnBf.mjs.map +1 -1
  19. package/dist/{graph-B7NcPiOr-UazPNQTc.d.mts → graph-DUSgjmzA-DhtkTqoz.d.mts} +3 -3
  20. package/dist/{graph-types-BgT9UEdm-Cg7wPD1I.d.mts → graph-types-BcbVhojU-C3fhxjPK.d.mts} +4 -4
  21. package/dist/{index-BoUJ4fEs.d.mts → index-BFBNz7Aw.d.mts} +3 -3
  22. package/dist/index.d.mts +4 -4
  23. package/dist/{index-DYfGGlv4.d.mts → nextjs-DLyeRR7M-DwnBjryZ.d.mts} +5 -5
  24. package/dist/nextjs-control.d.mts +5 -5
  25. package/dist/nextjs-control.mjs.map +1 -1
  26. package/dist/nextjs.d.mts +2 -2
  27. package/dist/nextjs.mjs.map +1 -1
  28. package/dist/node-control.d.mts +4 -4
  29. package/dist/node-control.mjs.map +1 -1
  30. package/dist/node.d.mts +4 -4
  31. package/dist/node.mjs.map +1 -1
  32. package/dist/report.d.mts +3 -3
  33. package/dist/report.mjs.map +1 -1
  34. package/dist/service-rpc.d.mts +17 -9
  35. package/dist/service-rpc.mjs +218 -49
  36. package/dist/service-rpc.mjs.map +1 -1
  37. package/dist/testing.d.mts +2 -2
  38. package/dist/testing.mjs.map +1 -1
  39. package/package.json +11 -11
  40. package/dist/config-Bz1VQOKQ.d.mts +0 -1
  41. package/dist/deploy-Bz1VQOKQ.d.mts +0 -1
@@ -1,12 +1,75 @@
1
- import { M as ResourceNode, V as ServiceNode, d as Graph, k as ProvisionNeed, n as Config, t as BuildAdapter, x as NodeId, y as ModuleNode } from "./graph-types-BgT9UEdm-Cg7wPD1I.mjs";
2
- import "./graph-B7NcPiOr-UazPNQTc.mjs";
1
+ import { A as ProvisionNeed, H as ServiceNode, N as ResourceNode, S as NodeId, d as Graph, n as Config, t as BuildAdapter, y as ModuleNode } from "./graph-types-BcbVhojU-C3fhxjPK.mjs";
2
+ import "./graph-DUSgjmzA-DhtkTqoz.mjs";
3
3
  import * as Alchemy from "alchemy";
4
4
  import { Input, StackServices } from "alchemy";
5
5
  import * as Effect from "effect/Effect";
6
6
  import * as Layer from "effect/Layer";
7
7
  import { State } from "alchemy/State/State";
8
- //#region ../../0-framework/1-core/core/dist/app-config-Bhu93gjR.d.mts
9
- //#region src/exports/deploy.d.ts
8
+ //#region ../../0-framework/1-core/core/dist/app-config-BPb3LCdH.d.mts
9
+ //#region src/container-transport.d.ts
10
+ /**
11
+ * Carries resolved containers from the CLI process into the alchemy process
12
+ * (ADR-0037). A deploy runs as two processes: the CLI resolves each
13
+ * extension's containers, then spawns `alchemy`, which re-imports the config
14
+ * from scratch and needs those containers back — and env vars are the only
15
+ * channel between the two. So the CLI writes each instance's `serialize()`
16
+ * output into one env var per extension, and in the alchemy process
17
+ * `deserializeContainers` reads each var back through the same extension's
18
+ * descriptor. The framework owns the vars; it never reads their contents.
19
+ */
20
+ /** The key an extension resolves a container from: which app, which stage. */
21
+ interface LocateContainerInput {
22
+ /** The application name (root node's name, or `--name`). */
23
+ readonly appName: string;
24
+ /** The named stage, or `undefined` for the default (production) stage. */
25
+ readonly stage: string | undefined;
26
+ }
27
+ /**
28
+ * One resolved container. The framework sees only this interface; the
29
+ * extension that produced the instance narrows it back to its own concrete
30
+ * type wherever the framework hands it back (ADR-0037).
31
+ */
32
+ interface ContainerInstance {
33
+ readonly input: LocateContainerInput;
34
+ /** Serialize to a non-empty string for the process transport above. The format is the extension's own; only its `deserialize` reads it. */
35
+ serialize(): string;
36
+ }
37
+ /**
38
+ * The platform containers an app deploys into, as one lifecycle. `I` is
39
+ * the extension's own instance type — the same descriptor produces and
40
+ * consumes it, so the extension gets full typing internally while the
41
+ * framework stores the erased form. METHOD SYNTAX REQUIRED on all four
42
+ * members: the erased assignment into ExtensionDescriptor compiles only
43
+ * through method bivariance; property-arrow members are checked
44
+ * contravariantly and the assignment fails (same rule as
45
+ * ServiceLowering<P, S> — ADR-0033).
46
+ */
47
+ interface ContainerDescriptor<I extends ContainerInstance = ContainerInstance> {
48
+ /** Resolve the container for (appName, stage), creating anything absent. Called by `deploy`. */
49
+ ensure(input: LocateContainerInput): Promise<I>;
50
+ /** Find the container for (appName, stage); `undefined` when nothing exists. Called by `destroy` — never creates. */
51
+ locate(input: LocateContainerInput): Promise<I | undefined>;
52
+ /** Remove the container after a successful destroy, after every extension's `teardown` has run. Failure policy is the extension's. */
53
+ remove(instance: I): Promise<void>;
54
+ /** Reconstruct an instance from its own `serialize()` output — the far end of the framework's parent→child transport. */
55
+ deserialize(serialized: string): I;
56
+ }
57
+ /** '@prisma/composer-prisma-cloud' → 'PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD' */
58
+ declare function containerEnvVarName(extensionId: string): string;
59
+ /** The env entries the CLI sets on the alchemy process: `{ [containerEnvVarName(id)]: instance.serialize() }` for every resolved instance. */
60
+ declare function containerEnv(instances: ReadonlyMap<string, ContainerInstance>): Record<string, string>;
61
+ /** The slice of `PrismaAppConfig.extensions` this module needs — kept narrow so this shared-plane module never imports the control-plane `ExtensionDescriptor`/`PrismaAppConfig` types (ADR-0028's plane split). */
62
+ interface ContainerTransportExtension {
63
+ readonly id: string;
64
+ readonly container?: ContainerDescriptor;
65
+ }
66
+ /**
67
+ * The alchemy-process side: for each extension with a container descriptor
68
+ * whose var is present in `env`, call its deserialize. Absent var → no entry.
69
+ */
70
+ declare function deserializeContainers(extensions: readonly ContainerTransportExtension[], env: Readonly<Record<string, string | undefined>>): ReadonlyMap<string, ContainerInstance>;
71
+ //#endregion
72
+ //#region src/control/deploy.d.ts
10
73
  /** The Layer shape every Alchemy state store must satisfy — what `LowerOptions.state` and `PrismaAppConfig.state` both traffic in. */
11
74
  type AlchemyStateLayer = Layer.Layer<State, never, StackServices>;
12
75
  /**
@@ -92,6 +155,13 @@ interface LowerContext {
92
155
  * it with its own type guard.
93
156
  */
94
157
  readonly application: unknown;
158
+ /**
159
+ * The owning extension's resolved container, deserialized from the
160
+ * framework transport; core never reads it — the extension narrows it
161
+ * with its own type guard. `undefined` when the extension declares no
162
+ * container descriptor.
163
+ */
164
+ readonly container: ContainerInstance | undefined;
95
165
  /** Already-lowered deps (topo order). */
96
166
  readonly lowered: ReadonlyMap<NodeId, Outputs>;
97
167
  /** Every provisioned param value minted this lowering, keyed by edge id (ADR-0031). */
@@ -219,10 +289,12 @@ declare function joinDeployment(graph: Graph, entries: readonly {
219
289
  }[]): readonly DeployedNode[];
220
290
  /**
221
291
  * The state-layer precedence a deploy resolves to: an explicit opts.state
222
- * always wins; failing that, the config's own (required) state. A pure
223
- * function so the precedence is testable without booting Alchemy.
292
+ * always wins; failing that, the config's own (required) state descriptor,
293
+ * created with its owning extension's resolved container (`undefined` when
294
+ * that extension declared none). A pure function so the precedence is
295
+ * testable without booting Alchemy.
224
296
  */
225
- declare function resolveStateLayer(opts: LowerOptions, config: PrismaAppConfig): AlchemyStateLayer;
297
+ declare function resolveStateLayer(opts: LowerOptions, config: PrismaAppConfig, containers: ReadonlyMap<string, ContainerInstance>): AlchemyStateLayer;
226
298
  /**
227
299
  * All configured extensions' providers merged, config array order — an
228
300
  * extension without `providers` is skipped; no used-extensions-only
@@ -241,7 +313,7 @@ declare function lowering(root: ModuleNode, config: PrismaAppConfig, opts: Lower
241
313
  */
242
314
  declare function lower(root: ModuleNode, config: PrismaAppConfig, opts: LowerOptions): Effect.Effect<Alchemy.CompiledStack<undefined, any>, import("effect/Config").ConfigError, never>;
243
315
  //#endregion
244
- //#region src/exports/app-config.d.ts
316
+ //#region src/control/app-config.d.ts
245
317
  /**
246
318
  * One extension's control-plane registry: everything the deploy pipeline may
247
319
  * look up for a node whose `extension` field names this package. `nodes` is
@@ -275,24 +347,38 @@ interface ExtensionDescriptor {
275
347
  * fail the command handles that itself. Async: it talks to the platform.
276
348
  */
277
349
  readonly teardown?: (input: TeardownInput) => Promise<void>;
350
+ /**
351
+ * The extension's container lifecycle, when its platform has containers
352
+ * (ADR-0038). The CLI resolves containers after assembly and before any
353
+ * stack file or Alchemy run (deploy ensures, destroy locates); the
354
+ * resolved instance reaches the alchemy process through the env transport
355
+ * in container-transport.ts.
356
+ */
357
+ readonly container?: ContainerDescriptor;
358
+ }
359
+ /**
360
+ * The deploy's one state store. It names its owning extension so core knows
361
+ * whose resolved container to pass into `create` (ADR-0038).
362
+ */
363
+ interface StateDescriptor {
364
+ /** The owning extension's id — matched against `ExtensionDescriptor.id`. */
365
+ readonly extension: string;
366
+ /** Build the state layer. `container` is the owning extension's resolved instance; `undefined` when it declared no container descriptor. */
367
+ create(container: ContainerInstance | undefined): AlchemyStateLayer;
278
368
  }
279
369
  /** The resolved deploy context handed to an extension's `preflight` hook. */
280
370
  interface PreflightInput {
281
371
  /** The loaded application graph — the manifest of prerequisites is read from it (`provisionManifest`). */
282
372
  readonly graph: Graph;
283
- /** The resolved Prisma Cloud Project id. */
284
- readonly projectId: string;
285
- /** The resolved Branch id for a named stage; `undefined` for the default (production) stage. */
286
- readonly branchId: string | undefined;
373
+ /** The calling extension's own resolved container; `undefined` when it declares no container descriptor. Narrow with the extension's guard. */
374
+ readonly container: ContainerInstance | undefined;
287
375
  /** The stage name (`--stage`), or `undefined` for the default stage — for diagnostics/scope. */
288
376
  readonly stage: string | undefined;
289
377
  }
290
378
  /** The resolved destroy context handed to an extension's `teardown` hook. */
291
379
  interface TeardownInput {
292
- /** The resolved Prisma Cloud Project id. */
293
- readonly projectId: string;
294
- /** The resolved Branch id for a named stage; `undefined` for the default (production) stage. */
295
- readonly branchId: string | undefined;
380
+ /** The calling extension's own resolved container; `undefined` when it declares no container descriptor. Narrow with the extension's guard. */
381
+ readonly container: ContainerInstance | undefined;
296
382
  /** The stage name (`--stage`), or `undefined` for the default stage — for diagnostics/scope. */
297
383
  readonly stage: string | undefined;
298
384
  }
@@ -316,10 +402,10 @@ type NodeDescriptor = ({
316
402
  */
317
403
  interface PrismaAppConfig {
318
404
  readonly extensions: ExtensionDescriptor[];
319
- readonly state: () => AlchemyStateLayer;
405
+ readonly state: StateDescriptor;
320
406
  }
321
407
  /** Typed identity — exists so `prisma-composer.config.ts` gets checked against PrismaAppConfig where it is written. */
322
408
  declare function defineConfig(config: PrismaAppConfig): PrismaAppConfig;
323
409
  //#endregion
324
- export { resolveStateLayer as A, TeardownInput as C, lower as D, joinDeployment as E, lowering as O, ServiceLowering as S, defineConfig as T, PackageInput as _, Bundle as a, ProvisionEdge as b, DeploymentResult as c, LowerError as d, LowerOptions as f, Outputs as g, NodeDescriptor as h, AssembleInput as i, mergedProviders as k, ExtensionDescriptor as l, Lowering as m, ApplicationDescriptor as n, DeployedEntity as o, LoweredResult as p, Artifact as r, DeployedNode as s, AlchemyStateLayer as t, LowerContext as u, PreflightInput as v, buildConfig as w, ProvisionerDescriptor as x, PrismaAppConfig as y };
325
- //# sourceMappingURL=app-config-Bhu93gjR-BFLsU-gF.d.mts.map
410
+ export { containerEnvVarName as A, ProvisionEdge as C, TeardownInput as D, StateDescriptor as E, lowering as F, mergedProviders as I, resolveStateLayer as L, deserializeContainers as M, joinDeployment as N, buildConfig as O, lower as P, PrismaAppConfig as S, ServiceLowering as T, Lowering as _, Bundle as a, PackageInput as b, DeployedEntity as c, ExtensionDescriptor as d, LocateContainerInput as f, LoweredResult as g, LowerOptions as h, AssembleInput as i, defineConfig as j, containerEnv as k, DeployedNode as l, LowerError as m, ApplicationDescriptor as n, ContainerDescriptor as o, LowerContext as p, Artifact as r, ContainerInstance as s, AlchemyStateLayer as t, DeploymentResult as u, NodeDescriptor as v, ProvisionerDescriptor as w, PreflightInput as x, Outputs as y };
411
+ //# sourceMappingURL=app-config-BPb3LCdH-CH7b8Khi.d.mts.map
@@ -1,5 +1,5 @@
1
1
  //#region ../../0-framework/0-foundation/foundation/dist/assertions.d.mts
2
- //#region src/exports/assertions.d.ts
2
+ //#region src/assertions.d.ts
3
3
  /**
4
4
  * Asserts that a value is defined (not null or undefined).
5
5
  * Use for invariants where the value should always exist at runtime.
@@ -1 +1 @@
1
- {"version":3,"file":"assertions.mjs","names":[],"sources":["../../../0-framework/0-foundation/foundation/dist/assertions.mjs"],"sourcesContent":["//#region src/exports/assertions.ts\n/**\n* Asserts that a value is defined (not null or undefined).\n* Use for invariants where the value should always exist at runtime.\n*\n* @throws Error if value is null or undefined\n*\n* @example\n* ```typescript\n* const port = config.ports[name];\n* assertDefined(port, `Port \"${name}\" not found`);\n* // port is now narrowed to non-nullable\n* ```\n*/\nfunction assertDefined(value, message) {\n\tif (value === null || value === void 0) throw new Error(message);\n}\n/**\n* Asserts that a condition is true.\n* Use for invariants that should always hold at runtime.\n*\n* @throws Error if condition is false\n*\n* @example\n* ```typescript\n* invariant(edges.length > 0, 'A wired module must have at least one connection');\n* ```\n*/\nfunction invariant(condition, message) {\n\tif (!condition) throw new Error(message);\n}\n//#endregion\nexport { assertDefined, invariant };\n\n//# sourceMappingURL=assertions.mjs.map"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,cAAc,OAAO,SAAS;CACtC,IAAI,UAAU,QAAQ,UAAU,KAAK,GAAG,MAAM,IAAI,MAAM,OAAO;AAChE;;;;;;;;;;;;AAYA,SAAS,UAAU,WAAW,SAAS;CACtC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,OAAO;AACxC"}
1
+ {"version":3,"file":"assertions.mjs","names":[],"sources":["../../../0-framework/0-foundation/foundation/dist/assertions.mjs"],"sourcesContent":["//#region src/assertions.ts\n/**\n* Asserts that a value is defined (not null or undefined).\n* Use for invariants where the value should always exist at runtime.\n*\n* @throws Error if value is null or undefined\n*\n* @example\n* ```typescript\n* const port = config.ports[name];\n* assertDefined(port, `Port \"${name}\" not found`);\n* // port is now narrowed to non-nullable\n* ```\n*/\nfunction assertDefined(value, message) {\n\tif (value === null || value === void 0) throw new Error(message);\n}\n/**\n* Asserts that a condition is true.\n* Use for invariants that should always hold at runtime.\n*\n* @throws Error if condition is false\n*\n* @example\n* ```typescript\n* invariant(edges.length > 0, 'A wired module must have at least one connection');\n* ```\n*/\nfunction invariant(condition, message) {\n\tif (!condition) throw new Error(message);\n}\n//#endregion\nexport { assertDefined, invariant };\n\n//# sourceMappingURL=assertions.mjs.map"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,cAAc,OAAO,SAAS;CACtC,IAAI,UAAU,QAAQ,UAAU,KAAK,GAAG,MAAM,IAAI,MAAM,OAAO;AAChE;;;;;;;;;;;;AAYA,SAAS,UAAU,WAAW,SAAS;CACtC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,OAAO;AACxC"}
package/dist/bin.mjs CHANGED
@@ -2,19 +2,9 @@
2
2
  import { Cli, Command, Option, UsageError } from "clipanion";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
- import { spawnSync } from "node:child_process";
6
- import * as Layer from "effect/Layer";
7
- import { createManagementApiClient } from "@prisma/management-api-sdk";
8
- import * as Context from "effect/Context";
9
- import * as Effect from "effect/Effect";
10
- import * as Redacted from "effect/Redacted";
11
- import * as Config from "effect/Config";
12
- import * as Data from "effect/Data";
13
- import * as Provider from "alchemy/Provider";
14
- import { Resource } from "alchemy";
15
- import * as Schedule from "effect/Schedule";
16
5
  import * as c12 from "c12";
17
6
  import { pathToFileURL } from "node:url";
7
+ import { spawnSync } from "node:child_process";
18
8
  //#region ../../0-framework/3-tooling/assemble/dist/index.mjs
19
9
  /**
20
10
  * A user-facing assembly failure with a message that already names the fix
@@ -585,155 +575,34 @@ function Load(root, opts) {
585
575
  throw new LoadError("Load expects a service or module root (received another node kind).");
586
576
  }
587
577
  //#endregion
588
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CxGdfSAP.mjs
589
- /**
590
- * The Prisma service token used to authenticate Management API calls. Kept
591
- * as a Redacted value so it never lands in logs or error output.
592
- */
593
- var PrismaCredentials = class extends Context.Service()("PrismaCredentials") {};
594
- /** Resolve the token from the `PRISMA_SERVICE_TOKEN` environment variable. */
595
- const fromEnv = () => Layer.effect(PrismaCredentials, Effect.gen(function* () {
596
- return { token: yield* Config.redacted("PRISMA_SERVICE_TOKEN") };
597
- }));
598
- /**
599
- * The typed Prisma Management API client, built once from the resolved
600
- * credentials. Providers yield this in their outer Effect and call it inside
601
- * `reconcile` / `delete`.
602
- */
603
- var ManagementClient = class extends Context.Service()("PrismaManagementClient") {};
604
- const layer = () => Layer.effect(ManagementClient, Effect.gen(function* () {
605
- const { token } = yield* PrismaCredentials;
606
- return createManagementApiClient({ token: Redacted.value(token) });
607
- }));
608
- /** A non-2xx response from the Management API (or a transport failure). */
609
- var PrismaApiError = class extends Data.TaggedError("PrismaApiError") {};
610
- const attempt = (f) => Effect.tryPromise({
611
- try: f,
612
- catch: (cause) => new PrismaApiError({
613
- status: 0,
614
- message: String(cause)
615
- })
616
- });
617
- const fail = (r) => Effect.fail(new PrismaApiError({
618
- status: r.response.status,
619
- message: JSON.stringify(r.error)
620
- }));
621
- /** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
622
- const call = (f) => attempt(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
623
- /** Fire-and-forget a call, tolerating a 404 (already deleted). */
624
- const callVoid = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
625
- Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
626
- Resource("Prisma.ComputeService");
627
- Resource("Prisma.Deployment");
628
- Resource("Prisma.EnvironmentVariable");
629
- Resource("PrismaCloud.ServiceKey");
630
- Resource("Prisma.Connection");
631
- Resource("Prisma.Database");
632
- Resource("Prisma.Project");
633
- //#endregion
634
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
635
- /** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
636
- var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
637
- const listAllProjects = (client) => Effect.gen(function* () {
638
- const projects = [];
639
- let cursor;
640
- for (;;) {
641
- const query = cursor === void 0 ? {} : { cursor };
642
- const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
643
- projects.push(...page.data);
644
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
645
- cursor = page.pagination.nextCursor;
578
+ //#region ../../0-framework/1-core/core/dist/container-transport-DKmKg5JQ.mjs
579
+ /** '@prisma/composer-prisma-cloud' → 'PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD' */
580
+ function containerEnvVarName(extensionId) {
581
+ return `PRISMA_COMPOSER_CONTAINER_${extensionId.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
582
+ }
583
+ function collisionError(a, b, varName) {
584
+ return /* @__PURE__ */ new Error(`Extension ids "${a}" and "${b}" both mangle to the container transport variable "${varName}" — rename one of the extensions.`);
585
+ }
586
+ function emptySerializeError(extensionId) {
587
+ return /* @__PURE__ */ new Error(`Extension "${extensionId}"'s container instance serialized to an empty string — ContainerInstance.serialize() must return a non-empty string.`);
588
+ }
589
+ /** The env entries the CLI sets on the alchemy process: `{ [containerEnvVarName(id)]: instance.serialize() }` for every resolved instance. */
590
+ function containerEnv(instances) {
591
+ const env = {};
592
+ const ownerByVarName = /* @__PURE__ */ new Map();
593
+ for (const [extensionId, instance] of instances) {
594
+ const varName = containerEnvVarName(extensionId);
595
+ const owner = ownerByVarName.get(varName);
596
+ if (owner !== void 0) throw collisionError(owner, extensionId, varName);
597
+ ownerByVarName.set(varName, extensionId);
598
+ const serialized = instance.serialize();
599
+ if (serialized.length === 0) throw emptySerializeError(extensionId);
600
+ env[varName] = serialized;
646
601
  }
647
- return projects;
648
- });
649
- /**
650
- * Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
651
- * bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
652
- * one (the same normalization `state/bootstrap.ts` applies to the same
653
- * `/v1/projects` listing).
654
- */
655
- const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
656
- /**
657
- * Finds the app's Project by name in the workspace — PDP allows duplicate
658
- * project names, so more than one can match; the oldest wins. Creates one
659
- * if none match, unless `ensure` is `false` (find-only — `destroy`), in
660
- * which case an absent Project fails with `ContainerNotFoundError`. No
661
- * ownership marker and no `--project` override (both deferred — see
662
- * ADR-0019).
663
- */
664
- const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
665
- const oldest = (yield* listAllProjects(client)).filter((p) => bareWorkspaceId(p.workspace.id) === bareWorkspaceId(workspaceId) && p.name === appName).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
666
- if (oldest !== void 0) return oldest.id;
667
- if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
668
- return (yield* call(() => client.POST("/v1/projects", { body: {
669
- name: appName,
670
- workspaceId
671
- } }))).data.id;
672
- });
673
- const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
674
- path: { projectId },
675
- query: { gitName }
676
- } })).pipe(Effect.map((page) => page.data[0]?.id));
677
- /**
678
- * Finds the stage's Branch by its exact `gitName`, creating it if absent
679
- * unless `ensure` is `false` (find-only — `destroy`), in which case an
680
- * absent Branch fails with `ContainerNotFoundError`. The Management API has
681
- * no server-side "create-or-return" idempotency (`POST
682
- * /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
683
- * field to make that a no-op), so idempotency is client-side: observe
684
- * first, and on a racing 409 from create, re-observe rather than fail.
685
- */
686
- const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
687
- const existing = yield* findBranchId(client, projectId, gitName);
688
- if (existing !== void 0) return existing;
689
- if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
690
- appName,
691
- stage: gitName
692
- }));
693
- return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
694
- params: { path: { projectId } },
695
- body: { gitName }
696
- })).pipe(Effect.map((r) => r.data.id), Effect.catch((err) => err.status === 409 ? findBranchId(client, projectId, gitName).pipe(Effect.flatMap((id) => id === void 0 ? Effect.fail(err) : Effect.succeed(id))) : Effect.fail(err)));
697
- });
698
- /**
699
- * Resolves the two containers a stage's deploy runs into (ADR-0019): the
700
- * app's **Project**, found-or-created by name, and — for a named stage
701
- * only — its **Branch**, found-or-created by `gitName`. The default stage
702
- * (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
703
- * false` (`destroy`), nothing is created — an absent Project or Branch
704
- * fails with `ContainerNotFoundError` instead.
705
- */
706
- const resolveContainer = (opts) => Effect.gen(function* () {
707
- const client = yield* ManagementClient;
708
- const ensure = opts.ensure ?? true;
709
- const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
710
- if (opts.stage === void 0) return { projectId };
711
- return {
712
- projectId,
713
- branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
714
- };
715
- });
716
- /**
717
- * Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
718
- * the Branch still has live members or is the production/default Branch —
719
- * that surfaces as a `PrismaApiError`.
720
- */
721
- const deleteBranch = (branchId) => Effect.gen(function* () {
722
- const client = yield* ManagementClient;
723
- yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
724
- });
725
- /**
726
- * Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
727
- * 400 if the Project still has live dependencies (e.g. another stage's
728
- * Branch/resources) — that surfaces as a `PrismaApiError`.
729
- */
730
- const deleteProject = (projectId) => Effect.gen(function* () {
731
- const client = yield* ManagementClient;
732
- yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
733
- });
734
- Provider.ProviderCollection()("Prisma");
602
+ return env;
603
+ }
735
604
  //#endregion
736
- //#region ../../0-framework/3-tooling/cli/dist/cli-BeT-XTbX.mjs
605
+ //#region ../../0-framework/3-tooling/cli/dist/cli-Bc6_i0ls.mjs
737
606
  /**
738
607
  * A user-facing failure with a message that already names the fix (deploy-cli.md
739
608
  * § Error surface). `bin.ts` catches this — and any other Error, including
@@ -745,89 +614,6 @@ var CliError = class extends Error {
745
614
  this.name = "CliError";
746
615
  }
747
616
  };
748
- /**
749
- * Pipeline pre-stack step: resolves the app's Project + (named stage) Branch
750
- * via `@internal/lowering`'s `resolveContainer`, before the generated stack file
751
- * runs — `deploy` creates-if-absent, `destroy` finds only.
752
- */
753
- /** Validates `stage` as a git ref name via `git check-ref-format` — no silent normalization. */
754
- function validateStageName(stage) {
755
- const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
756
- if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
757
- if (result.status !== 0) throw new CliError(`Invalid --stage "${stage}": must be a valid git ref name (git check-ref-format rejected "refs/heads/${stage}").`);
758
- }
759
- async function ensureContainers(input, deps) {
760
- const env = input.env ?? process.env;
761
- const workspaceId = env["PRISMA_WORKSPACE_ID"];
762
- if (workspaceId === void 0 || workspaceId.length === 0) throw new CliError("environment variable PRISMA_WORKSPACE_ID is required.");
763
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
764
- if (input.stage !== void 0) validateStageName(input.stage);
765
- const program = resolveContainer({
766
- workspaceId,
767
- appName: input.appName,
768
- ...input.stage !== void 0 ? { stage: input.stage } : {},
769
- ensure: input.command === "deploy"
770
- }).pipe(Effect.map((c) => ({
771
- ok: true,
772
- container: c
773
- })), Effect.catchTag("ContainerNotFoundError", (e) => Effect.succeed({
774
- ok: false,
775
- message: `Nothing deployed for ${e.appName}${e.stage ? `/${e.stage}` : ""} — deploy it first.`
776
- })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
777
- ok: false,
778
- message: `Prisma Management API error resolving containers: ${e.message}.`
779
- })));
780
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
781
- const outcome = await Effect.runPromise(provided);
782
- if (!outcome.ok) throw new CliError(outcome.message);
783
- return outcome.container;
784
- }
785
- /**
786
- * Soft-deletes a named stage's Branch after a successful `alchemy destroy`
787
- * has removed its members (spec §10) — the Management API refuses to delete
788
- * a Branch that still has live members.
789
- */
790
- async function deleteStageBranch(input, deps) {
791
- const env = input.env ?? process.env;
792
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
793
- const program = deleteBranch(input.branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
794
- ok: false,
795
- message: `Failed to delete the stage Branch: ${e.message}.`
796
- })));
797
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
798
- const outcome = await Effect.runPromise(provided);
799
- if (!outcome.ok) throw new CliError(outcome.message);
800
- }
801
- /**
802
- * Best-effort cleanup after a successful `--production` destroy: removes
803
- * the app's Project so hand-run stacks don't accumulate as empty Projects
804
- * (they eventually hit the workspace's plan limit). Unlike `deleteStageBranch`,
805
- * this never throws: the destroy itself already succeeded, and the API's own
806
- * 400 ("still has dependencies") is the only check that matters — failing
807
- * the command over a cleanup step would be worse than leaving a Project shell.
808
- */
809
- async function deleteAppProject(input, deps) {
810
- const env = input.env ?? process.env;
811
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
812
- console.warn(`Skipped removing the Project (${input.projectId}): PRISMA_SERVICE_TOKEN is not set.`);
813
- return;
814
- }
815
- const program = deleteProject(input.projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
816
- ok: false,
817
- error: e
818
- })));
819
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
820
- const outcome = await Effect.runPromise(provided);
821
- if (outcome.ok) {
822
- console.log(`Removed the Project (${input.projectId}) — nothing was left in it.`);
823
- return;
824
- }
825
- if (outcome.error.status === 400) {
826
- console.log(`Kept the Project (${input.projectId}) — it still has another stage's resources.`);
827
- return;
828
- }
829
- console.warn(`Could not remove the Project (${input.projectId}) after destroy: ${outcome.error.message}.`);
830
- }
831
617
  /** Pipeline step 6: writes a regenerated-every-run, independently runnable stack module at `.prisma-composer/alchemy.run.ts`. */
832
618
  const GENERATED_DIR = ".prisma-composer";
833
619
  const GENERATED_FILE = "alchemy.run.ts";
@@ -930,7 +716,8 @@ function validateConfigShape(loaded, configPath) {
930
716
  if (seen.has(id)) throw new CliError(`${CONFIG_FILENAME}: extension "${id}" is listed more than once in \`extensions\`.`);
931
717
  seen.add(id);
932
718
  }
933
- if (typeof loaded["state"] !== "function") throw fieldError("state", "must be a function returning the deploy state layer (e.g. () => prismaState())");
719
+ const state = loaded["state"];
720
+ if (!isRecord(state) || typeof state["extension"] !== "string" || typeof state["create"] !== "function") throw fieldError("state", "must be a state descriptor (e.g. prismaState())");
934
721
  return blindCast(loaded);
935
722
  }
936
723
  /**
@@ -989,7 +776,7 @@ function resolveAlchemyBin(startDir) {
989
776
  dir = parent;
990
777
  }
991
778
  }
992
- /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus the resolved Project/Branch ids. */
779
+ /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus every extension's resolved container. */
993
780
  function runAlchemy(input) {
994
781
  const bin = resolveAlchemyBin(input.cwd);
995
782
  const args = [
@@ -1003,8 +790,7 @@ function runAlchemy(input) {
1003
790
  stdio: "inherit",
1004
791
  env: {
1005
792
  ...input.env ?? process.env,
1006
- PRISMA_PROJECT_ID: input.projectId,
1007
- ...input.branchId !== void 0 ? { PRISMA_BRANCH_ID: input.branchId } : {}
793
+ ...input.containerEnv
1008
794
  }
1009
795
  });
1010
796
  if (result.error !== void 0) throw result.error;
@@ -1030,6 +816,12 @@ function validateRegistryCoverage(graph, config) {
1030
816
  lookup(extensions, node.build.extension, node.build.type, "build", `service node "${id}"'s build descriptor`);
1031
817
  }
1032
818
  }
819
+ /** A stage name must be a valid git ref (deploy-cli.md) — checked via `git check-ref-format`, never silently normalized. Runs before anything platform-specific. */
820
+ function validateStageName(stage) {
821
+ const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
822
+ if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
823
+ if (result.status !== 0) throw new CliError(`Invalid --stage "${stage}": must be a valid git ref name (git check-ref-format rejected "refs/heads/${stage}").`);
824
+ }
1033
825
  /**
1034
826
  * Argument parsing (clipanion — prisma-next's CLI idiom, see
1035
827
  * prisma-next/packages/1-framework/3-tooling/cli/src/migration-cli.ts) +
@@ -1125,6 +917,7 @@ async function run(argv, deps = {}) {
1125
917
  throw error;
1126
918
  }
1127
919
  const stage = effectiveStage(args);
920
+ if (stage !== void 0) validateStageName(stage);
1128
921
  const cwd = process.cwd();
1129
922
  if (args.command === "destroy") warnIfNoLocalDeployState(cwd);
1130
923
  const resolvedEntryPath = path.resolve(cwd, args.entry);
@@ -1144,18 +937,32 @@ async function run(argv, deps = {}) {
1144
937
  if (args.command === "destroy" && error instanceof Error) throw new CliError(`${error.message}\n\ndestroy evaluates the same stack program as deploy, which packages the built artifacts — so the app must be built first. Run the build, then retry the destroy.`);
1145
938
  throw error;
1146
939
  }
1147
- const { projectId, branchId } = await (deps.ensureContainers ?? ensureContainers)({
1148
- command: args.command,
1149
- appName: name,
1150
- stage
1151
- });
940
+ const containers = /* @__PURE__ */ new Map();
941
+ for (const extension of config.extensions) {
942
+ if (extension.container === void 0) continue;
943
+ try {
944
+ if (args.command === "deploy") containers.set(extension.id, await extension.container.ensure({
945
+ appName: name,
946
+ stage
947
+ }));
948
+ else {
949
+ const instance = await extension.container.locate({
950
+ appName: name,
951
+ stage
952
+ });
953
+ if (instance === void 0) throw new CliError(`Nothing deployed for ${name}${stage !== void 0 ? `/${stage}` : ""} — deploy it first.`);
954
+ containers.set(extension.id, instance);
955
+ }
956
+ } catch (error) {
957
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
958
+ }
959
+ }
1152
960
  if (args.command === "deploy") for (const extension of config.extensions) {
1153
961
  if (extension.preflight === void 0) continue;
1154
962
  try {
1155
963
  await extension.preflight({
1156
964
  graph,
1157
- projectId,
1158
- branchId,
965
+ container: containers.get(extension.id),
1159
966
  stage
1160
967
  });
1161
968
  } catch (error) {
@@ -1175,28 +982,36 @@ async function run(argv, deps = {}) {
1175
982
  stackFileRelativePath: GENERATED_STACK_RELATIVE_PATH,
1176
983
  cwd,
1177
984
  stage,
1178
- projectId,
1179
- ...branchId !== void 0 ? { branchId } : {}
985
+ containerEnv: containerEnv(containers)
1180
986
  });
1181
987
  if (status !== 0) {
1182
988
  console.error(`\nGenerated stack file: ${stackPath}`);
1183
989
  console.error(`Run \`alchemy ${args.command} ${GENERATED_STACK_RELATIVE_PATH} --yes\` from ${cwd} to reproduce this directly.`);
1184
990
  return status;
1185
991
  }
1186
- if (args.command === "destroy") for (const extension of config.extensions) {
1187
- if (extension.teardown === void 0) continue;
1188
- try {
1189
- await extension.teardown({
1190
- projectId,
1191
- branchId,
1192
- stage
1193
- });
1194
- } catch (error) {
1195
- throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
992
+ if (args.command === "destroy") {
993
+ for (const extension of config.extensions) {
994
+ if (extension.teardown === void 0) continue;
995
+ try {
996
+ await extension.teardown({
997
+ container: containers.get(extension.id),
998
+ stage
999
+ });
1000
+ } catch (error) {
1001
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1002
+ }
1003
+ }
1004
+ for (const extension of config.extensions) {
1005
+ if (extension.container === void 0) continue;
1006
+ const instance = containers.get(extension.id);
1007
+ if (instance === void 0) continue;
1008
+ try {
1009
+ await extension.container.remove(instance);
1010
+ } catch (error) {
1011
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1012
+ }
1196
1013
  }
1197
1014
  }
1198
- if (args.command === "destroy" && branchId !== void 0) await (deps.deleteBranch ?? ((input) => deleteStageBranch(input)))({ branchId });
1199
- else if (args.command === "destroy") await (deps.deleteProject ?? ((input) => deleteAppProject(input)))({ projectId });
1200
1015
  return status;
1201
1016
  } catch (error) {
1202
1017
  console.error(`\nGenerated stack file: ${stackPath}`);