@prisma/composer-prisma-cloud 0.30.0-dev.2 → 0.30.0-dev.4

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.
@@ -1,5 +1,5 @@
1
1
  import { t as blindCast$1 } from "./casts-DpaahrlC-Ykus3kCE.mjs";
2
- import { E as isWithin, b as mintKeyPair, c as PrismaCloudContainer, d as collectPreflightNames, h as prismaCloudContainerOf, i as OrmMigrationProvider, n as GeneratedParamProvider, p as deserialize, s as PgWarmProvider, u as S3CredentialsProvider, v as serviceAppName, w as ServiceKeyProvider, x as Providers } from "./s3-credentials-resource-BZjtuopJ-D0fKtjEH.mjs";
2
+ import { C as Providers, E as ServiceKeyProvider, O as isWithin, S as mintKeyPair, b as serviceAppName, c as PgWarmProvider, d as S3CredentialsProvider, f as collectPreflightNames, g as prismaCloudContainerOf, l as PrismaCloudContainer, m as deserialize, n as GeneratedParamProvider, r as LocalOrmMigrationProvider, y as resourceName } from "./s3-credentials-resource-D4jcpAZp-6qCQY8BD.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { blindCast } from "@prisma/composer/casts";
5
5
  import * as Effect from "effect/Effect";
@@ -1564,7 +1564,7 @@ var require_adapter = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1564
1564
  };
1565
1565
  }));
1566
1566
  //#endregion
1567
- //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/segments-BW2SYdBf.mjs
1567
+ //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/daemon-D9BV062Q.mjs
1568
1568
  var import_proper_lockfile = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
1569
1569
  const lockfile = require_lockfile();
1570
1570
  const { toPromise, toSync, toSyncOptions } = require_adapter();
@@ -2050,48 +2050,6 @@ async function ensureDaemon(name, entry, opts = {}) {
2050
2050
  await release();
2051
2051
  }
2052
2052
  }
2053
- /**
2054
- * API hygiene shared by both daemons (local-dev spec § 2): every `<app>`,
2055
- * `<id>`, and `<name>` path segment must match this shape or the request is
2056
- * a 400 naming the segment.
2057
- */
2058
- const SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/;
2059
- function isValidSegment(segment) {
2060
- return segment.length <= 63 && SEGMENT_RE.test(segment);
2061
- }
2062
- //#endregion
2063
- //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/instance-name-BF6J_weL.mjs
2064
- /**
2065
- * `pcdev-<app>-<database-id>` instance-name derivation (local-dev spec § 2
2066
- * `postgres-main.ts`): each half lowercased, every char outside `[a-z0-9]`
2067
- * replaced by `-`, runs collapsed, the combined name trimmed to 63 chars.
2068
- *
2069
- * A separate, side-effect-free module (not defined inline in
2070
- * `postgres-main.ts`) so it can be imported directly by tests without also
2071
- * running that file's own `main()` — `postgres-main.ts` is a daemon
2072
- * entrypoint script, always invoked as a subprocess, and calls `main()`
2073
- * unconditionally at module load.
2074
- */
2075
- /**
2076
- * Deliberately linear, no ambiguous quantifiers: a per-character replace
2077
- * (no `+`, so no run-length backtracking surface), a bounded-quantifier
2078
- * collapse (`{2,}`, not alternation-with-quantifiers), and plain
2079
- * index-walking for the leading/trailing trim instead of a regex — a
2080
- * combined `+`-plus-alternation trim (`/^-+|-+$/g`) is exactly the shape
2081
- * CodeQL's polynomial-ReDoS check flags, whether or not this particular
2082
- * instance is provably safe.
2083
- */
2084
- function slug(segment) {
2085
- const collapsed = segment.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-{2,}/g, "-");
2086
- let start = 0;
2087
- let end = collapsed.length;
2088
- while (start < end && collapsed[start] === "-") start++;
2089
- while (end > start && collapsed[end - 1] === "-") end--;
2090
- return collapsed.slice(start, end);
2091
- }
2092
- function instanceNameFor(app, id) {
2093
- return `pcdev-${slug(app)}-${slug(id)}`.slice(0, 63);
2094
- }
2095
2053
  //#endregion
2096
2054
  //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/index.mjs
2097
2055
  /**
@@ -2124,29 +2082,28 @@ function resolveBaseUrl(name, opts) {
2124
2082
  if (!isRegistryEntry(parsed) || !isPidAlive(parsed.pid)) throw notRunningError(name);
2125
2083
  return `http://127.0.0.1:${parsed.port}`;
2126
2084
  }
2127
- function encodeSegment(segment) {
2128
- if (!isValidSegment(segment)) throw new Error(`invalid path segment "${segment}"`);
2129
- return encodeURIComponent(segment);
2130
- }
2131
2085
  const ADMIN_FETCH_ATTEMPTS = 3;
2132
2086
  const ADMIN_FETCH_RETRY_DELAY_MS = 150;
2133
2087
  /**
2134
2088
  * `fetch` for the loopback admin calls, retried on transient socket errors:
2135
2089
  * a keep-alive reuse race (the runtime re-uses a pooled connection the
2136
2090
  * daemon's HTTP server just closed idle — surfaces as "fetch failed" /
2137
- * "other side closed") or a briefly overloaded daemon. Every admin call is
2138
- * idempotent by design, so a short blind retry is safe. A caller-driven
2139
- * abort is never retried. When every attempt fails to connect, it throws
2140
- * `DEV.EMULATOR_UNREACHABLE`; callers retry that code and nothing else.
2091
+ * "other side closed") or a briefly overloaded daemon. A short blind retry is
2092
+ * safe only for an idempotent call; a call that is not, such as creating an
2093
+ * instance, passes `retry: false`, because the emulator may have handled the
2094
+ * first attempt. A caller-driven abort is never retried. When every attempt
2095
+ * fails to connect, it throws `DEV.EMULATOR_UNREACHABLE`; callers retry that
2096
+ * code and nothing else.
2141
2097
  */
2142
- async function adminFetch(url, init) {
2098
+ async function adminFetch(url, init, opts = {}) {
2099
+ const attempts = opts.retry === false ? 1 : ADMIN_FETCH_ATTEMPTS;
2143
2100
  let lastError;
2144
- for (let attempt = 1; attempt <= ADMIN_FETCH_ATTEMPTS; attempt += 1) try {
2101
+ for (let attempt = 1; attempt <= attempts; attempt += 1) try {
2145
2102
  return await fetch(url, init);
2146
2103
  } catch (error) {
2147
2104
  if (init?.signal?.aborted === true) throw error;
2148
2105
  lastError = error;
2149
- if (attempt < ADMIN_FETCH_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, ADMIN_FETCH_RETRY_DELAY_MS));
2106
+ if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, ADMIN_FETCH_RETRY_DELAY_MS));
2150
2107
  }
