@prisma/composer 0.1.0 → 0.2.0-dev.10

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 (80) hide show
  1. package/dist/app-config-CVyObaOe-PF94uVBK.d.mts +419 -0
  2. package/dist/app-config-CVyObaOe-PF94uVBK.d.mts.map +1 -0
  3. package/dist/assertions.d.mts +1 -1
  4. package/dist/assertions.d.mts.map +1 -0
  5. package/dist/bin.mjs +220 -258
  6. package/dist/bin.mjs.map +1 -1
  7. package/dist/casts.d.mts +1 -1
  8. package/dist/casts.d.mts.map +1 -0
  9. package/dist/config-BP2DrIX_.d.mts +1 -0
  10. package/dist/config.d.mts +3 -2
  11. package/dist/config.mjs +2 -1
  12. package/dist/config.mjs.map +1 -1
  13. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs +45 -0
  14. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs.map +1 -0
  15. package/dist/deploy-BP2DrIX_.d.mts +1 -0
  16. package/dist/deploy.d.mts +3 -2
  17. package/dist/deploy.mjs +181 -23
  18. package/dist/deploy.mjs.map +1 -1
  19. package/dist/{dist-zBU8ASQW.mjs → dist-BkSJOwTm.mjs} +16 -4
  20. package/dist/dist-BkSJOwTm.mjs.map +1 -0
  21. package/dist/{graph-BYdCQKya-BI0njTow.mjs → graph-CP16cJyH-D5IW87Y-.mjs} +118 -21
  22. package/dist/graph-CP16cJyH-D5IW87Y-.mjs.map +1 -0
  23. package/dist/graph-DUSgjmzA-DhtkTqoz.d.mts +18 -0
  24. package/dist/graph-DUSgjmzA-DhtkTqoz.d.mts.map +1 -0
  25. package/dist/{config-ob5OhCSP-sP3GW3uu.d.mts → graph-types-BcbVhojU-C3fhxjPK.d.mts} +205 -114
  26. package/dist/graph-types-BcbVhojU-C3fhxjPK.d.mts.map +1 -0
  27. package/dist/{index-CZSc9drz.d.mts → index-BFBNz7Aw.d.mts} +5 -20
  28. package/dist/index-BFBNz7Aw.d.mts.map +1 -0
  29. package/dist/index.d.mts +4 -3
  30. package/dist/index.mjs +3 -3
  31. package/dist/{index-Dh4Zro0y.d.mts → nextjs-DLyeRR7M-DwnBjryZ.d.mts} +6 -5
  32. package/dist/nextjs-DLyeRR7M-DwnBjryZ.d.mts.map +1 -0
  33. package/dist/nextjs-control.d.mts +6 -5
  34. package/dist/nextjs-control.d.mts.map +1 -0
  35. package/dist/nextjs-control.mjs +9 -11
  36. package/dist/nextjs-control.mjs.map +1 -1
  37. package/dist/nextjs.d.mts +2 -2
  38. package/dist/nextjs.mjs.map +1 -1
  39. package/dist/node-control.d.mts +5 -3
  40. package/dist/node-control.d.mts.map +1 -0
  41. package/dist/node-control.mjs +115 -24
  42. package/dist/node-control.mjs.map +1 -1
  43. package/dist/node.d.mts +18 -5
  44. package/dist/node.d.mts.map +1 -0
  45. package/dist/node.mjs +2 -1
  46. package/dist/node.mjs.map +1 -1
  47. package/dist/report.d.mts +17 -0
  48. package/dist/report.d.mts.map +1 -0
  49. package/dist/report.mjs +79 -0
  50. package/dist/report.mjs.map +1 -0
  51. package/dist/service-rpc.d.mts +61 -0
  52. package/dist/service-rpc.d.mts.map +1 -0
  53. package/dist/service-rpc.mjs +353 -0
  54. package/dist/service-rpc.mjs.map +1 -0
  55. package/dist/testing.d.mts +2 -3
  56. package/dist/testing.d.mts.map +1 -0
  57. package/package.json +20 -18
  58. package/src/exports/assertions.ts +1 -0
  59. package/src/exports/casts.ts +1 -0
  60. package/src/exports/config.ts +1 -0
  61. package/src/exports/deploy.ts +1 -0
  62. package/src/exports/index.ts +1 -0
  63. package/src/exports/nextjs-control.ts +1 -0
  64. package/src/exports/nextjs.ts +2 -0
  65. package/src/exports/node-control.ts +1 -0
  66. package/src/exports/node.ts +2 -0
  67. package/src/exports/report.ts +1 -0
  68. package/src/exports/service-rpc.ts +1 -0
  69. package/src/exports/testing.ts +1 -0
  70. package/dist/app-config-BUqyK6N6-CVq3uvHF.d.mts +0 -188
  71. package/dist/config-BVVgDSdq.d.mts +0 -1
  72. package/dist/deploy-BVVgDSdq.d.mts +0 -1
  73. package/dist/dist-zBU8ASQW.mjs.map +0 -1
  74. package/dist/graph-BYdCQKya-BI0njTow.mjs.map +0 -1
  75. package/dist/rpc.d.mts +0 -43
  76. package/dist/rpc.mjs +0 -132
  77. package/dist/rpc.mjs.map +0 -1
  78. package/dist/tsdown.d.mts +0 -9
  79. package/dist/tsdown.mjs +0 -35
  80. package/dist/tsdown.mjs.map +0 -1
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
@@ -31,49 +21,26 @@ var AssembleError = class extends Error {
31
21
  }
32
22
  };