2151
2108
  throw new CliStructuredError("DEV.EMULATOR_UNREACHABLE", `Could not connect to the local dev emulator at ${new URL(url).origin}: ${lastError instanceof Error ? lastError.message : String(lastError)}`, {
2152
2109
  why: "The emulator is registered and its process is alive, but it did not accept the connection.",
@@ -2162,23 +2119,33 @@ async function expectOk(res) {
2162
2119
  }
2163
2120
  return res;
2164
2121
  }
2165
- const SERVICE_STATUSES = /* @__PURE__ */ new Set([
2122
+ const INSTANCE_STATUSES = /* @__PURE__ */ new Set([
2166
2123
  "running",
2167
2124
  "backoff",
2168
2125
  "held",
2169
2126
  "stopped"
2170
2127
  ]);
2171
- function isServiceInfo(value) {
2172
- return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "address" in value && typeof value.address === "string" && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && typeof value.status === "string" && SERVICE_STATUSES.has(value.status) && "logPath" in value && typeof value.logPath === "string";
2128
+ function isStringRecord$1(value) {
2129
+ return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
2173
2130
  }
2174
- function isServiceInfoArray(value) {
2175
- return Array.isArray(value) && value.every(isServiceInfo);
2131
+ function isServiceInstanceInfo(value) {
2132
+ return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && typeof value.status === "string" && INSTANCE_STATUSES.has(value.status) && "logPath" in value && typeof value.logPath === "string";
2133
+ }
2134
+ function isServiceInstanceInfoArray(value) {
2135
+ return Array.isArray(value) && value.every(isServiceInstanceInfo);
2176
2136
  }
2177
2137
  function isHealthBody(value) {
2178
2138
  return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
2179
2139
  }
2180
- function isServiceReservation(value) {
2181
- return typeof value === "object" && value !== null && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string";
2140
+ function instancePath(baseUrl, id) {
2141
+ return `${baseUrl}/instances/${encodeURIComponent(id)}`;
2142
+ }
2143
+ function postJson(body) {
2144
+ return {
2145
+ method: "POST",
2146
+ headers: { "content-type": "application/json" },
2147
+ body: JSON.stringify(body)
2148
+ };
2182
2149
  }
2183
2150
  function computeClient(opts = {}) {
2184
2151
  const baseUrl = resolveBaseUrl("compute", opts);
@@ -2189,26 +2156,33 @@ function computeClient(opts = {}) {
2189
2156
  if (!isHealthBody(body)) throw new Error("malformed /health response from the compute emulator");
2190
2157
  return body;
2191
2158
  },
2192
- async ensureService(app, id) {
2193
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}`, { method: "PUT" }))).json();
2194
- if (!isServiceReservation(body)) throw new Error("malformed service-reservation response from the compute emulator");
2159
+ async createInstance(request) {
2160
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
2161
+ if (!isServiceInstanceInfo(body)) throw new Error("malformed instance-create response from the compute emulator");
2162
+ return body;
2163
+ },
2164
+ async listInstances() {
2165
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
2166
+ if (!isServiceInstanceInfoArray(body)) throw new Error("malformed instances listing from the compute emulator");
2195
2167
  return body;
2196
2168
  },
2197
- async putDeployment(app, id, deployment) {
2198
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}/deployment`, {
2169
+ async getInstance(id) {
2170
+ const res = await adminFetch(instancePath(baseUrl, id));
2171
+ if (res.status === 404) return void 0;
2172
+ const body = await (await expectOk(res)).json();
2173
+ if (!isServiceInstanceInfo(body)) throw new Error("malformed instance response from the compute emulator");
2174
+ return body;
2175
+ },
2176
+ async putDeployment(id, deployment) {
2177
+ await expectOk(await adminFetch(`${instancePath(baseUrl, id)}/deployment`, {
2199
2178
  method: "PUT",
2200
2179
  headers: { "content-type": "application/json" },
2201
2180
  body: JSON.stringify(deployment)
2202
2181
  }));
2203
2182
  },
2204
- async listServices(app) {
2205
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services`))).json();
2206
- if (!isServiceInfoArray(body)) throw new Error("malformed services listing from the compute emulator");
2207
- return body;
2208
- },
2209
- async *followLogs(app, id, signal, opts) {
2183
+ async *followLogs(id, signal, opts) {
2210
2184
  const tail = opts?.tail !== void 0 ? `&tail=${String(Math.max(0, Math.trunc(opts.tail)))}` : "";
2211
- const body = (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/services/${encodeSegment(id)}/logs?follow=1${tail}`, signal ? { signal } : void 0))).body;
2185
+ const body = (await expectOk(await adminFetch(`${instancePath(baseUrl, id)}/logs?follow=1${tail}`, signal ? { signal } : void 0))).body;
2212
2186
  if (!body) return;
2213
2187
  const reader = body.getReader();
2214
2188
  const decoder = new TextDecoder();
@@ -2222,19 +2196,26 @@ function computeClient(opts = {}) {
2222
2196
  await reader.cancel().catch(() => void 0);
2223
2197
  }
2224
2198
  },
2225
- async stopApp(app) {
2226
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/stop`, { method: "POST" }));
2199
+ async startInstances(ids) {
2200
+ await expectOk(await adminFetch(`${baseUrl}/instances/start`, postJson({ ids })));
2227
2201
  },
2228
- async startApp(app) {
2229
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/start`, { method: "POST" }));
2202
+ async stopInstances(ids) {
2203
+ await expectOk(await adminFetch(`${baseUrl}/instances/stop`, postJson({ ids })));
2230
2204
  },
2231
- async deleteApp(app) {
2232
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}`, { method: "DELETE" }));
2205
+ async deleteInstance(id) {
2206
+ await expectOk(await adminFetch(instancePath(baseUrl, id), { method: "DELETE" }));
2233
2207
  }
2234
2208
  };
2235
2209
  }
2210
+ function isBucketInstanceInfo(value) {
2211
+ return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "dir" in value && typeof value.dir === "string";
2212
+ }
2213
+ function isBucketInstanceInfoArray(value) {
2214
+ return Array.isArray(value) && value.every(isBucketInstanceInfo);
2215
+ }
2236
2216
  function bucketsClient(opts = {}) {
2237
2217
  const baseUrl = resolveBaseUrl("buckets", opts);
2218
+ const adminUrl = `${baseUrl}/_pcdev/instances`;
2238
2219
  return {
2239
2220
  baseUrl,
2240
2221
  async health() {
@@ -2242,15 +2223,25 @@ function bucketsClient(opts = {}) {
2242
2223
  if (!isHealthBody(body)) throw new Error("malformed /_pcdev/health response from the buckets emulator");
2243
2224
  return body;
2244
2225
  },
2245
- async putBucket(app, name, dir) {
2246
- await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}/buckets/${encodeSegment(name)}`, {
2247
- method: "PUT",
2248
- headers: { "content-type": "application/json" },
2249
- body: JSON.stringify({ dir })
2250
- }));
2226
+ async createInstance(request) {
2227
+ const body = await (await expectOk(await adminFetch(adminUrl, postJson(request), { retry: false }))).json();
2228
+ if (!isBucketInstanceInfo(body)) throw new Error("malformed instance-create response from the buckets emulator");
2229
+ return body;
2230
+ },
2231
+ async listInstances() {
2232
+ const body = await (await expectOk(await adminFetch(adminUrl))).json();
2233
+ if (!isBucketInstanceInfoArray(body)) throw new Error("malformed instances listing from the buckets emulator");
2234
+ return body;
2251
2235
  },
2252
- async putCredentials(app, accessKeyId, secretAccessKey) {
2253
- await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}/credentials`, {
2236
+ async getInstance(id) {
2237
+ const res = await adminFetch(instancePath(`${baseUrl}/_pcdev`, id));
2238
+ if (res.status === 404) return void 0;
2239
+ const body = await (await expectOk(res)).json();
2240
+ if (!isBucketInstanceInfo(body)) throw new Error("malformed instance response from the buckets emulator");
2241
+ return body;
2242
+ },
2243
+ async putCredentials(id, accessKeyId, secretAccessKey) {
2244
+ await expectOk(await adminFetch(`${instancePath(`${baseUrl}/_pcdev`, id)}/credentials`, {
2254
2245
  method: "PUT",
2255
2246
  headers: { "content-type": "application/json" },
2256
2247
  body: JSON.stringify({
@@ -2259,19 +2250,16 @@ function bucketsClient(opts = {}) {
2259
2250
  })
2260
2251
  }));
2261
2252
  },
2262
- async deleteApp(app) {
2263
- await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}`, { method: "DELETE" }));
2253
+ async deleteInstance(id) {
2254
+ await expectOk(await adminFetch(instancePath(`${baseUrl}/_pcdev`, id), { method: "DELETE" }));
2264
2255
  }
2265
2256
  };
2266
2257
  }