33
23
  /**
34
- * The wrapper-inlining decision (S2's open question — see the project's
35
- * design-notes.md and this slice's final report for the write-up).
36
- *
37
- * The CLI has no config file, so it can't take per-app `wrapperNoExternal`
38
- * patterns — it needs one general rule for what an assembler inlines into
39
- * the wrapper build besides `@prisma/*` (already the assemblers' own
40
- * default).
41
- *
42
- * The rule: inline everything except the runtime's own modules — `bun`,
43
- * `bun:*` (e.g. bun:sqlite), and `node:*` builtins — which resolve inside
44
- * the deploy VM at runtime and must never be bundled. Verified (both
45
- * `@prisma/composer/node/control` and `@prisma/composer/nextjs/control` set
46
- * `external: ['bun']` ahead of `noExternal` in their tsdown call, and tsdown/
47
- * rolldown's explicit `external` wins over a `noExternal` match — proven by
48
- * building an app's wrapper with this exact pattern: the
49
- * `bun` import stays external while everything else, including the app's own
50
- * workspace deps, gets inlined) — see this slice's final report for the
51
- * storefront-auth wrapper proof (arktype + `@storefront-auth/auth/contract`
52
- * inlined with no other file changes there).
53
- */
54
- const INLINE_EVERYTHING_EXCEPT_RUNTIME_BUILTINS = [/^(?!bun$)(?!bun:)(?!node:).+/];
55
- /**
56
- * The registry route for one service's build: descriptor by
57
- * `build.extension`, control by `build.type`, kind must be "build". The CLI's
58
- * coverage validation reports the same misses earlier with the config fix;
59
- * these errors are the backstop for programmatic callers.
24
+ * The registry route for one service's build: extension by
25
+ * `build.extension`, node descriptor by `build.type`, kind must be "build".
26
+ * The CLI's coverage validation reports the same misses earlier with the
27
+ * config fix; these errors are the backstop for programmatic callers.
60
28
  */
61
- function buildControlAssemble(config, node, address, cwd) {
29
+ function buildDescriptorAssemble(config, node, address, cwd) {
62
30
  const { extension, type } = node.build;
63
- const descriptor = config.extensions.find((candidate) => candidate.id === extension);
64
- if (descriptor === void 0) throw new AssembleError(`No extension "${extension}" is configured (needed by service "${node.name}"'s build) — add it to prisma-composer.config.ts's \`extensions\`.`);
65
- const control = descriptor.nodes[type];
66
- if (control === void 0) throw new AssembleError(`Extension "${extension}" has no control for build type "${type}" (known: ${Object.keys(descriptor.nodes).join(", ")}).`);
67
- if (control.kind !== "build") throw new AssembleError(`Extension "${extension}"'s control for type "${type}" is a "${control.kind}" control — a service build descriptor needs a "build" control.`);
68
- return control.assemble({
31
+ const extensionDescriptor = config.extensions.find((candidate) => candidate.id === extension);
32
+ if (extensionDescriptor === void 0) throw new AssembleError(`No extension "${extension}" is configured (needed by service "${node.name}"'s build) — add it to prisma-composer.config.ts's \`extensions\`.`);
33
+ const nodeDescriptor = extensionDescriptor.nodes[type];
34
+ if (nodeDescriptor === void 0) throw new AssembleError(`Extension "${extension}" has no descriptor for build type "${type}" (known: ${Object.keys(extensionDescriptor.nodes).join(", ")}).`);
35
+ if (nodeDescriptor.kind !== "build") throw new AssembleError(`Extension "${extension}"'s descriptor for type "${type}" is a "${nodeDescriptor.kind}" descriptorassembling a service build needs a "build" descriptor.`);
36
+ return nodeDescriptor.assemble({
69
37
  build: node.build,
70
- wrapperNoExternal: INLINE_EVERYTHING_EXCEPT_RUNTIME_BUILTINS,
71
38
  address,
72
39
  cwd
73
40
  });
74
41
  }
75
42
  async function assembleServices(graph, config, cwd, run) {
76
- const runAssembler = run ?? ((node, address, nodeCwd) => buildControlAssemble(config, node, address, nodeCwd));
43
+ const runAssembler = run ?? ((node, address, nodeCwd) => buildDescriptorAssemble(config, node, address, nodeCwd));
77
44
  const serviceNodes = graph.nodes.filter((n) => n.node.kind === "service");
78
45
  if (serviceNodes.length === 0) throw new AssembleError("The loaded graph has no service to assemble.");
79
46
  const bundles = {};
@@ -121,7 +88,7 @@ function blindCast(input) {
121
88
  return input;
122
89
  }
123
90
  //#endregion
124
- //#region ../../0-framework/1-core/core/dist/graph-BYdCQKya.mjs
91
+ //#region ../../0-framework/1-core/core/dist/graph-CP16cJyH.mjs
125
92
  /** Thrown by Load when the graph is malformed. */
126
93
  var LoadError = class extends Error {
127
94
  constructor(message) {
@@ -139,6 +106,20 @@ const SECRET_SOURCE = blindCast(Symbol.for("prisma:secret-source"));
139
106
  function isSecretSource(value) {
140
107
  return typeof value === "object" && value !== null && blindCast(value)[SECRET_SOURCE] === true;
141
108
  }
109
+ const PARAM_SOURCE = blindCast(Symbol.for("prisma:param-source"));
110
+ /** True if `value` is a param source (an `envParam` result or a forwarded ctx.params ref). */
111
+ function isParamSource(value) {
112
+ return typeof value === "object" && value !== null && blindCast(value)[PARAM_SOURCE] === true;
113
+ }
114
+ /**
115
+ * Core's grammar for every name that becomes a config-key segment —
116
+ * addresses, input/param/secret names: ASCII letters and digits only.
117
+ * Conservative by design so any target medium — POSIX env-var keys
118
+ * included — can uppercase and "_"-join segments without escaping.
119
+ */
120
+ function isConfigKeySegment(name) {
121
+ return /^[A-Za-z0-9]+$/.test(name);
122
+ }
142
123
  /**
143
124
  * True if `value` was constructed by this module's factories. Checks the
144
125
  * brand only, never a prototype — a graph may mix nodes from a different
@@ -238,7 +219,8 @@ function loadService(root, rootId) {
238
219
  root: rootGraphNode,
239
220
  nodes: [...topoSort(nodes, edges), rootGraphNode],
240
221
  edges,
241
- secrets: []
222
+ secrets: [],
223
+ params: []
242
224
  };
243
225
  }
244
226
  /**
@@ -285,6 +267,8 @@ function producerIdOf(ref) {
285
267
  const MODULE_INPUT_KEY = Symbol("prisma:module-input-key");
286
268
  /** Same per-key identity trick as MODULE_INPUT_KEY, for the parallel `ctx.secrets` forwarding channel. */
287
269
  const MODULE_SECRET_KEY = Symbol("prisma:module-secret-key");
270
+ /** Same per-key identity trick as MODULE_INPUT_KEY, for the parallel `ctx.params` forwarding channel. */
271
+ const MODULE_PARAM_KEY = Symbol("prisma:module-param-key");
288
272
  /** Whether `ref` carries a callable `satisfies` that accepts `required` truthily. */
289
273
  function satisfiesRequired(ref, required) {
290
274
  return typeof ref === "object" && ref !== null && "satisfies" in ref && typeof ref.satisfies === "function" && ref.satisfies(required);
@@ -344,6 +328,29 @@ function validateSecretBinding(child, id, secretWiring, enclosingModuleName) {
344
328
  for (const slot of Object.keys(secretWiring)) if (!Object.hasOwn(child.secretSlots, slot)) throw new LoadError(`The secrets for "${id}" name "${slot}", which is not a secret slot of that ${kind} (module "${enclosingModuleName}").`);
345
329
  }
346
330
  /**
331
+ * Checks the params wired into a provisioned SERVICE: unlike a secret slot, a
332
+ * declared param is never required to be bound here — it may fall back to its
333
+ * own `default` (checked later, at `buildConfig`) — so this only rejects a
334
+ * wired key that names something other than a real declared param.
335
+ */
336
+ function validateServiceParamBinding(child, id, paramWiring, enclosingModuleName) {
337
+ for (const slot of Object.keys(paramWiring)) if (!Object.hasOwn(child.params, slot)) throw new LoadError(`The params for "${id}" name "${slot}", which is not a param of that service (module "${enclosingModuleName}").`);
338
+ }
339
+ /**
340
+ * Checks the params wired into a provisioned MODULE: every declared
341
+ * param-forwarding slot must be bound to a real `ParamSource` — a slot has no
342
+ * schema of its own, so (unlike a service param) it cannot fall back to a
343
+ * literal or a default; the param analog of `validateSecretBinding`.
344
+ */
345
+ function validateParamNeedBinding(child, id, paramWiring, enclosingModuleName) {
346
+ for (const slot of Object.keys(child.paramSlots)) {
347
+ const bound = paramWiring[slot];
348
+ if (bound === void 0) throw new LoadError(`Param slot "${slot}" of provisioned module "${id}" is not bound (module "${enclosingModuleName}") — bind it with a param source (e.g. envParam('NAME')) or forward ctx.params.`);
349
+ if (!isParamSource(bound)) throw new LoadError(`Param slot "${slot}" of "${id}" (module "${enclosingModuleName}") was wired with a non-source value — a module param-forwarding slot carries no schema to validate a literal against; use envParam('NAME') or a forwarded ctx.params ref.`);
350
+ }
351
+ for (const slot of Object.keys(paramWiring)) if (!Object.hasOwn(child.paramSlots, slot)) throw new LoadError(`The params for "${id}" name "${slot}", which is not a param slot of that module (module "${enclosingModuleName}").`);
352
+ }
353
+ /**
347
354
  * Recursively flattens one module's body into the shared graph state and
348
355
  * returns its resolved ModuleOutputs (one ref-port per expose key) for the
349
356
  * caller (the enclosing provision() call, or Load itself for the root) to
@@ -357,10 +364,11 @@ function validateSecretBinding(child, id, secretWiring, enclosingModuleName) {
357
364
  * forward in a producer provisioned by an ancestor scope, and it is the
358
365
  * shared `byId` (keyed by full address) that lets that resolve.
359
366
  */
360
- function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pending, secretBindings, byId) {
367
+ function flatten(moduleNode, address, wiring, secretWiring, paramWiring, nodes, edges, pending, secretBindings, paramBindings, byId) {
361
368
  const localIds = /* @__PURE__ */ new Set();
362
369
  const used = /* @__PURE__ */ new Set();
363
370
  const usedSecrets = /* @__PURE__ */ new Set();
371
+ const usedParams = /* @__PURE__ */ new Set();
364
372
  const ctxInputs = {};
365
373
  for (const key of Object.keys(moduleNode.deps)) {
366
374
  const wired = wiring[key];
@@ -383,12 +391,24 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
383
391
  const markSecretsUsed = (values) => {
384
392
  for (const value of Object.values(values)) for (const key of Object.keys(ctxSecrets)) if (value === ctxSecrets[key]) usedSecrets.add(key);
385
393
  };
394
+ const ctxParams = {};
395
+ for (const key of Object.keys(moduleNode.paramSlots)) {
396
+ const bound = paramWiring[key];
397
+ ctxParams[key] = typeof bound === "object" && bound !== null ? {
398
+ ...bound,
399
+ [MODULE_PARAM_KEY]: key
400
+ } : bound;
401
+ }
402
+ const markParamsUsed = (values) => {
403
+ for (const value of Object.values(values)) for (const key of Object.keys(ctxParams)) if (value === ctxParams[key]) usedParams.add(key);
404
+ };
386
405
  const provision = (child, opts) => {
387
406
  const id = opts?.id ?? child.name;
388
407
  const provisionWiring = opts?.deps;
389
408
  const provisionSecrets = opts?.secrets;
409
+ const provisionParams = opts?.params;
390
410
  if (typeof id !== "string" || id.length === 0) throw new LoadError(`provision() requires a non-empty id (module "${moduleNode.name}").`);
391
- if (id.includes("_") || id.includes(".")) throw new LoadError(`provision() id "${id}" (module "${moduleNode.name}") may not contain "_" or "." "_" is the config-key separator and "." the node-id path separator; either inside an id collides with the joined form of other names.`);
411
+ if (!isConfigKeySegment(id)) throw new LoadError(`provision() id "${id}" (module "${moduleNode.name}") must contain only ASCII letters and digits ([A-Za-z0-9]) the id becomes an address segment, and addresses derive deterministic config keys (segments uppercased and joined with "_"). "${id}" would put "${id.toUpperCase()}" inside the derived key. ("_" collides with the key separator and "." with the address separator.)`);
392
412
  if (localIds.has(id)) throw new LoadError(`Duplicate provision id "${id}" in module "${moduleNode.name}".`);
393
413
  const untrusted = child;
394
414
  if (!isNode(untrusted) || untrusted.kind !== "service" && untrusted.kind !== "resource" && untrusted.kind !== "module") throw new LoadError(`provision("${id}") expects a branded service, resource, or module node (construct it with the service()/resource()/module() factories or a pack's own).`);
@@ -397,6 +417,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
397
417
  if (child.kind === "resource") {
398
418
  if (provisionWiring !== void 0) throw new LoadError(`provision("${id}") received deps for a resource — a resource has no dependency slots to satisfy.`);
399
419
  if (provisionSecrets !== void 0) throw new LoadError(`provision("${id}") received secrets for a resource — a resource has no secret slots to satisfy.`);
420
+ if (provisionParams !== void 0) throw new LoadError(`provision("${id}") received params for a resource — a resource has no params to satisfy.`);
400
421
  if (child.type.length === 0) throw new LoadError(`provision("${id}") received a resource with an empty node type.`);
401
422
  byId.set(fullAddress, child);
402
423
  nodes.push({
@@ -410,6 +431,8 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
410
431
  const localSecrets = { ...provisionSecrets ?? {} };
411
432
  markSecretsUsed(localSecrets);
412
433
  validateSecretBinding(child, id, localSecrets, moduleNode.name);
434
+ const localParams = { ...provisionParams ?? {} };
435
+ markParamsUsed(localParams);
413
436
  if (child.kind === "service") {
414
437
  for (const slot of Object.keys(child.secretSlots)) {
415
438
  const bound = localSecrets[slot];
@@ -419,6 +442,12 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
419
442
  source: bound
420
443
  });
421
444
  }
445
+ validateServiceParamBinding(child, id, localParams, moduleNode.name);
446
+ for (const [slot, binding] of Object.entries(localParams)) paramBindings.push({
447
+ serviceAddress: fullAddress,
448
+ slot,
449
+ binding
450
+ });
422
451
  const inputs = serviceInputs(child, fullAddress);
423
452
  nodes.push(...inputs.nodes, {
424
453
  id: fullAddress,
@@ -435,6 +464,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
435
464
  byId.set(fullAddress, child);
436
465
  return refFor(fullAddress, child);
437
466
  }
467
+ validateParamNeedBinding(child, id, localParams, moduleNode.name);
438
468
  edges.push(...wiringEdges(localWiring, fullAddress));
439
469
  pending.push({
440
470
  deps: child.deps,
@@ -443,7 +473,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
443
473
  targetKind: "module",
444
474
  enclosingModuleName: moduleNode.name
445
475
  });
446
- const childOutputs = flatten(child, fullAddress, localWiring, localSecrets, nodes, edges, pending, secretBindings, byId);
476
+ const childOutputs = flatten(child, fullAddress, localWiring, localSecrets, localParams, nodes, edges, pending, secretBindings, paramBindings, byId);
447
477
  nodes.push({
448
478
  id: fullAddress,
449
479
  node: child
@@ -457,12 +487,14 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
457
487
  const ctx = blindCast({
458
488
  inputs: ctxInputs,
459
489
  secrets: ctxSecrets,
490
+ params: ctxParams,
460
491
  provision: blindCast(provision)
461
492
  });
462
493
  const outputs = blindCast(moduleNode.body(ctx) ?? {});
463
494
  markUsed(outputs);
464
495
  for (const key of Object.keys(moduleNode.deps)) if (!used.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares input "${key}" but never forwards it into a provision nor returns it as an output.`);
465
496
  for (const key of Object.keys(moduleNode.secretSlots)) if (!usedSecrets.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares secret "${key}" but never forwards it into a provision.`);
497
+ for (const key of Object.keys(moduleNode.paramSlots)) if (!usedParams.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares param "${key}" but never forwards it into a provision.`);
466
498
  for (const [key, contract] of Object.entries(moduleNode.expose)) {
467
499
  const port = outputs[key];
468
500
  if (port === void 0) throw new LoadError(`Module "${moduleNode.name}" declares expose "${key}" but its body did not return a port for it.`);
@@ -482,12 +514,18 @@ function loadModule(root, opts) {
482
514
  const names = rootSecretKeys.map((k) => `"${k}"`).join(", ");
483
515
  throw new LoadError(`Module "${root.name}" declares secret${rootSecretKeys.length > 1 ? "s" : ""} ${names} but is being deployed as the root — a root has no enclosing scope to bind them; the root binds secrets with envSecret('NAME'), it does not declare secret slots of its own.`);
484
516
  }
517
+ const rootParamKeys = Object.keys(root.paramSlots);
518
+ if (rootParamKeys.length > 0) {
519
+ const names = rootParamKeys.map((k) => `"${k}"`).join(", ");
520
+ throw new LoadError(`Module "${root.name}" declares param${rootParamKeys.length > 1 ? "s" : ""} ${names} but is being deployed as the root — a root has no enclosing scope to bind them; the root binds params directly on each provision() call, it does not declare param-forwarding slots of its own.`);
521
+ }
485
522
  const nodes = [];
486
523
  const edges = [];
487
524
  const pending = [];
488
525
  const secretBindings = [];
526
+ const paramBindings = [];
489
527
  const byId = /* @__PURE__ */ new Map();
490
- flatten(root, void 0, {}, {}, nodes, edges, pending, secretBindings, byId);
528
+ flatten(root, void 0, {}, {}, {}, nodes, edges, pending, secretBindings, paramBindings, byId);
491
529
  for (const entry of pending) validateWiring(entry, byId);
492
530
  assertDependencyDag(edges);
493
531
  const rootGraphNode = {
@@ -498,7 +536,8 @@ function loadModule(root, opts) {
498
536
  root: rootGraphNode,
499
537
  nodes: [...topoSort(nodes, edges), rootGraphNode],
500
538
  edges,
501
- secrets: secretBindings
539
+ secrets: secretBindings,
540
+ params: paramBindings
502
541
  };
503
542
  }
504
543
  /**
@@ -545,145 +584,34 @@ function Load(root, opts) {
545
584
  throw new LoadError("Load expects a service or module root (received another node kind).");
546
585
  }
547
586
  //#endregion
548
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CxGdfSAP.mjs
549
- /**
550
- * The Prisma service token used to authenticate Management API calls. Kept
551
- * as a Redacted value so it never lands in logs or error output.
552
- */
553
- var PrismaCredentials = class extends Context.Service()("PrismaCredentials") {};
554
- /** Resolve the token from the `PRISMA_SERVICE_TOKEN` environment variable. */
555
- const fromEnv = () => Layer.effect(PrismaCredentials, Effect.gen(function* () {
556
- return { token: yield* Config.redacted("PRISMA_SERVICE_TOKEN") };
557
- }));
558
- /**
559
- * The typed Prisma Management API client, built once from the resolved
560
- * credentials. Providers yield this in their outer Effect and call it inside
561
- * `reconcile` / `delete`.
562
- */
563
- var ManagementClient = class extends Context.Service()("PrismaManagementClient") {};
564
- const layer = () => Layer.effect(ManagementClient, Effect.gen(function* () {
565
- const { token } = yield* PrismaCredentials;
566
- return createManagementApiClient({ token: Redacted.value(token) });
567
- }));
568
- /** A non-2xx response from the Management API (or a transport failure). */
569
- var PrismaApiError = class extends Data.TaggedError("PrismaApiError") {};
570
- const attempt = (f) => Effect.tryPromise({
571
- try: f,
572
- catch: (cause) => new PrismaApiError({
573
- status: 0,
574
- message: String(cause)
575
- })
576
- });
577
- const fail = (r) => Effect.fail(new PrismaApiError({
578
- status: r.response.status,
579
- message: JSON.stringify(r.error)
580
- }));
581
- /** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
582
- const call = (f) => attempt(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
583
- /** Fire-and-forget a call, tolerating a 404 (already deleted). */
584
- const callVoid = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
585
- Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
586
- Resource("Prisma.ComputeService");
587
- Resource("Prisma.Deployment");
588
- Resource("Prisma.EnvironmentVariable");
589
- Resource("Prisma.Connection");
590
- Resource("Prisma.Database");
591
- Resource("Prisma.Project");
592
- //#endregion
593
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
594
- /** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
595
- var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
596
- const listAllProjects = (client) => Effect.gen(function* () {
597
- const projects = [];
598
- let cursor;
599
- for (;;) {
600
- const query = cursor === void 0 ? {} : { cursor };
601
- const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
602
- projects.push(...page.data);
603
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
604
- cursor = page.pagination.nextCursor;
587
+ //#region ../../0-framework/1-core/core/dist/container-transport-DKmKg5JQ.mjs
588
+ /** '@prisma/composer-prisma-cloud' → 'PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD' */
589
+ function containerEnvVarName(extensionId) {
590
+ return `PRISMA_COMPOSER_CONTAINER_${extensionId.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
591
+ }
592
+ function collisionError(a, b, varName) {
593
+ return /* @__PURE__ */ new Error(`Extension ids "${a}" and "${b}" both mangle to the container transport variable "${varName}" — rename one of the extensions.`);
594
+ }
595
+ function emptySerializeError(extensionId) {
596
+ return /* @__PURE__ */ new Error(`Extension "${extensionId}"'s container instance serialized to an empty string — ContainerInstance.serialize() must return a non-empty string.`);
597
+ }
598
+ /** The env entries the CLI sets on the alchemy process: `{ [containerEnvVarName(id)]: instance.serialize() }` for every resolved instance. */
599
+ function containerEnv(instances) {
600
+ const env = {};
601
+ const ownerByVarName = /* @__PURE__ */ new Map();
602
+ for (const [extensionId, instance] of instances) {
603
+ const varName = containerEnvVarName(extensionId);
604
+ const owner = ownerByVarName.get(varName);
605
+ if (owner !== void 0) throw collisionError(owner, extensionId, varName);
606
+ ownerByVarName.set(varName, extensionId);
607
+ const serialized = instance.serialize();
608
+ if (serialized.length === 0) throw emptySerializeError(extensionId);
609
+ env[varName] = serialized;
605
610
  }
606
- return projects;
607
- });
608
- /**
609
- * Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
610
- * bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
611
- * one (the same normalization `state/bootstrap.ts` applies to the same
612
- * `/v1/projects` listing).
613
- */
614
- const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
615
- /**
616
- * Finds the app's Project by name in the workspace — PDP allows duplicate
617
- * project names, so more than one can match; the oldest wins. Creates one
618
- * if none match, unless `ensure` is `false` (find-only — `destroy`), in
619
- * which case an absent Project fails with `ContainerNotFoundError`. No
620
- * ownership marker and no `--project` override (both deferred — see
621
- * ADR-0019).
622
- */
623
- const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
624
- 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];
625
- if (oldest !== void 0) return oldest.id;
626
- if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
627
- return (yield* call(() => client.POST("/v1/projects", { body: {
628
- name: appName,
629
- workspaceId
630
- } }))).data.id;
631
- });
632
- const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
633
- path: { projectId },
634
- query: { gitName }
635
- } })).pipe(Effect.map((page) => page.data[0]?.id));
636
- /**
637
- * Finds the stage's Branch by its exact `gitName`, creating it if absent
638
- * unless `ensure` is `false` (find-only — `destroy`), in which case an
639
- * absent Branch fails with `ContainerNotFoundError`. The Management API has
640
- * no server-side "create-or-return" idempotency (`POST
641
- * /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
642
- * field to make that a no-op), so idempotency is client-side: observe
643
- * first, and on a racing 409 from create, re-observe rather than fail.
644
- */
645
- const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
646
- const existing = yield* findBranchId(client, projectId, gitName);
647
- if (existing !== void 0) return existing;
648
- if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
649
- appName,
650
- stage: gitName
651
- }));
652
- return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
653
- params: { path: { projectId } },
654
- body: { gitName }
655
- })).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)));
656
- });
657
- /**
658
- * Resolves the two containers a stage's deploy runs into (ADR-0019): the
659
- * app's **Project**, found-or-created by name, and — for a named stage
660
- * only — its **Branch**, found-or-created by `gitName`. The default stage
661
- * (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
662
- * false` (`destroy`), nothing is created — an absent Project or Branch
663
- * fails with `ContainerNotFoundError` instead.
664
- */
665
- const resolveContainer = (opts) => Effect.gen(function* () {
666
- const client = yield* ManagementClient;
667
- const ensure = opts.ensure ?? true;
668
- const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
669
- if (opts.stage === void 0) return { projectId };
670
- return {
671
- projectId,
672
- branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
673
- };
674
- });
675
- /**
676
- * Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
677
- * the Branch still has live members or is the production/default Branch —
678
- * that surfaces as a `PrismaApiError`.
679
- */
680
- const deleteBranch = (branchId) => Effect.gen(function* () {
681
- const client = yield* ManagementClient;
682
- yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
683
- });
684
- Provider.ProviderCollection()("Prisma");
611
+ return env;
612
+ }
685
613
  //#endregion