2267
- function isDatabaseInfo(value) {
2268
- return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "url" in value && typeof value.url === "string" && "instanceName" in value && typeof value.instanceName === "string" && "databasePort" in value && typeof value.databasePort === "number";
2269
- }
2270
- function isDatabaseInfoArray(value) {
2271
- return Array.isArray(value) && value.every(isDatabaseInfo);
2258
+ function isDatabaseInstanceInfo(value) {
2259
+ return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "labels" in value && isStringRecord$1(value.labels) && "port" in value && typeof value.port === "number" && "url" in value && typeof value.url === "string" && "status" in value && (value.status === "running" || value.status === "stopped");
2272
2260
  }
2273
- function isEnsuredDatabase(value) {
2274
- return typeof value === "object" && value !== null && "url" in value && typeof value.url === "string";
2261
+ function isDatabaseInstanceInfoArray(value) {
2262
+ return Array.isArray(value) && value.every(isDatabaseInstanceInfo);
2275
2263
  }
2276
2264
  function postgresClient(opts = {}) {
2277
2265
  const baseUrl = resolveBaseUrl("postgres", opts);
@@ -2282,22 +2270,40 @@ function postgresClient(opts = {}) {
2282
2270
  if (!isHealthBody(body)) throw new Error("malformed /health response from the postgres emulator");
2283
2271
  return body;
2284
2272
  },
2285
- async ensureDatabase(app, id, prismaDevModulePath) {
2286
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/databases/${encodeSegment(id)}`, {
2287
- method: "PUT",
2288
- headers: { "content-type": "application/json" },
2289
- body: JSON.stringify({ prismaDevModulePath })
2290
- }))).json();
2291
- if (!isEnsuredDatabase(body)) throw new Error("malformed database-ensure response from the postgres emulator");
2273
+ async createInstance(request) {
2274
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
2275
+ if (!isDatabaseInstanceInfo(body)) throw new Error("malformed instance-create response from the postgres emulator");
2292
2276
  return body;
2293
2277
  },
2294
- async listDatabases(app) {
2295
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/databases`))).json();
2296
- if (!isDatabaseInfoArray(body)) throw new Error("malformed databases listing from the postgres emulator");
2278
+ async listInstances() {
2279
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
2280
+ if (!isDatabaseInstanceInfoArray(body)) throw new Error("malformed instances listing from the postgres emulator");
2297
2281
  return body;
2298
2282
  },
2299
- async deleteApp(app) {
2300
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}`, { method: "DELETE" }));
2283
+ async getInstance(id) {
2284
+ const res = await adminFetch(instancePath(baseUrl, id));
2285
+ if (res.status === 404) return void 0;
2286
+ const body = await (await expectOk(res)).json();
2287
+ if (!isDatabaseInstanceInfo(body)) throw new Error("malformed instance response from the postgres emulator");
2288
+ return body;
2289
+ },
2290
+ async startInstances(ids, prismaDevModulePath) {
2291
+ await expectOk(await adminFetch(`${baseUrl}/instances/start`, postJson({
2292
+ ids,
2293
+ prismaDevModulePath
2294
+ })));
2295
+ },
2296
+ async stopInstances(ids) {
2297
+ await expectOk(await adminFetch(`${baseUrl}/instances/stop`, postJson({ ids })));
2298
+ },
2299
+ async deleteInstance(id, prismaDevModulePath) {
2300
+ await expectOk(await adminFetch(instancePath(baseUrl, id), {
2301
+ method: "DELETE",
2302
+ ...prismaDevModulePath !== void 0 ? {
2303
+ headers: { "content-type": "application/json" },
2304
+ body: JSON.stringify({ prismaDevModulePath })
2305
+ } : {}
2306
+ }));
2301
2307
  }
2302
2308
  };
2303
2309
  }
@@ -5618,6 +5624,386 @@ function appNameOf(container) {
5618
5624
  return container.input.appName;
5619
5625
  }
5620
5626
  /**
5627
+ * The shared dev-instance store (local-dev spec § 4): the JSON files under
5628
+ * `<devDir>/` every local provider reads or writes. Every write is
5629
+ * temp-then-rename; reads and writes on the SAME file are additionally
5630
+ * serialized behind one in-process async queue per absolute path, because the
5631
+ * local providers reconcile concurrently inside the one alchemy child and a
5632
+ * naive read-modify-write would drop a concurrent writer's key. Postgres
5633
+ * instance state is NOT here (REVISED — operator review of #162): the
5634
+ * `postgres-main` daemon owns it now.
5635
+ */
5636
+ function isEnoent(err) {
5637
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
5638
+ }
5639
+ const queues = /* @__PURE__ */ new Map();
5640
+ function enqueue(filePath, task) {
5641
+ const result = (queues.get(filePath) ?? Promise.resolve()).then(() => void 0, () => void 0).then(task);
5642
+ queues.set(filePath, result.then(() => void 0, () => void 0));
5643
+ return result;
5644
+ }
5645
+ async function readJson(filePath, isValid, empty) {
5646
+ let raw;
5647
+ try {
5648
+ raw = await fs$1$1.readFile(filePath, "utf8");
5649
+ } catch (err) {
5650
+ if (isEnoent(err)) return empty;
5651
+ throw err;
5652
+ }
5653
+ let parsed;
5654
+ try {
5655
+ parsed = JSON.parse(raw);
5656
+ } catch {
5657
+ return empty;
5658
+ }
5659
+ return isValid(parsed) ? parsed : empty;
5660
+ }
5661
+ async function writeJsonAtomic(filePath, value, mode) {
5662
+ const dir = path$1.dirname(filePath);
5663
+ await fs$1$1.mkdir(dir, { recursive: true });
5664
+ const tmpPath = path$1.join(dir, `.${path$1.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
5665
+ const json = `${JSON.stringify(value, null, 2)}\n`;
5666
+ await fs$1$1.writeFile(tmpPath, json, mode !== void 0 ? { mode } : void 0);
5667
+ await fs$1$1.rename(tmpPath, filePath);
5668
+ if (mode !== void 0) await fs$1$1.chmod(filePath, mode);
5669
+ }
5670
+ function createStore(filePath, isValid, empty, mode) {
5671
+ return {
5672
+ read: () => enqueue(filePath, () => readJson(filePath, isValid, empty)),
5673
+ update: (mutate) => enqueue(filePath, async () => {
5674
+ const next = mutate(await readJson(filePath, isValid, empty));
5675
+ await writeJsonAtomic(filePath, next, mode);
5676
+ return next;
5677
+ })
5678
+ };
5679
+ }
5680
+ function isStringRecord(value) {
5681
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
5682
+ }
5683
+ /** `<devDir>/env.json` — every `EnvironmentVariable` row, key → value. Last write wins. */
5684
+ function envStore(devDir) {
5685
+ return createStore(path$1.join(devDir, "env.json"), isStringRecord, {});
5686
+ }
5687
+ /** `<devDir>/secrets.json` — platform var name → value (shell-sourced or minted placeholder). Mode 0o600. */
5688
+ function secretsStore(devDir) {
5689
+ return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
5690
+ }
5691
+ /** Where local dev loads `@prisma/dev` from, for the Postgres emulator. */
5692
+ function noPrismaDevError(cause) {
5693
+ return /* @__PURE__ */ new Error(`local dev needs @prisma/dev for its local Postgres emulator — @prisma/composer-prisma-cloud declares it as a dependency, but it did not resolve from Composer's own installation; reinstall your dependencies. (${cause instanceof Error ? cause.message : String(cause)})`);
5694
+ }
5695
+ /**
5696
+ * Composer owns the local Postgres emulator's version: `@prisma/dev` is a
5697
+ * dependency of `@prisma/composer-prisma-cloud` and is resolved from
5698
+ * Composer's own package, never from the app. The app's copy (if any) is
5699
+ * ignored on purpose — an old `@prisma/dev` pulled in transitively (alchemy
5700
+ * pins `^0.20.0`, whose pglite-socket crashes on any message over 64 KiB)
5701
+ * would otherwise silently replace the version Composer tested against.
5702
+ * The daemon imports the returned path dynamically.
5703
+ */
5704
+ function resolvePrismaDevModulePath() {
5705
+ try {
5706
+ return createRequire(import.meta.url).resolve("@prisma/dev");
5707
+ } catch (cause) {
5708
+ throw noPrismaDevError(cause);
5709
+ }
5710
+ }
5711
+ /**
5712
+ * Which emulator instance backs each node of each app run from a directory.
5713
+ * `prisma dev` records it in `<directory>/.prisma-composer/dev/`, and every
5714
+ * Composer operation on an emulator instance goes through it. The emulators
5715
+ * know nothing about Composer: the labels written on create are for people
5716
+ * reading an emulator listing, and nothing here reads them back.
5717
+ */
5718
+ const EMULATOR_INSTANCES_FILE = "emulator-instances.json";
5719
+ function isNodeInstance(value) {
5720
+ if (typeof value !== "object" || value === null) return false;
5721
+ if (!("id" in value) || typeof value.id !== "string" || !("kind" in value)) return false;
5722
+ if (value.kind === "bucket") return true;
5723
+ return (value.kind === "service" || value.kind === "database") && "port" in value && typeof value.port === "number";
5724
+ }
5725
+ function isNodes(value) {
5726
+ return typeof value === "object" && value !== null && Object.values(value).every(isNodeInstance);
5727
+ }
5728
+ function isAppInstances(value) {
5729
+ return typeof value === "object" && value !== null && "nodes" in value && isNodes(value.nodes);
5730
+ }
5731
+ function isEmulatorInstances(value) {
5732
+ return typeof value === "object" && value !== null && "current" in value && typeof value.current === "string" && "apps" in value && typeof value.apps === "object" && value.apps !== null && Object.values(value.apps).every(isAppInstances);
5733
+ }
5734
+ function isEarlierServiceNode(value) {
5735
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "compute" && "id" in value && typeof value.id === "string" && "port" in value && typeof value.port === "number";
5736
+ }
5737
+ function isSingleAppRecord(value) {
5738
+ return typeof value === "object" && value !== null && "app" in value && typeof value.app === "string" && "nodes" in value && typeof value.nodes === "object" && value.nodes !== null && Object.values(value.nodes).every(isEarlierServiceNode);
5739
+ }
5740
+ function isStoredRecord(value) {
5741
+ return isEmulatorInstances(value) || isSingleAppRecord(value);
5742
+ }
5743
+ /** A single-app record reads as a record holding that one app; the next write rewrites it. */
5744
+ function normalized(stored) {
5745
+ if (stored === void 0 || isEmulatorInstances(stored)) return stored;
5746
+ const nodes = Object.fromEntries(Object.entries(stored.nodes).map(([address, { id, port }]) => [address, {
5747
+ kind: "service",
5748
+ id,
5749
+ port
5750
+ }]));
5751
+ return {
5752
+ current: stored.app,
5753
+ apps: { [stored.app]: { nodes } }
5754
+ };
5755
+ }
5756
+ /**
5757
+ * A database's or bucket's key in the record: its resource `name`, which the
5758
+ * lowering sets from the same helper the emulators hook keys it by. A
5759
+ * resource without a name cannot be matched to the instance the hook
5760
+ * recorded, so it is refused rather than recorded under another key.
5761
+ */
5762
+ function recordKeyOf(kind, resourceId, name) {
5763
+ if (name === void 0) throw new Error(`local dev: the ${kind} resource "${resourceId}" has no name, so its emulator instance cannot be found in the directory's record.`);
5764
+ return name;
5765
+ }
5766
+ /** The project directory that holds `devDir` (`<directory>/.prisma-composer/dev`). */
5767
+ function directoryOf(devDir) {
5768
+ const normalizedDir = path$1.normalize(devDir);
5769
+ return normalizedDir.endsWith(DEV_DIR) ? normalizedDir.slice(0, -DEV_DIR.length - 1) : normalizedDir;
5770
+ }
5771
+ function store(devDir) {
5772
+ return createStore(path$1.join(devDir, EMULATOR_INSTANCES_FILE), isStoredRecord, void 0);
5773
+ }
5774
+ async function readRecord(devDir) {
5775
+ return normalized(await store(devDir).read());
5776
+ }
5777
+ async function updateRecord(devDir, mutate) {
5778
+ await store(devDir).update((stored) => mutate(normalized(stored)));
5779
+ }
5780
+ async function appNodes$1(scope) {
5781
+ return (await readRecord(scope.devDir))?.apps[scope.app]?.nodes ?? {};
5782
+ }
5783
+ async function updateNodes(scope, mutate) {
5784
+ await updateRecord(scope.devDir, (record) => {
5785
+ const nodes = { ...record?.apps[scope.app]?.nodes ?? {} };
5786
+ mutate(nodes);
5787
+ return {
5788
+ current: record?.current ?? scope.app,
5789
+ apps: {
5790
+ ...record?.apps,
5791
+ [scope.app]: { nodes }
5792
+ }
5793
+ };
5794
+ });
5795
+ }
5796
+ function labelsFor(scope, address) {
5797
+ return {
5798
+ "composer.directory": directoryOf(scope.devDir),
5799
+ "composer.app": scope.app,
5800
+ "composer.address": address
5801
+ };
5802
+ }
5803
+ function idsOf(nodes, kind) {
5804
+ return Object.values(nodes).flatMap((node) => node.kind === kind ? [node.id] : []);
5805
+ }
5806
+ /**
5807
+ * Stops the service processes and database servers of `nodes`, keeping
5808
+ * their data and ports. A kind whose emulator is not running has nothing
5809
+ * running to stop.
5810
+ */
5811
+ async function stopNodes(nodes) {
5812
+ const services = idsOf(nodes, "service");
5813
+ const databases = idsOf(nodes, "database");
5814
+ const failure = (await Promise.allSettled([services.length > 0 ? whenRunning(() => computeClient().stopInstances(services)) : void 0, databases.length > 0 ? whenRunning(() => postgresClient().stopInstances(databases)) : void 0])).find((result) => result.status === "rejected");
5815
+ if (failure) throw failure.reason;
5816
+ }
5817
+ /** Runs `action` against an emulator, treating "the emulator is not running" as nothing to do. */
5818
+ async function whenRunning(action) {
5819
+ let run;
5820
+ try {
5821
+ run = action();
5822
+ } catch {
5823
+ return;
5824
+ }
5825
+ await run;
5826
+ }
5827
+ /**
5828
+ * Makes `scope.app` the directory's current app. When another app ran here
5829
+ * last, its service processes and database servers are stopped and its
5830
+ * instances stay recorded, so running it again resumes them with their data
5831
+ * and ports. Returns the previous app's name when it changed.
5832
+ */
5833
+ async function selectApp(scope) {
5834
+ const record = await readRecord(scope.devDir);
5835
+ const previous = record?.current;
5836
+ if (previous !== void 0 && previous !== scope.app) await stopNodes(record?.apps[previous]?.nodes ?? {});
5837
+ await updateRecord(scope.devDir, (latest) => ({
5838
+ current: scope.app,
5839
+ apps: {
5840
+ ...latest?.apps,
5841
+ [scope.app]: latest?.apps[scope.app] ?? { nodes: {} }
5842
+ }
5843
+ }));
5844
+ return previous !== void 0 && previous !== scope.app ? previous : void 0;
5845
+ }
5846
+ /** Each node of the app of `kind`, with the instance that backs it. */
5847
+ async function nodeInstancesOf(scope, kind) {
5848
+ return Object.entries(await appNodes$1(scope)).flatMap(([address, instance]) => isOfKind(instance, kind) ? [{
5849
+ address,
5850
+ instance
5851
+ }] : []);
5852
+ }
5853
+ function isOfKind(instance, kind) {
5854
+ return instance.kind === kind;
5855
+ }
5856
+ /** The instance of `kind` recorded for `address`; undefined when there is none. */
5857
+ async function recordedInstance(scope, address, kind) {
5858
+ const recorded = (await appNodes$1(scope))[address];
5859
+ return recorded !== void 0 && isOfKind(recorded, kind) ? recorded : void 0;
5860
+ }
5861
+ /**
5862
+ * The instance that runs the service at `address`. The recorded instance is
5863
+ * reused while the Compute emulator still has it. Otherwise a new one is
5864
+ * created, requesting the recorded port, and recorded.
5865
+ */
5866
+ async function ensureServiceInstance(scope, address) {
5867
+ const client = computeClient();
5868
+ const recorded = await recordedInstance(scope, address, "service");
5869
+ if (recorded && await client.getInstance(recorded.id)) return { instance: recorded };
5870
+ const created = await client.createInstance({
5871
+ labels: labelsFor(scope, address),
5872
+ ...recorded ? { port: recorded.port } : {}
5873
+ });
5874
+ const instance = {
5875
+ kind: "service",
5876
+ id: created.id,
5877
+ port: created.port
5878
+ };
5879
+ await updateNodes(scope, (nodes) => {
5880
+ nodes[address] = instance;
5881
+ });
5882
+ return {
5883
+ instance,
5884
+ ...portChangeOf("service", address, recorded?.port, created.port)
5885
+ };
5886
+ }
5887
+ function portChangeOf(kind, address, previous, given) {
5888
+ return previous !== void 0 && previous !== given ? { portChange: {
5889
+ kind,
5890
+ address,
5891
+ previous,
5892
+ given
5893
+ } } : {};
5894
+ }
5895
+ /**
5896
+ * The running database for `address`. A recorded database the Postgres
5897
+ * emulator still has is started if it is stopped. Otherwise a new one is
5898
+ * created, requesting the recorded port, and recorded; it starts empty.
5899
+ */
5900
+ async function ensureDatabaseInstance(scope, address, prismaDevModulePath) {
5901
+ const client = postgresClient();
5902
+ const recorded = await recordedInstance(scope, address, "database");
5903
+ const existing = recorded ? await client.getInstance(recorded.id) : void 0;
5904
+ if (existing?.status === "running") return { instance: existing };
5905
+ if (existing) {
5906
+ await client.startInstances([existing.id], prismaDevModulePath);
5907
+ return { instance: await client.getInstance(existing.id) ?? existing };
5908
+ }
5909
+ const created = await client.createInstance({
5910
+ labels: labelsFor(scope, address),
5911
+ ...recorded ? { port: recorded.port } : {},
5912
+ prismaDevModulePath
5913
+ });
5914
+ await updateNodes(scope, (nodes) => {
5915
+ nodes[address] = {
5916
+ kind: "database",
5917
+ id: created.id,
5918
+ port: created.port
5919
+ };
5920
+ });
5921
+ return {
5922
+ instance: created,
5923
+ ...portChangeOf("database", address, recorded?.port, created.port)
5924
+ };
5925
+ }
5926
+ /**
5927
+ * The bucket for `address`. The recorded bucket is reused while the bucket
5928
+ * emulator still has it. Otherwise a new one is created over `dir` and
5929
+ * recorded; the objects already in `dir` are its objects.
5930
+ */
5931
+ async function ensureBucketInstance(scope, address, dir) {
5932
+ const client = bucketsClient();
5933
+ const recorded = await recordedInstance(scope, address, "bucket");
5934
+ const existing = recorded ? await client.getInstance(recorded.id) : void 0;
5935
+ if (existing) return existing;
5936
+ const created = await client.createInstance({
5937
+ labels: labelsFor(scope, address),
5938
+ dir
5939
+ });
5940
+ await updateNodes(scope, (nodes) => {
5941
+ nodes[address] = {
5942
+ kind: "bucket",
5943
+ id: created.id
5944
+ };
5945
+ });
5946
+ return created;
5947
+ }
5948
+ /** The Compute emulator's view of the instance recorded for `address`; undefined when either is missing. */
5949
+ async function liveServiceInstance(scope, address) {
5950
+ const recorded = await recordedInstance(scope, address, "service");
5951
+ return recorded ? computeClient().getInstance(recorded.id) : void 0;
5952
+ }
5953
+ /** The id recorded for `address`; undefined when there is none. */
5954
+ async function recordedInstanceId(scope, address) {
5955
+ return (await appNodes$1(scope))[address]?.id;
5956
+ }
5957
+ async function deleteInstance(instance) {
5958
+ if (instance.kind === "service") await computeClient().deleteInstance(instance.id);
5959
+ else if (instance.kind === "database") await postgresClient().deleteInstance(instance.id, resolvePrismaDevModulePath());
5960
+ else await bucketsClient().deleteInstance(instance.id);
5961
+ }
5962
+ /** Deletes the instance recorded for `address` and forgets it. */
5963
+ async function deleteNodeInstance(scope, address) {
5964
+ const recorded = (await appNodes$1(scope))[address];
5965
+ if (!recorded) return;
5966
+ await deleteInstance(recorded);
5967
+ await updateNodes(scope, (nodes) => {
5968
+ delete nodes[address];
5969
+ });
5970
+ }
5971
+ /** The kinds of instance recorded for the app, so a caller can start their emulators first. */
5972
+ async function recordedKinds(scope) {
5973
+ return new Set(Object.values(await appNodes$1(scope)).map((node) => node.kind));
5974
+ }
5975
+ /**
5976
+ * `--fresh`: deletes the app's recorded instances of every kind and its
5977
+ * entry. Other apps' entries are left alone. A bucket's objects live in the
5978
+ * dev directory, which `--fresh` removes separately.
5979
+ */
5980
+ async function deleteAppInstances(scope) {
5981
+ for (const instance of Object.values(await appNodes$1(scope))) await deleteInstance(instance);
5982
+ await updateRecord(scope.devDir, (record) => {
5983
+ const apps = { ...record?.apps };
5984
+ delete apps[scope.app];
5985
+ return {
5986
+ current: record?.current ?? scope.app,
5987
+ apps
5988
+ };
5989
+ });
5990
+ }
5991
+ /** When `prisma dev` ends: stops the app's service processes and database servers, keeping their data and ports. */
5992
+ async function stopAppInstances(scope) {
5993
+ await stopNodes(await appNodes$1(scope));
5994
+ }
5995
+ /**
5996
+ * The emulators' failure messages name instances by id; `prisma dev` names
5997
+ * services by address. Rewrites a plain `Error`'s message and leaves any
5998
+ * other error, such as a structured one, untouched.
5999
+ */
6000
+ function withServiceAddresses(error, nodes) {
6001
+ if (!(error instanceof Error) || error.constructor !== Error) return error;
6002
+ let message = error.message.replace("deploy it with listens: false", "declare it with compute({ listens: false })");
6003
+ for (const { address, instance } of nodes) message = message.replaceAll(`instance "${instance.id}"`, `service "${address}"`);
6004
+ return message === error.message ? error : new Error(message, { cause: error });
6005
+ }
6006
+ /**
5621
6007
  * Shared helpers for the upstream-attribute records every local provider
5622
6008
  * emits — one implementation, so the compute and postgres families cannot
5623
6009
  * drift on the `'local'` project fallback or the timestamp.
@@ -5636,26 +6022,38 @@ function projectIdOfInput(value) {
5636
6022
  * already up by the time these run, since the extension's
5637
6023
  * `localTarget.emulators` hook ensures it before converge.
5638
6024
  */
5639
- /** Upstream lets `name` default to the resource's logical ID; locally a bucket must have a concrete name to register under. */
5640
- const bucketNameOf = (news, id) => news.name ?? id;
5641
6025
  /** Reads a bucket id from upstream's `bucket` input: a plain string or a resolved `Prisma.Bucket` attributes record. */
5642
6026
  function bucketIdOfInput(value) {
5643
6027
  if (typeof value === "string") return value;
5644
6028
  if (Predicate.isObject(value) && typeof value["bucketId"] === "string") return value["bucketId"];
5645
6029
  return "local";
5646
6030
  }
5647
- /** `Bucket` → registers `<app>--<name>` with the bucket emulator, backed by an in-project data root. */
6031
+ const BUCKETS_DIR = "buckets";
6032
+ /** Where a bucket's objects live: in the project's dev directory, under its app, so `--fresh` removes them with the app. */
6033
+ function bucketDataDir(scope, name) {
6034
+ return path$1.join(scope.devDir, BUCKETS_DIR, scope.app, name);
6035
+ }
6036
+ function appScopeOf$2(input) {
6037
+ return {
6038
+ devDir: input.devDir,
6039
+ app: appNameOf(input.container)
6040
+ };
6041
+ }
6042
+ /**
6043
+ * `Bucket` → the bucket instance the directory's record names for the
6044
+ * resource, created when there is none, over an in-project data root. Its
6045
+ * `bucketId` is the instance id, which is also its S3 bucket name. A Bucket
6046
+ * whose recorded instance changed is updated, so its access key is
6047
+ * registered on the new instance.
6048
+ */
5648
6049
  function LocalBucketProvider(input) {
5649
6050
  return Provider.effect(Bucket, Effect.succeed({
5650
6051
  list: () => Effect.succeed([]),
5651
6052
  reconcile: ({ news, id }) => Effect.tryPromise({
5652
6053
  try: async () => {
5653
- const app = appNameOf(input.container);
5654
- const name = bucketNameOf(news, id);
5655
- const dir = path$1.join(input.devDir, "buckets", name);
5656
- await bucketsClient().putBucket(app, name, dir);
6054
+ const name = recordKeyOf("bucket", id, news.name);
5657
6055
  return {
5658
- bucketId: name,
6056
+ bucketId: (await ensureBucketInstance(appScopeOf$2(input), name, bucketDataDir(appScopeOf$2(input), name))).id,
5659
6057
  name,
5660
6058
  projectId: projectIdOfInput(news.project),
5661
6059
  createdAt: DEV_TIMESTAMP,
@@ -5664,6 +6062,13 @@ function LocalBucketProvider(input) {
5664
6062
  },
5665
6063
  catch: (cause) => cause
5666
6064
  }),
6065
+ diff: ({ output }) => Effect.tryPromise({
6066
+ try: async () => {
6067
+ if (output === void 0) return void 0;
6068
+ return await recordedInstanceId(appScopeOf$2(input), output.name) === output.bucketId ? void 0 : { action: "update" };
6069
+ },
6070
+ catch: (cause) => cause
6071
+ }),
5667
6072
  delete: () => Effect.void,
5668
6073
  read: ({ output }) => Effect.succeed(output)
5669
6074
  }));
@@ -5675,26 +6080,25 @@ function LocalBucketProvider(input) {
5675
6080
  * emulator, so a bucket emulator whose own state was wiped self-heals on the
5676
6081
  * next converge.
5677
6082
  */
5678
- function LocalBucketAccessKeyProvider(input) {
6083
+ function LocalBucketAccessKeyProvider(_input) {
5679
6084
  return Provider.effect(BucketAccessKey, Effect.succeed({
5680
6085
  list: () => Effect.succeed([]),
5681
6086
  reconcile: ({ news, output, id }) => Effect.tryPromise({
5682
6087
  try: async () => {
5683
- const app = appNameOf(input.container);
5684
6088
  const pair = output !== void 0 ? {
5685
6089
  accessKeyId: output.accessKeyId,
5686
6090
  secretAccessKey: Redacted.value(output.secretAccessKey)
5687
6091
  } : mintKeyPair();
5688
6092
  const client = bucketsClient();
5689
- await client.putCredentials(app, pair.accessKeyId, pair.secretAccessKey);
5690
6093
  const bucketId = bucketIdOfInput(news.bucket);
6094
+ await client.putCredentials(bucketId, pair.accessKeyId, pair.secretAccessKey);
5691
6095
  return {
5692
6096
  bucketAccessKeyId: news.name ?? id,
5693
6097
  bucketId,
5694
6098
  accessKeyId: pair.accessKeyId,
5695
6099
  secretAccessKey: Redacted.make(pair.secretAccessKey),
5696
6100
  endpoint: client.baseUrl,
5697
- bucketName: `${app}--${bucketId}`
6101
+ bucketName: bucketId
5698
6102
  };
5699
6103
  },
5700
6104
  catch: (cause) => cause
@@ -5704,6 +6108,36 @@ function LocalBucketAccessKeyProvider(input) {
5704
6108
  }));
5705
6109
  }
5706
6110
  /**
6111
+ * The emulators hook's half of the directory's record: before Alchemy runs,
6112
+ * every node of the app gets its emulator instance, one at a time in the
6113
+ * order given.
6114
+ */
6115
+ /**
6116
+ * Ensures each node's instance, in the order given: a recorded instance is
6117
+ * reused (a stopped database is started), a missing one is created again on
6118
+ * its remembered port, and a new node gets a new instance. A new instance
6119
+ * gets the smallest free port, so on a fresh start the ports follow this
6120
+ * order. Returns the nodes whose re-created instance could not get its
6121
+ * remembered port back.
6122
+ */
6123
+ async function ensureAppInstances(scope, nodes) {
6124
+ const changes = [];
6125
+ for (const { address, kind } of nodes) try {
6126
+ const { portChange } = await ensureNodeInstance(scope, address, kind);
6127
+ if (portChange) changes.push(portChange);
6128
+ } catch (cause) {
6129
+ const reason = cause instanceof Error ? cause.message : String(cause);
6130
+ throw new Error(`ensuring the local instance for ${kind} "${address}" failed: ${reason}`, { cause });
6131
+ }
6132
+ return changes;
6133
+ }
6134
+ async function ensureNodeInstance(scope, address, kind) {
6135
+ if (kind === "service") return ensureServiceInstance(scope, address);
6136
+ if (kind === "database") return ensureDatabaseInstance(scope, address, resolvePrismaDevModulePath());
6137
+ await ensureBucketInstance(scope, address, bucketDataDir(scope, address));
6138
+ return {};
6139
+ }
6140
+ /**
5707
6141
  * Extracts the tar.gz `@internal/lowering`'s `packageComputeArtifact` writes
5708
6142
  * (local-dev spec § 4) — emulation-only code, so it lives here rather than
5709
6143
  * beside the writer (production source stays physically separated from
@@ -5768,71 +6202,6 @@ function extractComputeArtifact(tarGzPath, destDir) {
5768
6202
  }
5769
6203
  }
5770
6204
  /**
5771
- * The shared dev-instance store (local-dev spec § 4): the JSON files under
5772
- * `<devDir>/` every local provider reads or writes. Every write is
5773
- * temp-then-rename; reads and writes on the SAME file are additionally
5774
- * serialized behind one in-process async queue per absolute path, because the
5775
- * local providers reconcile concurrently inside the one alchemy child and a
5776
- * naive read-modify-write would drop a concurrent writer's key. Postgres
5777
- * instance state is NOT here (REVISED — operator review of #162): the
5778
- * `postgres-main` daemon owns it now.
5779
- */
5780
- function isEnoent(err) {
5781
- return err instanceof Error && "code" in err && err.code === "ENOENT";
5782
- }
5783
- const queues = /* @__PURE__ */ new Map();
5784
- function enqueue(filePath, task) {
5785
- const result = (queues.get(filePath) ?? Promise.resolve()).then(() => void 0, () => void 0).then(task);
5786
- queues.set(filePath, result.then(() => void 0, () => void 0));
5787
- return result;
5788
- }
5789
- async function readJson(filePath, isValid, empty) {
5790
- let raw;
5791
- try {
5792
- raw = await fs$1$1.readFile(filePath, "utf8");
5793
- } catch (err) {
5794
- if (isEnoent(err)) return empty;
5795
- throw err;
5796
- }
5797
- let parsed;
5798
- try {
5799
- parsed = JSON.parse(raw);
5800
- } catch {
5801
- return empty;
5802
- }
5803
- return isValid(parsed) ? parsed : empty;
5804
- }
5805
- async function writeJsonAtomic(filePath, value, mode) {
5806
- const dir = path$1.dirname(filePath);
5807
- await fs$1$1.mkdir(dir, { recursive: true });
5808
- const tmpPath = path$1.join(dir, `.${path$1.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
5809
- const json = `${JSON.stringify(value, null, 2)}\n`;
5810
- await fs$1$1.writeFile(tmpPath, json, mode !== void 0 ? { mode } : void 0);
5811
- await fs$1$1.rename(tmpPath, filePath);
5812
- if (mode !== void 0) await fs$1$1.chmod(filePath, mode);
5813
- }
5814
- function createStore(filePath, isValid, empty, mode) {
5815
- return {
5816
- read: () => enqueue(filePath, () => readJson(filePath, isValid, empty)),
5817
- update: (mutate) => enqueue(filePath, async () => {
5818
- const next = mutate(await readJson(filePath, isValid, empty));
5819
- await writeJsonAtomic(filePath, next, mode);
5820
- return next;
5821
- })
5822
- };
5823
- }
5824
- function isStringRecord(value) {
5825
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
5826
- }
5827
- /** `<devDir>/env.json` — every `EnvironmentVariable` row, key → value. Last write wins. */
5828
- function envStore(devDir) {
5829
- return createStore(path$1.join(devDir, "env.json"), isStringRecord, {});
5830
- }
5831
- /** `<devDir>/secrets.json` — platform var name → value (shell-sourced or minted placeholder). Mode 0o600. */
5832
- function secretsStore(devDir) {
5833
- return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
5834
- }
5835
- /**
5836
6205
  * Local compute-cluster providers: upstream alchemy's `Prisma.App` and
5837
6206
  * `Prisma.Deployment` become clients of the machine-scoped Compute emulator;
5838
6207
  * `Prisma.EnvironmentVariable` becomes a row in the dev env store;
@@ -5937,16 +6306,6 @@ function readComputeManifest(artifactDir) {
5937
6306
  listens: parsed.listens !== false
5938
6307
  };
5939
6308
  }
5940
- /**
5941
- * A service address is dot-separated (`"orders.service"`) but the emulator's
5942
- * `<id>` segment must match `/^[a-z0-9][a-z0-9-]*$/`: disallowed characters
5943
- * become dashes, runs collapse. The real address still rides the deployment
5944
- * body's `address` field untouched, so listings show it verbatim.
5945
- */
5946
- function slugServiceId(address) {
5947
- const slug = address.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5948
- return slug.length > 0 ? slug : "svc";
5949
- }
5950
6309
  async function materializeEnv(devDir, address, port) {
5951
6310
  const env = scopedEnvRows(await envStore(devDir).read(), address);
5952
6311
  env[servicePortEnvKey(address)] = JSON.stringify(port);
@@ -5956,21 +6315,30 @@ async function materializeEnv(devDir, address, port) {
5956
6315
  if (process.env["HOME"] !== void 0) env["HOME"] = process.env["HOME"];
5957
6316
  return env;
5958
6317
  }
6318
+ function appScopeOf$1(input) {
6319
+ return {
6320
+ devDir: input.devDir,
6321
+ app: appNameOf(input.container)
6322
+ };
6323
+ }
6324
+ function localUrl(port) {
6325
+ return `http://localhost:${String(port)}`;
6326
+ }
5959
6327
  /**
5960
- * `Prisma.App` → the Compute emulator: reserves (or returns) the service's
5961
- * stable port. The app id here IS the service's own address, which
5962
- * `Deployment` slugs back into the emulator's id. `delete` is a no-op —
5963
- * instance removal belongs to `teardown` (`DELETE /apps/<app>`), not
5964
- * per-resource Alchemy deletes.
6328
+ * `Prisma.App` → the Compute emulator: finds or creates the instance that
6329
+ * runs the service, which holds the service's port. The app id here IS the
6330
+ * service's own address. An App whose instance was re-created on another
6331
+ * port is updated, so its dependents get the new URL. Deleting an App (its
6332
+ * service was removed) deletes its instance.
5965
6333
  */
5966
6334
  function LocalAppProvider(input) {
5967
6335
  return Provider.effect(App, Effect.succeed({
5968
6336
  list: () => Effect.succeed([]),
5969
6337
  reconcile: ({ id, news }) => Effect.tryPromise({
5970
6338
  try: async () => {
5971
- const appName = appNameOf(input.container);
5972
6339
  const name = news.displayName ?? id;
5973
- const { url } = await computeClient().ensureService(appName, slugServiceId(name));
6340
+ const { instance } = await ensureServiceInstance(appScopeOf$1(input), name);
6341
+ const url = localUrl(instance.port);
5974
6342
  return {
5975
6343
  appId: name,
5976
6344
  name,
@@ -5985,26 +6353,21 @@ function LocalAppProvider(input) {
5985
6353
  },
5986
6354
  catch: (cause) => cause
5987
6355
  }),
5988
- delete: () => Effect.void,
6356
+ diff: ({ output }) => Effect.tryPromise({
6357
+ try: async () => {
6358
+ if (output === void 0) return void 0;
6359
+ if ((await liveServiceInstance(appScopeOf$1(input), output.name))?.url === output.appEndpointDomain) return void 0;
6360
+ return { action: "update" };
6361
+ },
6362
+ catch: (cause) => cause
6363
+ }),
6364
+ delete: ({ output }) => Effect.tryPromise({
6365
+ try: () => deleteNodeInstance(appScopeOf$1(input), output.name),
6366
+ catch: (cause) => cause
6367
+ }),
5989
6368
  read: ({ output }) => Effect.succeed(output)
5990
6369
  }));
5991
6370
  }
5992
- /**
5993
- * Reserves each service's emulator port one at a time, in the order given,
5994
- * before Alchemy applies the `App` resources concurrently. A new service gets
5995
- * the smallest free port, so on a fresh start the ports follow this order; a
5996
- * service that already has a port keeps it. `serviceAppNames` are the
5997
- * `App` resources' `displayName`s.
5998
- */
5999
- async function reserveServicePorts(container, serviceAppNames) {
6000
- const appName = appNameOf(container);
6001
- for (const service of serviceAppNames) try {
6002
- await computeClient().ensureService(appName, slugServiceId(service));
6003
- } catch (cause) {
6004
- const reason = cause instanceof Error ? cause.message : String(cause);
6005
- throw new Error(`reserving a local port for service "${service}" failed: ${reason}`, { cause });
6006
- }
6007
- }
6008
6371
  /** `Prisma.EnvironmentVariable` → a key/value row in `<devDir>/env.json`. Upstream's value is `Redacted`; env.json holds the plain string the child process is given. */
6009
6372
  function LocalEnvironmentVariableProvider(input) {
6010
6373
  return Provider.effect(EnvironmentVariable, Effect.succeed({
@@ -6043,8 +6406,8 @@ function LocalEnvironmentVariableProvider(input) {
6043
6406
  }));
6044
6407
  }
6045
6408
  /**
6046
- * `Prisma.Deployment` → unpacks the artifact once per hash, fetches the
6047
- * emulator's assigned port, materializes the child's full env (env store +
6409
+ * `Prisma.Deployment` → unpacks the artifact once per hash, finds the
6410
+ * service's emulator instance and its port, materializes the child's full env (env store +
6048
6411
  * secrets + the port override + `PATH`/`HOME`), and puts the deployment — the
6049
6412
  * emulator (re)starts the child only when the hash or env actually changed.
6050
6413
  * `portMapping` is ignored on purpose: the emulator owns port allocation, and
@@ -6056,24 +6419,27 @@ function LocalDeploymentProvider(input) {
6056
6419
  list: () => Effect.succeed([]),
6057
6420
  reconcile: ({ news }) => Effect.tryPromise({
6058
6421
  try: async () => {
6059
- const app = appNameOf(input.container);
6060
6422
  const appId = appIdOfInput(news.app);
6061
- const emulatorId = slugServiceId(appId);
6062
6423
  if (news.artifactPath === void 0) throw new Error("local Deployment requires an artifactPath — nothing to run.");
6063
6424
  const artifactHash = await artifactSha256(news.artifactPath);
6064
6425
  const artifactDir = path$1.join(input.devDir, "artifacts", artifactHash);
6065
6426
  if (!fs$2.existsSync(artifactDir)) extractComputeArtifact(news.artifactPath, artifactDir);
6066
6427
  const { address, listens } = readComputeManifest(artifactDir);
6067
- const { port } = await computeClient().ensureService(app, emulatorId);
6068
- const env = await materializeEnv(input.devDir, address, port);
6069
- await computeClient().putDeployment(app, emulatorId, {
6070
- address,
6071
- artifactDir,
6072
- artifactHash,
6073
- env,
6074
- port,
6075
- listens
6076
- });
6428
+ const { instance } = await ensureServiceInstance(appScopeOf$1(input), appId);
6429
+ const env = await materializeEnv(input.devDir, address, instance.port);
6430
+ try {
6431
+ await computeClient().putDeployment(instance.id, {
6432
+ artifactDir,
6433
+ artifactHash,
6434
+ env,
6435
+ listens
6436
+ });
6437
+ } catch (error) {
6438
+ throw withServiceAddresses(error, [{
6439
+ address,
6440
+ instance
6441
+ }]);
6442
+ }
6077
6443
  return {
6078
6444
  deploymentId: artifactHash,
6079
6445
  appId,
@@ -6081,12 +6447,20 @@ function LocalDeploymentProvider(input) {
6081
6447
  status: "running",
6082
6448
  previewDomain: null,
6083
6449
  artifactHash,
6084
- appEndpointDomain: `http://localhost:${port}`,
6450
+ appEndpointDomain: localUrl(instance.port),
6085
6451
  createdAt: DEV_TIMESTAMP
6086
6452
  };
6087
6453
  },
6088
6454
  catch: (cause) => cause
6089
6455
  }),
6456
+ diff: ({ output }) => Effect.tryPromise({
6457
+ try: async () => {
6458
+ if (output === void 0) return void 0;
6459
+ if ((await liveServiceInstance(appScopeOf$1(input), output.appId))?.artifactHash === output.artifactHash) return void 0;
6460
+ return { action: "update" };
6461
+ },
6462
+ catch: (cause) => cause
6463
+ }),
6090
6464
  delete: () => Effect.void
6091
6465
  }));
6092
6466
  }
@@ -6116,57 +6490,28 @@ function LocalProjectProvider(_input) {
6116
6490
  delete: () => Effect.void
6117
6491
  }));
6118
6492
  }
6119
- /**
6120
- * Local postgres-cluster providers: upstream alchemy's `Prisma.Database` and
6121
- * `Prisma.Connection` become clients of the `postgres-main` emulator daemon
6122
- * (one named, persistent `@prisma/dev` server per `Database` resource).
6123
- * `PgWarm`/`OrmMigration` are not here — the hosted ones run against whatever
6124
- * URL they are handed. Attributes match upstream's shapes; the daemon's
6125
- * DIRECT connection string maps to `directConnectionString` and
6126
- * `databaseUrl`, everything else is left absent. Instance names come from
6127
- * `@internal/dev-emulators`' own `instanceNameFor` — a locally re-derived
6128
- * slug drifted from the daemon's and broke `Connection`'s lookup.
6129
- */
6130
6493
  /** Reads a database id from upstream's `database` input: a plain string or a resolved `Prisma.Database` attributes record. */
6131
6494
  function databaseIdOfInput(value) {
6132
6495
  if (typeof value === "string") return value;
6133
6496
  if (Predicate.isObject(value) && typeof value["databaseId"] === "string") return value["databaseId"];
6134
6497
  }
6135
- function noPrismaDevError(cause) {
6136
- return /* @__PURE__ */ new Error(`local dev needs @prisma/dev for its local Postgres emulator — @prisma/composer-prisma-cloud declares it as a dependency, but it did not resolve from Composer's own installation; reinstall your dependencies. (${cause instanceof Error ? cause.message : String(cause)})`);
6137
- }
6138
6498
  /**
6139
- * Composer owns the local Postgres emulator's version: `@prisma/dev` is a
6140
- * dependency of `@prisma/composer-prisma-cloud` and is resolved from
6141
- * Composer's own package, never from the app. The app's copy (if any) is
6142
- * ignored on purpose — an old `@prisma/dev` pulled in transitively (alchemy
6143
- * pins `^0.20.0`, whose pglite-socket crashes on any message over 64 KiB)
6144
- * would otherwise silently replace the version Composer tested against.
6145
- * The daemon imports the returned path dynamically.
6146
- */
6147
- function resolvePrismaDevModulePath() {
6148
- try {
6149
- return createRequire(import.meta.url).resolve("@prisma/dev");
6150
- } catch (cause) {
6151
- throw noPrismaDevError(cause);
6152
- }
6153
- }
6154
- /**
6155
- * `Prisma.Database` → an ensured `postgres-main` server, one per resource.
6156
- * Stores the daemon's returned url as the `directConnectionString` attribute.
6499
+ * `Prisma.Database` → the database instance the directory's record names for
6500
+ * the resource, started if it is stopped and created when there is none.
6501
+ * Stores its url as the `directConnectionString` attribute. A Database whose
6502
+ * recorded instance changed (re-created after the emulator lost it) is
6503
+ * updated, so its Connection and dependents get the new instance's URL.
6157
6504
  */
6158
6505
  function LocalDatabaseProvider(input) {
6159
6506
  return Provider.effect(Prisma.Database, Effect.succeed({
6160
6507
  list: () => Effect.succeed([]),
6161
6508
  reconcile: ({ id, news }) => Effect.tryPromise({
6162
6509
  try: async () => {
6163
- const app = appNameOf(input.container);
6164
- const name = news.name ?? id;
6165
- const prismaDevModulePath = resolvePrismaDevModulePath();
6166
- const { url } = await postgresClient().ensureDatabase(app, slug(name), prismaDevModulePath);
6167
- const direct = Redacted.make(url);
6510
+ const name = recordKeyOf("database", id, news.name);
6511
+ const { instance } = await ensureDatabaseInstance(appScopeOf(input), name, resolvePrismaDevModulePath());
6512
+ const direct = Redacted.make(instance.url);
6168
6513
  return {
6169
- databaseId: instanceNameFor(app, name),
6514
+ databaseId: instance.id,
6170
6515
  databaseName: name,
6171
6516
  projectId: projectIdOfInput(news.project),
6172
6517
  status: "ready",
@@ -6186,28 +6531,40 @@ function LocalDatabaseProvider(input) {
6186
6531
  },
6187
6532
  catch: (cause) => cause
6188
6533
  }),
6534
+ diff: ({ output }) => Effect.tryPromise({
6535
+ try: async () => {
6536
+ if (output === void 0 || output.databaseName === void 0) return void 0;
6537
+ return await recordedInstanceId(appScopeOf(input), output.databaseName) === output.databaseId ? void 0 : { action: "update" };
6538
+ },
6539
+ catch: (cause) => cause
6540
+ }),
6189
6541
  delete: () => Effect.void
6190
6542
  }));
6191
6543
  }
6544
+ function appScopeOf(input) {
6545
+ return {
6546
+ devDir: input.devDir,
6547
+ app: appNameOf(input.container)
6548
+ };
6549
+ }
6192
6550
  function noRecordedInstanceError(databaseId) {
6193
6551
  return /* @__PURE__ */ new Error(`no local Postgres instance recorded for databaseId "${databaseId}" — the Database provider did not run; converge is corrupt (try --fresh).`);
6194
6552
  }
6195
- /** `Prisma.Connection` → the daemon's live listing, matched by instance name (the Database attributes' `databaseId` IS the instance name). */
6196
- function LocalConnectionProvider(input) {
6553
+ /** `Prisma.Connection` → the database instance the Database attributes' `databaseId` names. */
6554
+ function LocalConnectionProvider(_input) {
6197
6555
  return Provider.effect(Prisma.Connection, Effect.succeed({
6198
6556
  list: () => Effect.succeed([]),
6199
6557
  reconcile: ({ id, news }) => Effect.tryPromise({
6200
6558
  try: async () => {
6201
- const app = appNameOf(input.container);
6202
6559
  const databaseId = databaseIdOfInput(news.database);
6203
6560
  if (databaseId === void 0) throw noRecordedInstanceError(String(news.database));
6204
- const found = (await postgresClient().listDatabases(app)).find((entry) => entry.instanceName === databaseId);
6561
+ const found = await postgresClient().getInstance(databaseId);
6205
6562
  if (found === void 0) throw noRecordedInstanceError(databaseId);
6206
6563
  const direct = Redacted.make(found.url);
6207
6564
  return {
6208
- connectionId: found.instanceName,
6565
+ connectionId: found.id,
6209
6566
  connectionName: news.name ?? id,
6210
- databaseId: found.instanceName,
6567
+ databaseId: found.id,
6211
6568
  kind: "postgres",
6212
6569
  createdAt: DEV_TIMESTAMP,
6213
6570
  directConnectionString: direct,
@@ -6253,10 +6610,8 @@ function resolvePackageEntry(specifier) {
6253
6610
  * ADR-0041 D12). The target extension's `dev/teardown.ts` orchestrates
6254
6611
  * (which app, which emulator clients); this module owns the actual
6255
6612
  * filesystem operations, so the extension's own source stays free of
6256
- * `node:`/`bun:` imports (its invariant 5). Postgres instance removal is
6257
- * NOT here (REVISED — operator review of #162): it is a
6258
- * `postgresClient().deleteApp(app)` call, made directly alongside the
6259
- * compute/buckets clients — no filesystem or child-process work of its own.
6613
+ * `node:`/`bun:` imports (its invariant 5). Emulator instances are not
6614
+ * removed here: the extension deletes them through the directory's record.
6260
6615
  */
6261
6616
  /** `fs.rm(..., { recursive: true, force: true })` for every given path — tolerates absence. */
6262
6617
  function removeLocalPaths(paths) {
@@ -6265,6 +6620,21 @@ function removeLocalPaths(paths) {
6265
6620
  force: true
6266
6621
  });
6267
6622
  }
6623
+ /**
6624
+ * Removes the dev state directory's contents for `--fresh`, except what
6625
+ * other apps run from the directory still use: the record of their emulator
6626
+ * instances and their buckets' objects. This app's objects go.
6627
+ */
6628
+ function removeAppDevState(devDir, app) {
6629
+ let entries;
6630
+ try {
6631
+ entries = fs$2.readdirSync(devDir);
6632
+ } catch {
6633
+ return;
6634
+ }
6635
+ const kept = /* @__PURE__ */ new Set([EMULATOR_INSTANCES_FILE, BUCKETS_DIR]);
6636
+ removeLocalPaths([...entries.filter((entry) => !kept.has(entry)).map((entry) => path$1.join(devDir, entry)), path$1.join(devDir, BUCKETS_DIR, app)]);
6637
+ }
6268
6638
  //#endregion
6269
6639
  //#region ../../1-prisma-cloud/1-extensions/target/dist/local-target.mjs
6270
6640
  const RELIST_INTERVAL_MS = 2e3;
@@ -6275,7 +6645,7 @@ const RELIST_INTERVAL_MS = 2e3;
6275
6645
  * dropped (an emulator restart shows a gap in that service's stream, never a
6276
6646
  * dead session).
6277
6647
  */
6278
- async function* mergedLogs(app, signal, tail) {
6648
+ async function* mergedLogs(scope, signal, tail) {
6279
6649
  const client = computeClient();
6280
6650
  const followed = /* @__PURE__ */ new Set();
6281
6651
  const backlogged = /* @__PURE__ */ new Set();
@@ -6294,7 +6664,7 @@ async function* mergedLogs(app, signal, tail) {
6294
6664
  (async () => {
6295
6665
  try {
6296
6666
  let buffer = "";
6297
- for await (const chunk of client.followLogs(app, id, signal, { tail: followTail })) {
6667
+ for await (const chunk of client.followLogs(id, signal, { tail: followTail })) {
6298
6668
  buffer += chunk;
6299
6669
  let newlineAt = buffer.indexOf("\n");
6300
6670
  while (newlineAt !== -1) {
@@ -6312,8 +6682,7 @@ async function* mergedLogs(app, signal, tail) {
6312
6682
  })();
6313
6683
  };
6314
6684
  const relist = async () => {
6315
- const services = await client.listServices(app);
6316
- for (const svc of services) follow(svc.id, svc.address);
6685
+ for (const { address, instance } of await nodeInstancesOf(scope, "service")) follow(instance.id, address);
6317
6686
  };
6318
6687
  await relist();
6319
6688
  const timer = setInterval(() => void relist(), RELIST_INTERVAL_MS);
@@ -6338,18 +6707,28 @@ async function* mergedLogs(app, signal, tail) {
6338
6707
  }
6339
6708
  }
6340
6709
  async function devAttach(input) {
6341
- const app = prismaCloudContainerOf(input.container).input.appName;
6710
+ const scope = {
6711
+ devDir: input.devDir,
6712
+ app: prismaCloudContainerOf(input.container).input.appName
6713
+ };
6342
6714
  const client = computeClient();
6343
6715
  return {
6344
- startServices: () => client.startApp(app),
6716
+ startServices: async () => {
6717
+ const nodes = await nodeInstancesOf(scope, "service");
6718
+ try {
6719
+ await client.startInstances(nodes.map(({ instance }) => instance.id));
6720
+ } catch (error) {
6721
+ throw withServiceAddresses(error, nodes);
6722
+ }
6723
+ },
6345
6724
  endpoints: async () => {
6346
- return (await client.listServices(app)).map((svc) => ({
6347
- address: svc.address,
6348
- url: svc.url
6725
+ return (await nodeInstancesOf(scope, "service")).map(({ address, instance }) => ({
6726
+ address,
6727
+ url: `http://localhost:${String(instance.port)}`
6349
6728
  }));
6350
6729
  },
6351
- logs: (signal, opts) => mergedLogs(app, signal, opts?.tail ?? 0),
6352
- stopServices: () => client.stopApp(app)
6730
+ logs: (signal, opts) => mergedLogs(scope, signal, opts?.tail ?? 0),
6731
+ stopApp: () => stopAppInstances(scope)
6353
6732
  };
6354
6733
  }
6355
6734
  const LOCAL_PROJECT_ID = "local";
@@ -6371,28 +6750,54 @@ function devContainerDescriptor() {
6371
6750
  function daemonEntry(name) {
6372
6751
  return resolvePackageEntry(`@prisma/composer-prisma-cloud/local-target/${name}-main`);
6373
6752
  }
6374
- function usesBuckets(input) {
6375
- return input.graph.nodes.some((n) => n.node.kind === "resource" && n.node.type === "s3");
6376
- }
6377
- function usesPostgres(input) {
6378
- return input.graph.nodes.some((n) => n.node.kind === "resource" && (n.node.type === "raw-postgres" || n.node.type === "postgres"));
6379
- }
6380
- /** The `App` name of each of this extension's services, in `graph.nodes` order: dependencies first, ties in declaration order. */
6381
- function serviceAppNames(input) {
6382
- return input.graph.nodes.filter((n) => n.node.kind === "service" && n.node.extension === "@prisma/composer-prisma-cloud").map((n) => serviceAppName(n.id));
6753
+ const DATABASE_TYPES = /* @__PURE__ */ new Set(["raw-postgres", "postgres"]);
6754
+ const BUCKET_TYPE = "s3";
6755
+ /**
6756
+ * This extension's nodes that run on an emulator, in `graph.nodes` order —
6757
+ * dependencies first, ties in declaration order — keyed by the name their
6758
+ * resource is given: a service by its `App` name, a database and a bucket by
6759
+ * their resource name — the keys the local providers look them up by.
6760
+ */
6761
+ function appNodes(input) {
6762
+ return input.graph.nodes.flatMap((n) => {
6763
+ if (n.node.kind === "service" && n.node.extension === "@prisma/composer-prisma-cloud") return [{
6764
+ address: serviceAppName(n.id),
6765
+ kind: "service"
6766
+ }];
6767
+ if (n.node.kind === "resource" && DATABASE_TYPES.has(n.node.type)) return [{
6768
+ address: resourceName(n.id),
6769
+ kind: "database"
6770
+ }];
6771
+ if (n.node.kind === "resource" && n.node.type === BUCKET_TYPE) return [{
6772
+ address: resourceName(n.id),
6773
+ kind: "bucket"
6774
+ }];
6775
+ return [];
6776
+ });
6383
6777
  }
6384
6778
  async function runDevEmulators(input) {
6779
+ const nodes = appNodes(input);
6385
6780
  const { url: computeUrl } = await ensureDaemon("compute", daemonEntry("compute"));
6386
6781
  console.log(`[dev] compute emulator ready at ${computeUrl}`);
6387
- await reserveServicePorts(input.container, serviceAppNames(input));
6388
- if (usesBuckets(input)) {
6782
+ if (nodes.some((node) => node.kind === "bucket")) {
6389
6783
  const { url: bucketsUrl } = await ensureDaemon("buckets", daemonEntry("buckets"));
6390
6784
  console.log(`[dev] buckets emulator ready at ${bucketsUrl}`);
6391
6785
  }
6392
- if (usesPostgres(input)) {
6786
+ if (nodes.some((node) => node.kind === "database")) {
6393
6787
  const { url: postgresUrl } = await ensureDaemon("postgres", daemonEntry("postgres"));
6394
6788
  console.log(`[dev] postgres emulator ready at ${postgresUrl}`);
6395
6789
  }
6790
+ const scope = {
6791
+ devDir: input.devDir,
6792
+ app: appNameOf(input.container)
6793
+ };
6794
+ const previous = await selectApp(scope);
6795
+ if (previous !== void 0) console.log(`[dev] stopped the local services of "${previous}", the app run here before; its data is kept`);
6796
+ const portChanges = await ensureAppInstances(scope, nodes).catch(async (error) => {
6797
+ await stopAppInstances(scope).catch(() => void 0);
6798
+ throw error;
6799
+ });
6800
+ for (const { kind, address, previous: before, given } of portChanges) console.log(`[dev] ${kind} "${address}" now uses port ${String(given)}: its previous port ${String(before)} is in use`);
6396
6801
  }
6397
6802
  /** `local-placeholder-<16 lowercase hex>` — Web Crypto only, matching the extension's other local mints (ServiceKey, S3Credentials). */
6398
6803
  function mintPlaceholder() {
@@ -6433,21 +6838,25 @@ async function runDevPreflight(input) {
6433
6838
  }));
6434
6839
  if (missing.length > 0) throw missingEnvParamsError(missing);
6435
6840
  }
6436
- async function tolerateUnreachable(action) {
6437
- try {
6438
- await action();
6439
- } catch {}
6440
- }
6841
+ const DAEMON_OF = {
6842
+ service: "compute",
6843
+ database: "postgres",
6844
+ bucket: "buckets"
6845
+ };
6441
6846
  async function runDevTeardown(input) {
6442
6847
  const app = prismaCloudContainerOf(input.container).input.appName;
6443
6848
  const cwd = process.cwd();
6444
- await tolerateUnreachable(() => postgresClient().deleteApp(app));
6849
+ const devDir = `${cwd}/${DEV_DIR}`;
6850
+ const scope = {
6851
+ devDir,
6852
+ app
6853
+ };
6445
6854
  try {
6446
- await ensureDaemon("compute", daemonEntry("compute"));
6447
- await computeClient().deleteApp(app);
6855
+ for (const kind of await recordedKinds(scope)) await ensureDaemon(DAEMON_OF[kind], daemonEntry(DAEMON_OF[kind]));
6856
+ await deleteAppInstances(scope);
6448
6857
  } finally {
6449
- await tolerateUnreachable(() => bucketsClient().deleteApp(app));
6450
- removeLocalPaths([`${cwd}/${DEV_DIR}`, `${cwd}/.alchemy/state/${app}/dev`]);
6858
+ removeAppDevState(devDir, app);
6859
+ removeLocalPaths([`${cwd}/.alchemy/state/${app}/dev`]);
6451
6860
  }
6452
6861
  }
6453
6862
  /** `localTargetProviders()`'s `ProviderCollection` doesn't structurally unify with Alchemy's inferred providers Layer (a @internal/lowering typings gap); it satisfies it at runtime — mirrors `control/extension.ts`'s own `asProvidersLayer`, duplicated rather than shared so this module imports nothing from `./control`. */
@@ -6462,7 +6871,7 @@ function asProvidersLayer(layer) {
6462
6871
  function localTargetDescriptor() {
6463
6872
  return {
6464
6873
  container: devContainerDescriptor(),
6465
- providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(), OrmMigrationProvider(), S3CredentialsProvider(), GeneratedParamProvider(), ServiceKeyProvider())),
6874
+ providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(), LocalOrmMigrationProvider(), S3CredentialsProvider(), GeneratedParamProvider(), ServiceKeyProvider())),
6466
6875
  preflight: (input) => runDevPreflight(input),
6467
6876
  emulators: (input) => runDevEmulators(input),
6468
6877
  attach: (input) => devAttach(input),