686
- //#region ../../0-framework/3-tooling/cli/dist/cli-BGF4efRK.mjs
614
+ //#region ../../0-framework/3-tooling/cli/dist/cli-riJGhp7G.mjs
687
615
  /**
688
616
  * A user-facing failure with a message that already names the fix (deploy-cli.md
689
617
  * § Error surface). `bin.ts` catches this — and any other Error, including
@@ -695,59 +623,6 @@ var CliError = class extends Error {
695
623
  this.name = "CliError";
696
624
  }
697
625
  };
698
- /**
699
- * Pipeline pre-stack step: resolves the app's Project + (named stage) Branch
700
- * via `@internal/lowering`'s `resolveContainer`, before the generated stack file
701
- * runs — `deploy` creates-if-absent, `destroy` finds only.
702
- */
703
- /** Validates `stage` as a git ref name via `git check-ref-format` — no silent normalization. */
704
- function validateStageName(stage) {
705
- const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
706
- if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
707
- 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}").`);
708
- }
709
- async function ensureContainers(input, deps) {
710
- const env = input.env ?? process.env;
711
- const workspaceId = env["PRISMA_WORKSPACE_ID"];
712
- if (workspaceId === void 0 || workspaceId.length === 0) throw new CliError("environment variable PRISMA_WORKSPACE_ID is required.");
713
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
714
- if (input.stage !== void 0) validateStageName(input.stage);
715
- const program = resolveContainer({
716
- workspaceId,
717
- appName: input.appName,
718
- ...input.stage !== void 0 ? { stage: input.stage } : {},
719
- ensure: input.command === "deploy"
720
- }).pipe(Effect.map((c) => ({
721
- ok: true,
722
- container: c
723
- })), Effect.catchTag("ContainerNotFoundError", (e) => Effect.succeed({
724
- ok: false,
725
- message: `Nothing deployed for ${e.appName}${e.stage ? `/${e.stage}` : ""} — deploy it first.`
726
- })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
727
- ok: false,
728
- message: `Prisma Management API error resolving containers: ${e.message}.`
729
- })));
730
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
731
- const outcome = await Effect.runPromise(provided);
732
- if (!outcome.ok) throw new CliError(outcome.message);
733
- return outcome.container;
734
- }
735
- /**
736
- * Soft-deletes a named stage's Branch after a successful `alchemy destroy`
737
- * has removed its members (spec §10) — the Management API refuses to delete
738
- * a Branch that still has live members.
739
- */
740
- async function deleteStageBranch(input, deps) {
741
- const env = input.env ?? process.env;
742
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
743
- const program = deleteBranch(input.branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
744
- ok: false,
745
- message: `Failed to delete the stage Branch: ${e.message}.`
746
- })));
747
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
748
- const outcome = await Effect.runPromise(provided);
749
- if (!outcome.ok) throw new CliError(outcome.message);
750
- }
751
626
  /** Pipeline step 6: writes a regenerated-every-run, independently runnable stack module at `.prisma-composer/alchemy.run.ts`. */
752
627
  const GENERATED_DIR = ".prisma-composer";
753
628
  const GENERATED_FILE = "alchemy.run.ts";
@@ -768,6 +643,7 @@ function renderOptions(input) {
768
643
  lines.push(" bundles: {");
769
644
  for (const [id, bundle] of Object.entries(input.assembled.bundles)) lines.push(` ${quote(id)}: ${renderBundle(bundle)},`);
770
645
  lines.push(" },");
646
+ lines.push(" report: deploymentReport,");
771
647
  return lines.join("\n");
772
648
  }
773
649
  /** Renders the stack module's source (tests assert on it without touching disk) — uses `//` headers, not a block comment, since a cwd path with a star-slash could close one early. */
@@ -782,6 +658,7 @@ function renderStackFile(input) {
782
658
  //
783
659
  // bisects a CLI bug from an Alchemy bug (deploy-cli.md § Implementation decisions).
784
660
  import { lower } from '@prisma/composer/deploy';
661
+ import { deploymentReport } from '@prisma/composer/report';
785
662
  import config from ${quote(configImport)};
786
663
  import app from ${quote(appImport)};
787
664
 
@@ -848,7 +725,8 @@ function validateConfigShape(loaded, configPath) {
848
725
  if (seen.has(id)) throw new CliError(`${CONFIG_FILENAME}: extension "${id}" is listed more than once in \`extensions\`.`);
849
726
  seen.add(id);
850
727
  }
851
- if (typeof loaded["state"] !== "function") throw fieldError("state", "must be a function returning the deploy state layer (e.g. () => prismaState())");
728
+ const state = loaded["state"];
729
+ if (!isRecord(state) || typeof state["extension"] !== "string" || typeof state["create"] !== "function") throw fieldError("state", "must be a state descriptor (e.g. prismaState())");
852
730
  return blindCast(loaded);
853
731
  }
854
732
  /**
@@ -873,6 +751,41 @@ async function loadAppConfig(configPath) {
873
751
  };
874
752
  }
875
753
  /**
754
+ * `loadEntry` imports a user's topology with the host runtime's own module
755
+ * loader — under node (the CLI's shebang runtime), that loader strips
756
+ * TypeScript types but has no JSX transform (ADR-0005: the framework
757
+ * doesn't bundle or transform the app's code; deploy loading is no
758
+ * exception). A `.tsx`/`.jsx` file anywhere in the topology's import graph
759
+ * dies with node's raw `ERR_UNKNOWN_FILE_EXTENSION`, which names the file
760
+ * but not the cause or the fix. This turns that one case into a CliError
761
+ * message that does — every other load failure (syntax errors, missing
762
+ * files, non-JSX unknown extensions) is untouched, re-thrown as-is.
763
+ */
764
+ const UNKNOWN_EXTENSION_CODE = "ERR_UNKNOWN_FILE_EXTENSION";
765
+ const JSX_EXTENSIONS = [".tsx", ".jsx"];
766
+ function errorCode(error) {
767
+ return "code" in error && typeof error.code === "string" ? error.code : void 0;
768
+ }
769
+ function unknownExtensionPath(error) {
770
+ if (!(error instanceof Error)) return void 0;
771
+ if (errorCode(error) !== UNKNOWN_EXTENSION_CODE) return void 0;
772
+ return /Unknown file extension "\.\w+" for (.+)$/.exec(error.message)?.[1];
773
+ }
774
+ /**
775
+ * Returns a CliError-ready message when `error` is node's
776
+ * `ERR_UNKNOWN_FILE_EXTENSION` for a `.tsx`/`.jsx` file — `undefined` for
777
+ * every other error (including unknown-extension failures for some other
778
+ * extension), so the caller can fall through to its normal rethrow.
779
+ */
780
+ function explainJsxLoadError(error, entryPath) {
781
+ const offendingPath = unknownExtensionPath(error);
782
+ if (offendingPath === void 0) return void 0;
783
+ if (!JSX_EXTENSIONS.some((ext) => offendingPath.endsWith(ext))) return void 0;
784
+ return `"${offendingPath}" can't be loaded: deploy loads "${entryPath}"'s topology with node's own module loader, which strips TypeScript types but has no JSX transform — so a .tsx/.jsx file can't sit anywhere in that import graph.
785
+
786
+ Precompile that file in your own build (transform the JSX away, keep the real npm packages external) and import the compiled output instead of the raw .tsx/.jsx — see examples/email/scripts/build.ts in the prisma/composer repo for a worked example.`;
787
+ }
788
+ /**
876
789
  * Pipeline step 1 (deploy-cli.md § The pipeline): import the entry module
877
790
  * (resolved against cwd) and require its default export to be a node — a
878
791
  * service or module, branded by core's factories. Whatever this module exports
@@ -880,7 +793,15 @@ async function loadAppConfig(configPath) {
880
793
  */
881
794
  async function loadEntry(entryArg, cwd) {
882
795
  const resolvedPath = path.resolve(cwd, entryArg);
883
- const root = (await import(pathToFileURL(resolvedPath).href)).default;
796
+ let mod;
797
+ try {
798
+ mod = await import(pathToFileURL(resolvedPath).href);
799
+ } catch (error) {
800
+ const explained = explainJsxLoadError(error, resolvedPath);
801
+ if (explained !== void 0) throw new CliError(explained);
802
+ throw error;
803
+ }
804
+ const root = blindCast(mod).default;
884
805
  if (!isNode(root) || root.kind === "dependency" || root.kind === "resource") throw new CliError(`Entry module "${resolvedPath}" must default-export a node (a service or a module) — construct it with service() or module() from @prisma/composer.`);
885
806
  return {
886
807
  path: resolvedPath,
@@ -907,7 +828,7 @@ function resolveAlchemyBin(startDir) {
907
828
  dir = parent;
908
829
  }
909
830
  }
910
- /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus the resolved Project/Branch ids. */
831
+ /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus every extension's resolved container. */
911
832
  function runAlchemy(input) {
912
833
  const bin = resolveAlchemyBin(input.cwd);
913
834
  const args = [
@@ -921,8 +842,7 @@ function runAlchemy(input) {
921
842
  stdio: "inherit",
922
843
  env: {
923
844
  ...input.env ?? process.env,
924
- PRISMA_PROJECT_ID: input.projectId,
925
- ...input.branchId !== void 0 ? { PRISMA_BRANCH_ID: input.branchId } : {}
845
+ ...input.containerEnv
926
846
  }
927
847
  });
928
848
  if (result.error !== void 0) throw result.error;
@@ -948,6 +868,12 @@ function validateRegistryCoverage(graph, config) {
948
868
  lookup(extensions, node.build.extension, node.build.type, "build", `service node "${id}"'s build descriptor`);
949
869
  }
950
870
  }
871
+ /** 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. */
872
+ function validateStageName(stage) {
873
+ const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
874
+ if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
875
+ 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}").`);
876
+ }
951
877
  /**
952
878
  * Argument parsing (clipanion — prisma-next's CLI idiom, see
953
879
  * prisma-next/packages/1-framework/3-tooling/cli/src/migration-cli.ts) +
@@ -1043,6 +969,7 @@ async function run(argv, deps = {}) {
1043
969
  throw error;
1044
970
  }
1045
971
  const stage = effectiveStage(args);
972
+ if (stage !== void 0) validateStageName(stage);
1046
973
  const cwd = process.cwd();
1047
974
  if (args.command === "destroy") warnIfNoLocalDeployState(cwd);
1048
975
  const resolvedEntryPath = path.resolve(cwd, args.entry);
@@ -1062,18 +989,32 @@ async function run(argv, deps = {}) {
1062
989
  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.`);
1063
990
  throw error;
1064
991
  }
1065
- const { projectId, branchId } = await (deps.ensureContainers ?? ensureContainers)({
1066
- command: args.command,
1067
- appName: name,
1068
- stage
1069
- });
992
+ const containers = /* @__PURE__ */ new Map();
993
+ for (const extension of config.extensions) {
994
+ if (extension.container === void 0) continue;
995
+ try {
996
+ if (args.command === "deploy") containers.set(extension.id, await extension.container.ensure({
997
+ appName: name,
998
+ stage
999
+ }));
1000
+ else {
1001
+ const instance = await extension.container.locate({
1002
+ appName: name,
1003
+ stage
1004
+ });
1005
+ if (instance === void 0) throw new CliError(`Nothing deployed for ${name}${stage !== void 0 ? `/${stage}` : ""} — deploy it first.`);
1006
+ containers.set(extension.id, instance);
1007
+ }
1008
+ } catch (error) {
1009
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1010
+ }
1011
+ }
1070
1012
  if (args.command === "deploy") for (const extension of config.extensions) {
1071
1013
  if (extension.preflight === void 0) continue;
1072
1014
  try {
1073
1015
  await extension.preflight({
1074
1016
  graph,
1075
- projectId,
1076
- branchId,
1017
+ container: containers.get(extension.id),
1077
1018
  stage
1078
1019
  });
1079
1020
  } catch (error) {
@@ -1093,15 +1034,36 @@ async function run(argv, deps = {}) {
1093
1034
  stackFileRelativePath: GENERATED_STACK_RELATIVE_PATH,
1094
1035
  cwd,
1095
1036
  stage,
1096
- projectId,
1097
- ...branchId !== void 0 ? { branchId } : {}
1037
+ containerEnv: containerEnv(containers)
1098
1038
  });
1099
1039
  if (status !== 0) {
1100
1040
  console.error(`\nGenerated stack file: ${stackPath}`);
1101
1041
  console.error(`Run \`alchemy ${args.command} ${GENERATED_STACK_RELATIVE_PATH} --yes\` from ${cwd} to reproduce this directly.`);
1102
1042
  return status;
1103
1043
  }
1104
- if (args.command === "destroy" && branchId !== void 0) await (deps.deleteBranch ?? ((input) => deleteStageBranch(input)))({ branchId });
1044
+ if (args.command === "destroy") {
1045
+ for (const extension of config.extensions) {
1046
+ if (extension.teardown === void 0) continue;
1047
+ try {
1048
+ await extension.teardown({
1049
+ container: containers.get(extension.id),
1050
+ stage
1051
+ });
1052
+ } catch (error) {
1053
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1054
+ }
1055
+ }
1056
+ for (const extension of config.extensions) {
1057
+ if (extension.container === void 0) continue;
1058
+ const instance = containers.get(extension.id);
1059
+ if (instance === void 0) continue;
1060
+ try {
1061
+ await extension.container.remove(instance);
1062
+ } catch (error) {
1063
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1064
+ }
1065
+ }
1066
+ }
1105
1067
  return status;
1106
1068
  } catch (error) {
1107
1069
  console.error(`\nGenerated stack file: ${stackPath}`);