@prisma/composer-prisma-cloud 0.30.0-dev.3 → 0.30.0-dev.5

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-azxxffwQ.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-DrGYkOaZ.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { blindCast } from "@prisma/composer/casts";
5
5
  import * as Effect from "effect/Effect";
@@ -58,6 +58,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
58
58
  }) : target, mod));
59
59
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
60
60
  //#endregion
61
+ //#region ../../0-framework/0-foundation/foundation/dist/internal-error-Dfur7Zqt.mjs
62
+ /**
63
+ * A bug in Prisma ORM, not a user error. Never catch this except at the
64
+ * outermost boundary for crash reporting — an InternalError means an invariant
65
+ * broke and the process cannot reliably continue. User-facing failures use
66
+ * `structuredError` with a dotted code instead.
67
+ */
68
+ var InternalError = class extends Error {
69
+ isPrismaInternalError = true;
70
+ constructor(message, options) {
71
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
72
+ this.name = "InternalError";
73
+ }
74
+ };
75
+ //#endregion
61
76
  //#region ../../0-framework/0-foundation/foundation/dist/errors.mjs
62
77
  /**
63
78
  * Returns an object with the key/value if value is defined, otherwise an empty object.
@@ -1780,6 +1795,9 @@ function isPidAlive(pid) {
1780
1795
  return false;
1781
1796
  }
1782
1797
  }
1798
+ async function readRegistryEntry(registryRoot, name) {
1799
+ return readJsonFile(registryFilePath(registryRoot, name), isRegistryEntry$1);
1800
+ }
1783
1801
  async function probeHealth(port, healthPath, timeoutMs) {
1784
1802
  try {
1785
1803
  const res = await fetch(`http://127.0.0.1:${port}${healthPath}`, { signal: AbortSignal.timeout(timeoutMs) });
@@ -2050,50 +2068,53 @@ async function ensureDaemon(name, entry, opts = {}) {
2050
2068
  await release();
2051
2069
  }
2052
2070
  }
2053
- //#endregion
2054
- //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/segments-CA7TOJ1U.mjs
2055
- /**
2056
- * API hygiene shared by both daemons (local-dev spec § 2): every `<app>`,
2057
- * `<id>`, and `<name>` path segment must match this shape or the request is
2058
- * a 400 naming the segment.
2059
- */
2060
- const SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/;
2061
- function isValidSegment(segment) {
2062
- return segment.length <= 63 && SEGMENT_RE.test(segment);
2063
- }
2064
- //#endregion
2065
- //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/instance-name-BF6J_weL.mjs
2066
- /**
2067
- * `pcdev-<app>-<database-id>` instance-name derivation (local-dev spec § 2
2068
- * `postgres-main.ts`): each half lowercased, every char outside `[a-z0-9]`
2069
- * replaced by `-`, runs collapsed, the combined name trimmed to 63 chars.
2070
- *
2071
- * A separate, side-effect-free module (not defined inline in
2072
- * `postgres-main.ts`) so it can be imported directly by tests without also
2073
- * running that file's own `main()` — `postgres-main.ts` is a daemon
2074
- * entrypoint script, always invoked as a subprocess, and calls `main()`
2075
- * unconditionally at module load.
2076
- */
2077
- /**
2078
- * Deliberately linear, no ambiguous quantifiers: a per-character replace
2079
- * (no `+`, so no run-length backtracking surface), a bounded-quantifier
2080
- * collapse (`{2,}`, not alternation-with-quantifiers), and plain
2081
- * index-walking for the leading/trailing trim instead of a regex — a
2082
- * combined `+`-plus-alternation trim (`/^-+|-+$/g`) is exactly the shape
2083
- * CodeQL's polynomial-ReDoS check flags, whether or not this particular
2084
- * instance is provably safe.
2085
- */
2086
- function slug(segment) {
2087
- const collapsed = segment.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-{2,}/g, "-");
2088
- let start = 0;
2089
- let end = collapsed.length;
2090
- while (start < end && collapsed[start] === "-") start++;
2091
- while (end > start && collapsed[end - 1] === "-") end--;
2092
- return collapsed.slice(start, end);
2093
- }
2094
- function instanceNameFor(app, id) {
2095
- return `pcdev-${slug(app)}-${slug(id)}`.slice(0, 63);
2096
- }
2071
+ (class ResultImpl {
2072
+ ok;
2073
+ _value;
2074
+ _failure;
2075
+ constructor(ok, valueOrFailure) {
2076
+ this.ok = ok;
2077
+ if (ok) this._value = blindCast$1(valueOrFailure);
2078
+ else this._failure = blindCast$1(valueOrFailure);
2079
+ Object.freeze(this);
2080
+ }
2081
+ get value() {
2082
+ if (!this.ok) throw new InternalError("Cannot access value on NotOk result");
2083
+ return this._value;
2084
+ }
2085
+ get failure() {
2086
+ if (this.ok) throw new InternalError("Cannot access failure on Ok result");
2087
+ return this._failure;
2088
+ }
2089
+ /**
2090
+ * Creates a successful result.
2091
+ */
2092
+ static ok(value) {
2093
+ return blindCast$1(new ResultImpl(true, value));
2094
+ }
2095
+ /**
2096
+ * Creates an unsuccessful result.
2097
+ */
2098
+ static notOk(failure) {
2099
+ return blindCast$1(new ResultImpl(false, failure));
2100
+ }
2101
+ /**
2102
+ * Asserts that this result is Ok and returns the value.
2103
+ * Throws if the result is NotOk.
2104
+ */
2105
+ assertOk() {
2106
+ if (!this.ok) throw new InternalError("Expected Ok result but got NotOk");
2107
+ return this.value;
2108
+ }
2109
+ /**
2110
+ * Asserts that this result is NotOk and returns the failure.
2111
+ * Throws if the result is Ok.
2112
+ */
2113
+ assertNotOk() {
2114
+ if (this.ok) throw new InternalError("Expected NotOk result but got Ok");
2115
+ return this.failure;
2116
+ }
2117
+ }).ok(void 0);
2097
2118
  //#endregion
2098
2119
  //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/index.mjs
2099
2120
  /**
@@ -2126,10 +2147,6 @@ function resolveBaseUrl(name, opts) {
2126
2147
  if (!isRegistryEntry(parsed) || !isPidAlive(parsed.pid)) throw notRunningError(name);
2127
2148
  return `http://127.0.0.1:${parsed.port}`;
2128
2149
  }
2129
- function encodeSegment(segment) {
2130
- if (!isValidSegment(segment)) throw new Error(`invalid path segment "${segment}"`);
2131
- return encodeURIComponent(segment);
2132
- }
2133
2150
  const ADMIN_FETCH_ATTEMPTS = 3;
2134
2151
  const ADMIN_FETCH_RETRY_DELAY_MS = 150;
2135
2152
  /**
@@ -2176,11 +2193,11 @@ const INSTANCE_STATUSES = /* @__PURE__ */ new Set([
2176
2193
  function isStringRecord$1(value) {
2177
2194
  return typeof value === "object" && value !== null && Object.values(value).every((v) => typeof v === "string");
2178
2195
  }
2179
- function isInstanceInfo(value) {
2196
+ function isServiceInstanceInfo(value) {
2180
2197
  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";
2181
2198
  }
2182
- function isInstanceInfoArray(value) {
2183
- return Array.isArray(value) && value.every(isInstanceInfo);
2199
+ function isServiceInstanceInfoArray(value) {
2200
+ return Array.isArray(value) && value.every(isServiceInstanceInfo);
2184
2201
  }
2185
2202
  function isHealthBody(value) {
2186
2203
  return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
@@ -2206,19 +2223,19 @@ function computeClient(opts = {}) {
2206
2223
  },
2207
2224
  async createInstance(request) {
2208
2225
  const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
2209
- if (!isInstanceInfo(body)) throw new Error("malformed instance-create response from the compute emulator");
2226
+ if (!isServiceInstanceInfo(body)) throw new Error("malformed instance-create response from the compute emulator");
2210
2227
  return body;
2211
2228
  },
2212
2229
  async listInstances() {
2213
2230
  const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
2214
- if (!isInstanceInfoArray(body)) throw new Error("malformed instances listing from the compute emulator");
2231
+ if (!isServiceInstanceInfoArray(body)) throw new Error("malformed instances listing from the compute emulator");
2215
2232
  return body;
2216
2233
  },
2217
2234
  async getInstance(id) {
2218
2235
  const res = await adminFetch(instancePath(baseUrl, id));
2219
2236
  if (res.status === 404) return void 0;
2220
2237
  const body = await (await expectOk(res)).json();
2221
- if (!isInstanceInfo(body)) throw new Error("malformed instance response from the compute emulator");
2238
+ if (!isServiceInstanceInfo(body)) throw new Error("malformed instance response from the compute emulator");
2222
2239
  return body;
2223
2240
  },
2224
2241
  async putDeployment(id, deployment) {
@@ -2255,8 +2272,15 @@ function computeClient(opts = {}) {
2255
2272
  }
2256
2273
  };
2257
2274
  }
2275
+ function isBucketInstanceInfo(value) {
2276
+ 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";
2277
+ }
2278
+ function isBucketInstanceInfoArray(value) {
2279
+ return Array.isArray(value) && value.every(isBucketInstanceInfo);
2280
+ }
2258
2281
  function bucketsClient(opts = {}) {
2259
2282
  const baseUrl = resolveBaseUrl("buckets", opts);
2283
+ const adminUrl = `${baseUrl}/_pcdev/instances`;
2260
2284
  return {
2261
2285
  baseUrl,
2262
2286
  async health() {
@@ -2264,15 +2288,25 @@ function bucketsClient(opts = {}) {
2264
2288
  if (!isHealthBody(body)) throw new Error("malformed /_pcdev/health response from the buckets emulator");
2265
2289
  return body;
2266
2290
  },
2267
- async putBucket(app, name, dir) {
2268
- await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}/buckets/${encodeSegment(name)}`, {
2269
- method: "PUT",
2270
- headers: { "content-type": "application/json" },
2271
- body: JSON.stringify({ dir })
2272
- }));
2291
+ async createInstance(request) {
2292
+ const body = await (await expectOk(await adminFetch(adminUrl, postJson(request), { retry: false }))).json();
2293
+ if (!isBucketInstanceInfo(body)) throw new Error("malformed instance-create response from the buckets emulator");
2294
+ return body;
2295
+ },
2296
+ async listInstances() {
2297
+ const body = await (await expectOk(await adminFetch(adminUrl))).json();
2298
+ if (!isBucketInstanceInfoArray(body)) throw new Error("malformed instances listing from the buckets emulator");
2299
+ return body;
2300
+ },
2301
+ async getInstance(id) {
2302
+ const res = await adminFetch(instancePath(`${baseUrl}/_pcdev`, id));
2303
+ if (res.status === 404) return void 0;
2304
+ const body = await (await expectOk(res)).json();
2305
+ if (!isBucketInstanceInfo(body)) throw new Error("malformed instance response from the buckets emulator");
2306
+ return body;
2273
2307
  },
2274
- async putCredentials(app, accessKeyId, secretAccessKey) {
2275
- await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}/credentials`, {
2308
+ async putCredentials(id, accessKeyId, secretAccessKey) {
2309
+ await expectOk(await adminFetch(`${instancePath(`${baseUrl}/_pcdev`, id)}/credentials`, {
2276
2310
  method: "PUT",
2277
2311
  headers: { "content-type": "application/json" },
2278
2312
  body: JSON.stringify({
@@ -2281,19 +2315,16 @@ function bucketsClient(opts = {}) {
2281
2315
  })
2282
2316
  }));
2283
2317
  },
2284
- async deleteApp(app) {
2285
- await expectOk(await adminFetch(`${baseUrl}/_pcdev/apps/${encodeSegment(app)}`, { method: "DELETE" }));
2318
+ async deleteInstance(id) {
2319
+ await expectOk(await adminFetch(instancePath(`${baseUrl}/_pcdev`, id), { method: "DELETE" }));
2286
2320
  }
2287
2321
  };
2288
2322
  }
2289
- function isDatabaseInfo(value) {
2290
- 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";
2323
+ function isDatabaseInstanceInfo(value) {
2324
+ 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");
2291
2325
  }
2292
- function isDatabaseInfoArray(value) {
2293
- return Array.isArray(value) && value.every(isDatabaseInfo);
2294
- }
2295
- function isEnsuredDatabase(value) {
2296
- return typeof value === "object" && value !== null && "url" in value && typeof value.url === "string";
2326
+ function isDatabaseInstanceInfoArray(value) {
2327
+ return Array.isArray(value) && value.every(isDatabaseInstanceInfo);
2297
2328
  }
2298
2329
  function postgresClient(opts = {}) {
2299
2330
  const baseUrl = resolveBaseUrl("postgres", opts);
@@ -2304,25 +2335,53 @@ function postgresClient(opts = {}) {
2304
2335
  if (!isHealthBody(body)) throw new Error("malformed /health response from the postgres emulator");
2305
2336
  return body;
2306
2337
  },
2307
- async ensureDatabase(app, id, prismaDevModulePath) {
2308
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/databases/${encodeSegment(id)}`, {
2309
- method: "PUT",
2310
- headers: { "content-type": "application/json" },
2311
- body: JSON.stringify({ prismaDevModulePath })
2312
- }))).json();
2313
- if (!isEnsuredDatabase(body)) throw new Error("malformed database-ensure response from the postgres emulator");
2338
+ async createInstance(request) {
2339
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`, postJson(request), { retry: false }))).json();
2340
+ if (!isDatabaseInstanceInfo(body)) throw new Error("malformed instance-create response from the postgres emulator");
2341
+ return body;
2342
+ },
2343
+ async listInstances() {
2344
+ const body = await (await expectOk(await adminFetch(`${baseUrl}/instances`))).json();
2345
+ if (!isDatabaseInstanceInfoArray(body)) throw new Error("malformed instances listing from the postgres emulator");
2314
2346
  return body;
2315
2347
  },
2316
- async listDatabases(app) {
2317
- const body = await (await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}/databases`))).json();
2318
- if (!isDatabaseInfoArray(body)) throw new Error("malformed databases listing from the postgres emulator");
2348
+ async getInstance(id) {
2349
+ const res = await adminFetch(instancePath(baseUrl, id));
2350
+ if (res.status === 404) return void 0;
2351
+ const body = await (await expectOk(res)).json();
2352
+ if (!isDatabaseInstanceInfo(body)) throw new Error("malformed instance response from the postgres emulator");
2319
2353
  return body;
2320
2354
  },
2321
- async deleteApp(app) {
2322
- await expectOk(await adminFetch(`${baseUrl}/apps/${encodeSegment(app)}`, { method: "DELETE" }));
2355
+ async startInstances(ids, prismaDevModulePath) {
2356
+ await expectOk(await adminFetch(`${baseUrl}/instances/start`, postJson({
2357
+ ids,
2358
+ prismaDevModulePath
2359
+ })));
2360
+ },
2361
+ async stopInstances(ids) {
2362
+ await expectOk(await adminFetch(`${baseUrl}/instances/stop`, postJson({ ids })));
2363
+ },
2364
+ async deleteInstance(id, prismaDevModulePath) {
2365
+ await expectOk(await adminFetch(instancePath(baseUrl, id), {
2366
+ method: "DELETE",
2367
+ ...prismaDevModulePath !== void 0 ? {
2368
+ headers: { "content-type": "application/json" },
2369
+ body: JSON.stringify({ prismaDevModulePath })
2370
+ } : {}
2371
+ }));
2323
2372
  }
2324
2373
  };
2325
2374
  }
2375
+ const DAEMON_OF$1 = {
2376
+ service: "compute",
2377
+ database: "postgres",
2378
+ bucket: "buckets"
2379
+ };
2380
+ /** Whether the emulator for `kind` is registered with a live process. */
2381
+ async function isEmulatorRunning(kind, opts = {}) {
2382
+ const entry = await readRegistryEntry(opts.registryRoot ?? defaultRegistryRoot(), DAEMON_OF$1[kind]);
2383
+ return entry !== void 0 && isPidAlive(entry.pid);
2384
+ }
2326
2385
  //#endregion
2327
2386
  //#region ../../../node_modules/tar/dist/esm/index.min.js
2328
2387
  var zr = Object.defineProperty;
@@ -5640,156 +5699,6 @@ function appNameOf(container) {
5640
5699
  return container.input.appName;
5641
5700
  }
5642
5701
  /**
5643
- * Shared helpers for the upstream-attribute records every local provider
5644
- * emits — one implementation, so the compute and postgres families cannot
5645
- * drift on the `'local'` project fallback or the timestamp.
5646
- */
5647
- /** The fixed `createdAt`/`updatedAt` local providers stamp: local dev has no meaningful creation time. */
5648
- const DEV_TIMESTAMP = "1970-01-01T00:00:00.000Z";
5649
- /** Reads a project id from upstream's `project` input: a plain string or a resolved `Prisma.Project` attributes record. */
5650
- function projectIdOfInput(value) {
5651
- if (typeof value === "string") return value;
5652
- if (Predicate.isObject(value) && typeof value["projectId"] === "string") return value["projectId"];
5653
- return "local";
5654
- }
5655
- /**
5656
- * Local bucket-cluster providers (local-dev spec § 4): both `Bucket` and
5657
- * `BucketAccessKey` are clients of the machine-global bucket emulator —
5658
- * already up by the time these run, since the extension's
5659
- * `localTarget.emulators` hook ensures it before converge.
5660
- */
5661
- /** Upstream lets `name` default to the resource's logical ID; locally a bucket must have a concrete name to register under. */
5662
- const bucketNameOf = (news, id) => news.name ?? id;
5663
- /** Reads a bucket id from upstream's `bucket` input: a plain string or a resolved `Prisma.Bucket` attributes record. */
5664
- function bucketIdOfInput(value) {
5665
- if (typeof value === "string") return value;
5666
- if (Predicate.isObject(value) && typeof value["bucketId"] === "string") return value["bucketId"];
5667
- return "local";
5668
- }
5669
- /** `Bucket` → registers `<app>--<name>` with the bucket emulator, backed by an in-project data root. */
5670
- function LocalBucketProvider(input) {
5671
- return Provider.effect(Bucket, Effect.succeed({
5672
- list: () => Effect.succeed([]),
5673
- reconcile: ({ news, id }) => Effect.tryPromise({
5674
- try: async () => {
5675
- const app = appNameOf(input.container);
5676
- const name = bucketNameOf(news, id);
5677
- const dir = path$1.join(input.devDir, "buckets", name);
5678
- await bucketsClient().putBucket(app, name, dir);
5679
- return {
5680
- bucketId: name,
5681
- name,
5682
- projectId: projectIdOfInput(news.project),
5683
- createdAt: DEV_TIMESTAMP,
5684
- logicalId: null
5685
- };
5686
- },
5687
- catch: (cause) => cause
5688
- }),
5689
- delete: () => Effect.void,
5690
- read: ({ output }) => Effect.succeed(output)
5691
- }));
5692
- }
5693
- /**
5694
- * `BucketAccessKey` → mint-once-stable (the same lifecycle as `ServiceKey`/
5695
- * `S3Credentials`: reuse `output`'s pair when present, mint only on first
5696
- * create) — but ALWAYS re-registers the (prior or fresh) pair with the
5697
- * emulator, so a bucket emulator whose own state was wiped self-heals on the
5698
- * next converge.
5699
- */
5700
- function LocalBucketAccessKeyProvider(input) {
5701
- return Provider.effect(BucketAccessKey, Effect.succeed({
5702
- list: () => Effect.succeed([]),
5703
- reconcile: ({ news, output, id }) => Effect.tryPromise({
5704
- try: async () => {
5705
- const app = appNameOf(input.container);
5706
- const pair = output !== void 0 ? {
5707
- accessKeyId: output.accessKeyId,
5708
- secretAccessKey: Redacted.value(output.secretAccessKey)
5709
- } : mintKeyPair();
5710
- const client = bucketsClient();
5711
- await client.putCredentials(app, pair.accessKeyId, pair.secretAccessKey);
5712
- const bucketId = bucketIdOfInput(news.bucket);
5713
- return {
5714
- bucketAccessKeyId: news.name ?? id,
5715
- bucketId,
5716
- accessKeyId: pair.accessKeyId,
5717
- secretAccessKey: Redacted.make(pair.secretAccessKey),
5718
- endpoint: client.baseUrl,
5719
- bucketName: `${app}--${bucketId}`
5720
- };
5721
- },
5722
- catch: (cause) => cause
5723
- }),
5724
- delete: () => Effect.void,
5725
- read: ({ output }) => Effect.succeed(output)
5726
- }));
5727
- }
5728
- /**
5729
- * Extracts the tar.gz `@internal/lowering`'s `packageComputeArtifact` writes
5730
- * (local-dev spec § 4) — emulation-only code, so it lives here rather than
5731
- * beside the writer (production source stays physically separated from
5732
- * emulation). Reading tar is commodity even though the writer is a
5733
- * deterministic subset we own (fixed mtimes, sorted entries): the maintained
5734
- * `tar` package (dependency razor) does the parsing; this module keeps only
5735
- * the pinned entry filtering (regular files plus safe relative symlinks,
5736
- * reject devices/path escapes) and the directory-level temp-then-rename.
5737
- */
5738
- function unsupportedEntryError(entryPath, type) {
5739
- return /* @__PURE__ */ new Error(`compute artifact entry "${entryPath}" has type "${type}" — only regular files and safe symlinks are supported; this artifact was not produced by packageComputeArtifact.`);
5740
- }
5741
- function pathEscapeError(entryPath) {
5742
- return /* @__PURE__ */ new Error(`compute artifact entry "${entryPath}" escapes the extraction directory — this artifact was not produced by packageComputeArtifact.`);
5743
- }
5744
- const REGULAR_FILE_TYPES = /* @__PURE__ */ new Set([
5745
- "File",
5746
- "OldFile",
5747
- "ContiguousFile"
5748
- ]);
5749
- const SYMLINK_TYPE = "SymbolicLink";
5750
- /**
5751
- * Extracts `tarGzPath` (a `packageComputeArtifact` tar.gz) into `destDir`,
5752
- * temp-then-rename at the directory level: entries are written into a
5753
- * sibling temp directory first, then that directory is renamed onto
5754
- * `destDir` — a concurrent unpack of the SAME hash races benignly (last
5755
- * rename wins, both write identical bytes), and a reader of `destDir` never
5756
- * sees a partial tree.
5757
- */
5758
- function extractComputeArtifact(tarGzPath, destDir) {
5759
- const parentDir = path$1.dirname(destDir);
5760
- fs$2.mkdirSync(parentDir, { recursive: true });
5761
- const tmpDir = path$1.join(parentDir, `.${path$1.basename(destDir)}.${crypto$1.randomUUID()}.tmp`);
5762
- fs$2.mkdirSync(tmpDir, { recursive: true });
5763
- try {
5764
- So({
5765
- file: tarGzPath,
5766
- cwd: tmpDir,
5767
- sync: true,
5768
- onentry: (entry) => {
5769
- const resolved = path$1.resolve(tmpDir, entry.path);
5770
- if (!isWithin(tmpDir, resolved)) throw pathEscapeError(entry.path);
5771
- if (REGULAR_FILE_TYPES.has(entry.type)) return;
5772
- if (entry.type !== SYMLINK_TYPE) throw unsupportedEntryError(entry.path, entry.type);
5773
- if (entry.linkpath === void 0) throw unsupportedEntryError(entry.path, `${entry.type} without a target`);
5774
- if (path$1.isAbsolute(entry.linkpath)) throw pathEscapeError(`${entry.path} -> ${entry.linkpath}`);
5775
- const linkTarget = path$1.resolve(path$1.dirname(resolved), entry.linkpath);
5776
- if (!isWithin(tmpDir, linkTarget)) throw pathEscapeError(`${entry.path} -> ${entry.linkpath}`);
5777
- }
5778
- });
5779
- fs$2.rmSync(destDir, {
5780
- recursive: true,
5781
- force: true
5782
- });
5783
- fs$2.renameSync(tmpDir, destDir);
5784
- } catch (error) {
5785
- fs$2.rmSync(tmpDir, {
5786
- recursive: true,
5787
- force: true
5788
- });
5789
- throw error;
5790
- }
5791
- }
5792
- /**
5793
5702
  * The shared dev-instance store (local-dev spec § 4): the JSON files under
5794
5703
  * `<devDir>/` every local provider reads or writes. Every write is
5795
5704
  * temp-then-rename; reads and writes on the SAME file are additionally
@@ -5854,8 +5763,28 @@ function envStore(devDir) {
5854
5763
  function secretsStore(devDir) {
5855
5764
  return createStore(path$1.join(devDir, "secrets.json"), isStringRecord, {}, 384);
5856
5765
  }
5766
+ /** Where local dev loads `@prisma/dev` from, for the Postgres emulator. */
5767
+ function noPrismaDevError(cause) {
5768
+ 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)})`);
5769
+ }
5770
+ /**
5771
+ * Composer owns the local Postgres emulator's version: `@prisma/dev` is a
5772
+ * dependency of `@prisma/composer-prisma-cloud` and is resolved from
5773
+ * Composer's own package, never from the app. The app's copy (if any) is
5774
+ * ignored on purpose — an old `@prisma/dev` pulled in transitively (alchemy
5775
+ * pins `^0.20.0`, whose pglite-socket crashes on any message over 64 KiB)
5776
+ * would otherwise silently replace the version Composer tested against.
5777
+ * The daemon imports the returned path dynamically.
5778
+ */
5779
+ function resolvePrismaDevModulePath() {
5780
+ try {
5781
+ return createRequire(import.meta.url).resolve("@prisma/dev");
5782
+ } catch (cause) {
5783
+ throw noPrismaDevError(cause);
5784
+ }
5785
+ }
5857
5786
  /**
5858
- * Which emulator instance backs each node of the app run from a directory.
5787
+ * Which emulator instance backs each node of each app run from a directory.
5859
5788
  * `prisma dev` records it in `<directory>/.prisma-composer/dev/`, and every
5860
5789
  * Composer operation on an emulator instance goes through it. The emulators
5861
5790
  * know nothing about Composer: the labels written on create are for people
@@ -5863,128 +5792,293 @@ function secretsStore(devDir) {
5863
5792
  */
5864
5793
  const EMULATOR_INSTANCES_FILE = "emulator-instances.json";
5865
5794
  function isNodeInstance(value) {
5866
- 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";
5795
+ if (typeof value !== "object" || value === null) return false;
5796
+ if (!("id" in value) || typeof value.id !== "string" || !("kind" in value)) return false;
5797
+ if (value.kind === "bucket") return true;
5798
+ return (value.kind === "service" || value.kind === "database") && "port" in value && typeof value.port === "number";
5799
+ }
5800
+ function isNodes(value) {
5801
+ return typeof value === "object" && value !== null && Object.values(value).every(isNodeInstance);
5802
+ }
5803
+ function isAppInstances(value) {
5804
+ return typeof value === "object" && value !== null && "nodes" in value && isNodes(value.nodes);
5867
5805
  }
5868
5806
  function isEmulatorInstances(value) {
5869
- 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(isNodeInstance);
5807
+ 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);
5808
+ }
5809
+ function isEarlierServiceNode(value) {
5810
+ 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";
5811
+ }
5812
+ function isSingleAppRecord(value) {
5813
+ 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);
5814
+ }
5815
+ function isStoredRecord(value) {
5816
+ return isEmulatorInstances(value) || isSingleAppRecord(value);
5870
5817
  }
5818
+ /** A single-app record reads as a record holding that one app; the next write rewrites it. */
5819
+ function normalized(stored) {
5820
+ if (stored === void 0 || isEmulatorInstances(stored)) return stored;
5821
+ const nodes = Object.fromEntries(Object.entries(stored.nodes).map(([address, { id, port }]) => [address, {
5822
+ kind: "service",
5823
+ id,
5824
+ port
5825
+ }]));
5826
+ return {
5827
+ current: stored.app,
5828
+ apps: { [stored.app]: { nodes } }
5829
+ };
5830
+ }
5831
+ /**
5832
+ * A database's or bucket's key in the record: its resource `name`, which the
5833
+ * lowering sets from the same helper the emulators hook keys it by. A
5834
+ * resource without a name cannot be matched to the instance the hook
5835
+ * recorded, so it is refused rather than recorded under another key.
5836
+ */
5837
+ function recordKeyOf(kind, resourceId, name) {
5838
+ 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.`);
5839
+ return name;
5840
+ }
5841
+ /** The project directory that holds `devDir` (`<directory>/.prisma-composer/dev`). */
5871
5842
  function directoryOf(devDir) {
5872
- const normalized = path$1.normalize(devDir);
5873
- return normalized.endsWith(DEV_DIR) ? normalized.slice(0, -DEV_DIR.length - 1) : normalized;
5843
+ const normalizedDir = path$1.normalize(devDir);
5844
+ return normalizedDir.endsWith(DEV_DIR) ? normalizedDir.slice(0, -DEV_DIR.length - 1) : normalizedDir;
5874
5845
  }
5875
5846
  function store(devDir) {
5876
- return createStore(path$1.join(devDir, EMULATOR_INSTANCES_FILE), isEmulatorInstances, void 0);
5847
+ return createStore(path$1.join(devDir, EMULATOR_INSTANCES_FILE), isStoredRecord, void 0);
5848
+ }
5849
+ async function readRecord(devDir) {
5850
+ return normalized(await store(devDir).read());
5851
+ }
5852
+ async function updateRecord(devDir, mutate) {
5853
+ await store(devDir).update((stored) => mutate(normalized(stored)));
5877
5854
  }
5878
- /** This app's nodes. A record left by another app run from the same directory counts as empty. */
5879
- async function appNodes(scope) {
5880
- const current = await store(scope.devDir).read();
5881
- return current?.app === scope.app ? current.nodes : {};
5855
+ /** The record `prisma dev` left in `directory`; undefined when it has not run there. */
5856
+ function readEmulatorInstances(directory) {
5857
+ return readRecord(path$1.join(directory, DEV_DIR));
5858
+ }
5859
+ async function appNodes$1(scope) {
5860
+ return (await readRecord(scope.devDir))?.apps[scope.app]?.nodes ?? {};
5882
5861
  }
5883
5862
  async function updateNodes(scope, mutate) {
5884
- await store(scope.devDir).update((current) => {
5885
- const nodes = { ...current?.app === scope.app ? current.nodes : {} };
5863
+ await updateRecord(scope.devDir, (record) => {
5864
+ const nodes = { ...record?.apps[scope.app]?.nodes ?? {} };
5886
5865
  mutate(nodes);
5887
5866
  return {
5888
- app: scope.app,
5889
- nodes
5867
+ current: record?.current ?? scope.app,
5868
+ apps: {
5869
+ ...record?.apps,
5870
+ [scope.app]: { nodes }
5871
+ }
5890
5872
  };
5891
5873
  });
5892
5874
  }
5875
+ function labelsFor(scope, address) {
5876
+ return {
5877
+ "composer.directory": directoryOf(scope.devDir),
5878
+ "composer.app": scope.app,
5879
+ "composer.address": address
5880
+ };
5881
+ }
5882
+ function idsOf(nodes, kind) {
5883
+ return Object.values(nodes).flatMap((node) => node.kind === kind ? [node.id] : []);
5884
+ }
5893
5885
  /**
5894
- * A directory has one dev instance. When the record names another app, that
5895
- * app's compute instances are deleted and the record starts over for this
5896
- * one. Returns the previous app's name when it was retired.
5886
+ * Stops the service processes and database servers of `nodes`, keeping
5887
+ * their data and ports, and returns the nodes it stopped. A kind whose
5888
+ * emulator is not running has nothing running to stop.
5897
5889
  */
5898
- async function retirePreviousApp(scope) {
5899
- const current = await store(scope.devDir).read();
5900
- if (current === void 0 || current.app === scope.app) return void 0;
5901
- const client = computeClient();
5902
- for (const instance of Object.values(current.nodes)) await client.deleteInstance(instance.id);
5903
- await store(scope.devDir).update(() => ({
5904
- app: scope.app,
5905
- nodes: {}
5890
+ async function stopNodes(nodes) {
5891
+ const services = idsOf(nodes, "service");
5892
+ const databases = idsOf(nodes, "database");
5893
+ const results = await Promise.allSettled([services.length > 0 && whenRunning("service", () => computeClient().stopInstances(services)), databases.length > 0 && whenRunning("database", () => postgresClient().stopInstances(databases))]);
5894
+ const failure = results.find((result) => result.status === "rejected");
5895
+ if (failure) throw failure.reason;
5896
+ const stoppedKinds = /* @__PURE__ */ new Set();
5897
+ if (results[0].status === "fulfilled" && results[0].value === true) stoppedKinds.add("service");
5898
+ if (results[1].status === "fulfilled" && results[1].value === true) stoppedKinds.add("database");
5899
+ return Object.entries(nodes).flatMap(([address, node]) => node.kind !== "bucket" && stoppedKinds.has(node.kind) ? [{
5900
+ address,
5901
+ kind: node.kind
5902
+ }] : []);
5903
+ }
5904
+ /** Runs `action` when the emulator for `kind` is running; returns whether it ran. */
5905
+ async function whenRunning(kind, action) {
5906
+ if (!await isEmulatorRunning(kind)) return false;
5907
+ await action();
5908
+ return true;
5909
+ }
5910
+ /**
5911
+ * Makes `scope.app` the directory's current app. When another app ran here
5912
+ * last, its service processes and database servers are stopped and its
5913
+ * instances stay recorded, so running it again resumes them with their data
5914
+ * and ports. Returns the previous app's name when it changed.
5915
+ */
5916
+ async function selectApp(scope) {
5917
+ const record = await readRecord(scope.devDir);
5918
+ const previous = record?.current;
5919
+ if (previous !== void 0 && previous !== scope.app) await stopNodes(record?.apps[previous]?.nodes ?? {});
5920
+ await updateRecord(scope.devDir, (latest) => ({
5921
+ current: scope.app,
5922
+ apps: {
5923
+ ...latest?.apps,
5924
+ [scope.app]: latest?.apps[scope.app] ?? { nodes: {} }
5925
+ }
5906
5926
  }));
5907
- return current.app;
5927
+ return previous !== void 0 && previous !== scope.app ? previous : void 0;
5908
5928
  }
5909
- /** Each compute node of the app, with the instance that backs it. */
5910
- async function computeNodeInstances(scope) {
5911
- return Object.entries(await appNodes(scope)).map(([address, instance]) => ({
5929
+ /** Each node of the app of `kind`, with the instance that backs it. */
5930
+ async function nodeInstancesOf(scope, kind) {
5931
+ return Object.entries(await appNodes$1(scope)).flatMap(([address, instance]) => isOfKind(instance, kind) ? [{
5912
5932
  address,
5913
5933
  instance
5914
- }));
5934
+ }] : []);
5915
5935
  }
5916
- /** The compute instance recorded for `address`; undefined when there is none. */
5917
- async function mappedComputeInstance(scope, address) {
5918
- return (await appNodes(scope))[address];
5936
+ function isOfKind(instance, kind) {
5937
+ return instance.kind === kind;
5938
+ }
5939
+ /** The instance of `kind` recorded for `address`; undefined when there is none. */
5940
+ async function recordedInstance(scope, address, kind) {
5941
+ const recorded = (await appNodes$1(scope))[address];
5942
+ return recorded !== void 0 && isOfKind(recorded, kind) ? recorded : void 0;
5919
5943
  }
5920
5944
  /**
5921
5945
  * The instance that runs the service at `address`. The recorded instance is
5922
5946
  * reused while the Compute emulator still has it. Otherwise a new one is
5923
5947
  * created, requesting the recorded port, and recorded.
5924
5948
  */
5925
- async function ensureComputeInstance(scope, address) {
5949
+ async function ensureServiceInstance(scope, address) {
5926
5950
  const client = computeClient();
5927
- const recorded = await mappedComputeInstance(scope, address);
5951
+ const recorded = await recordedInstance(scope, address, "service");
5928
5952
  if (recorded && await client.getInstance(recorded.id)) return { instance: recorded };
5929
5953
  const created = await client.createInstance({
5930
- labels: {
5931
- "composer.directory": directoryOf(scope.devDir),
5932
- "composer.app": scope.app,
5933
- "composer.address": address
5934
- },
5954
+ labels: labelsFor(scope, address),
5935
5955
  ...recorded ? { port: recorded.port } : {}
5936
5956
  });
5937
5957
  const instance = {
5938
- kind: "compute",
5958
+ kind: "service",
5939
5959
  id: created.id,
5940
5960
  port: created.port
5941
5961
  };
5942
5962
  await updateNodes(scope, (nodes) => {
5943
5963
  nodes[address] = instance;
5944
5964
  });
5945
- if (recorded && recorded.port !== created.port) return {
5965
+ return {
5946
5966
  instance,
5947
- portChange: {
5948
- address,
5949
- previous: recorded.port,
5950
- given: created.port
5951
- }
5967
+ ...portChangeOf("service", address, recorded?.port, created.port)
5952
5968
  };
5953
- return { instance };
5954
5969
  }
5955
- /** The emulator's view of the instance recorded for `address`; undefined when either is missing. */
5956
- async function liveComputeInstance(scope, address) {
5957
- const recorded = await mappedComputeInstance(scope, address);
5970
+ function portChangeOf(kind, address, previous, given) {
5971
+ return previous !== void 0 && previous !== given ? { portChange: {
5972
+ kind,
5973
+ address,
5974
+ previous,
5975
+ given
5976
+ } } : {};
5977
+ }
5978
+ /**
5979
+ * The running database for `address`. A recorded database the Postgres
5980
+ * emulator still has is started if it is stopped. Otherwise a new one is
5981
+ * created, requesting the recorded port, and recorded; it starts empty.
5982
+ */
5983
+ async function ensureDatabaseInstance(scope, address, prismaDevModulePath) {
5984
+ const client = postgresClient();
5985
+ const recorded = await recordedInstance(scope, address, "database");
5986
+ const existing = recorded ? await client.getInstance(recorded.id) : void 0;
5987
+ if (existing?.status === "running") return { instance: existing };
5988
+ if (existing) {
5989
+ await client.startInstances([existing.id], prismaDevModulePath);
5990
+ return { instance: await client.getInstance(existing.id) ?? existing };
5991
+ }
5992
+ const created = await client.createInstance({
5993
+ labels: labelsFor(scope, address),
5994
+ ...recorded ? { port: recorded.port } : {},
5995
+ prismaDevModulePath
5996
+ });
5997
+ await updateNodes(scope, (nodes) => {
5998
+ nodes[address] = {
5999
+ kind: "database",
6000
+ id: created.id,
6001
+ port: created.port
6002
+ };
6003
+ });
6004
+ return {
6005
+ instance: created,
6006
+ ...portChangeOf("database", address, recorded?.port, created.port)
6007
+ };
6008
+ }
6009
+ /**
6010
+ * The bucket for `address`. The recorded bucket is reused while the bucket
6011
+ * emulator still has it. Otherwise a new one is created over `dir` and
6012
+ * recorded; the objects already in `dir` are its objects.
6013
+ */
6014
+ async function ensureBucketInstance(scope, address, dir) {
6015
+ const client = bucketsClient();
6016
+ const recorded = await recordedInstance(scope, address, "bucket");
6017
+ const existing = recorded ? await client.getInstance(recorded.id) : void 0;
6018
+ if (existing) return existing;
6019
+ const created = await client.createInstance({
6020
+ labels: labelsFor(scope, address),
6021
+ dir
6022
+ });
6023
+ await updateNodes(scope, (nodes) => {
6024
+ nodes[address] = {
6025
+ kind: "bucket",
6026
+ id: created.id
6027
+ };
6028
+ });
6029
+ return created;
6030
+ }
6031
+ /** The Compute emulator's view of the instance recorded for `address`; undefined when either is missing. */
6032
+ async function liveServiceInstance(scope, address) {
6033
+ const recorded = await recordedInstance(scope, address, "service");
5958
6034
  return recorded ? computeClient().getInstance(recorded.id) : void 0;
5959
6035
  }
6036
+ /** The id recorded for `address`; undefined when there is none. */
6037
+ async function recordedInstanceId(scope, address) {
6038
+ return (await appNodes$1(scope))[address]?.id;
6039
+ }
6040
+ async function deleteInstance(instance) {
6041
+ if (instance.kind === "service") await computeClient().deleteInstance(instance.id);
6042
+ else if (instance.kind === "database") await postgresClient().deleteInstance(instance.id, resolvePrismaDevModulePath());
6043
+ else await bucketsClient().deleteInstance(instance.id);
6044
+ }
5960
6045
  /** Deletes the instance recorded for `address` and forgets it. */
5961
- async function deleteComputeInstance(scope, address) {
5962
- const recorded = await mappedComputeInstance(scope, address);
6046
+ async function deleteNodeInstance(scope, address) {
6047
+ const recorded = (await appNodes$1(scope))[address];
5963
6048
  if (!recorded) return;
5964
- await computeClient().deleteInstance(recorded.id);
6049
+ await deleteInstance(recorded);
5965
6050
  await updateNodes(scope, (nodes) => {
5966
6051
  delete nodes[address];
5967
6052
  });
5968
6053
  }
6054
+ /** The kinds of instance recorded for the app, so a caller can start their emulators first. */
6055
+ async function recordedKinds(scope) {
6056
+ return new Set(Object.values(await appNodes$1(scope)).map((node) => node.kind));
6057
+ }
5969
6058
  /**
5970
- * `--fresh`: deletes every instance the directory's record names, whichever
5971
- * app wrote it, then empties the record. Teardown runs before the emulators
5972
- * hook, so the record may still name the app run here before.
6059
+ * `--fresh`: deletes the app's recorded instances of every kind and its
6060
+ * entry. Other apps' entries are left alone. A bucket's objects live in the
6061
+ * dev directory, which `--fresh` removes separately.
5973
6062
  */
5974
- async function deleteRecordedInstances(devDir) {
5975
- const current = await store(devDir).read();
5976
- if (current === void 0) return;
5977
- const client = computeClient();
5978
- for (const instance of Object.values(current.nodes)) await client.deleteInstance(instance.id);
5979
- await store(devDir).update(() => ({
5980
- app: current.app,
5981
- nodes: {}
5982
- }));
6063
+ async function deleteAppInstances(scope) {
6064
+ for (const instance of Object.values(await appNodes$1(scope))) await deleteInstance(instance);
6065
+ await updateRecord(scope.devDir, (record) => {
6066
+ const apps = { ...record?.apps };
6067
+ delete apps[scope.app];
6068
+ return {
6069
+ current: record?.current ?? scope.app,
6070
+ apps
6071
+ };
6072
+ });
6073
+ }
6074
+ /** When `prisma dev` ends: stops the app's service processes and database servers, keeping their data and ports. */
6075
+ async function stopAppInstances(scope) {
6076
+ return stopNodes(await appNodes$1(scope));
5983
6077
  }
5984
6078
  /**
5985
- * The Compute emulator's failure messages name instances by id; `prisma dev`
5986
- * names services by address. Rewrites a plain `Error`'s message and leaves
5987
- * any other error, such as a structured one, untouched.
6079
+ * The emulators' failure messages name instances by id; `prisma dev` names
6080
+ * services by address. Rewrites a plain `Error`'s message and leaves any
6081
+ * other error, such as a structured one, untouched.
5988
6082
  */
5989
6083
  function withServiceAddresses(error, nodes) {
5990
6084
  if (!(error instanceof Error) || error.constructor !== Error) return error;
@@ -5993,6 +6087,204 @@ function withServiceAddresses(error, nodes) {
5993
6087
  return message === error.message ? error : new Error(message, { cause: error });
5994
6088
  }
5995
6089
  /**
6090
+ * Shared helpers for the upstream-attribute records every local provider
6091
+ * emits — one implementation, so the compute and postgres families cannot
6092
+ * drift on the `'local'` project fallback or the timestamp.
6093
+ */
6094
+ /** The fixed `createdAt`/`updatedAt` local providers stamp: local dev has no meaningful creation time. */
6095
+ const DEV_TIMESTAMP = "1970-01-01T00:00:00.000Z";
6096
+ /** Reads a project id from upstream's `project` input: a plain string or a resolved `Prisma.Project` attributes record. */
6097
+ function projectIdOfInput(value) {
6098
+ if (typeof value === "string") return value;
6099
+ if (Predicate.isObject(value) && typeof value["projectId"] === "string") return value["projectId"];
6100
+ return "local";
6101
+ }
6102
+ /**
6103
+ * Local bucket-cluster providers (local-dev spec § 4): both `Bucket` and
6104
+ * `BucketAccessKey` are clients of the machine-global bucket emulator —
6105
+ * already up by the time these run, since the extension's
6106
+ * `localTarget.emulators` hook ensures it before converge.
6107
+ */
6108
+ /** Reads a bucket id from upstream's `bucket` input: a plain string or a resolved `Prisma.Bucket` attributes record. */
6109
+ function bucketIdOfInput(value) {
6110
+ if (typeof value === "string") return value;
6111
+ if (Predicate.isObject(value) && typeof value["bucketId"] === "string") return value["bucketId"];
6112
+ return "local";
6113
+ }
6114
+ const BUCKETS_DIR = "buckets";
6115
+ /** Where a bucket's objects live: in the project's dev directory, under its app, so `--fresh` removes them with the app. */
6116
+ function bucketDataDir(scope, name) {
6117
+ return path$1.join(scope.devDir, BUCKETS_DIR, scope.app, name);
6118
+ }
6119
+ function appScopeOf$2(input) {
6120
+ return {
6121
+ devDir: input.devDir,
6122
+ app: appNameOf(input.container)
6123
+ };
6124
+ }
6125
+ /**
6126
+ * `Bucket` → the bucket instance the directory's record names for the
6127
+ * resource, created when there is none, over an in-project data root. Its
6128
+ * `bucketId` is the instance id, which is also its S3 bucket name. A Bucket
6129
+ * whose recorded instance changed is updated, so its access key is
6130
+ * registered on the new instance.
6131
+ */
6132
+ function LocalBucketProvider(input) {
6133
+ return Provider.effect(Bucket, Effect.succeed({
6134
+ list: () => Effect.succeed([]),
6135
+ reconcile: ({ news, id }) => Effect.tryPromise({
6136
+ try: async () => {
6137
+ const name = recordKeyOf("bucket", id, news.name);
6138
+ return {
6139
+ bucketId: (await ensureBucketInstance(appScopeOf$2(input), name, bucketDataDir(appScopeOf$2(input), name))).id,
6140
+ name,
6141
+ projectId: projectIdOfInput(news.project),
6142
+ createdAt: DEV_TIMESTAMP,
6143
+ logicalId: null
6144
+ };
6145
+ },
6146
+ catch: (cause) => cause
6147
+ }),
6148
+ diff: ({ output }) => Effect.tryPromise({
6149
+ try: async () => {
6150
+ if (output === void 0) return void 0;
6151
+ return await recordedInstanceId(appScopeOf$2(input), output.name) === output.bucketId ? void 0 : { action: "update" };
6152
+ },
6153
+ catch: (cause) => cause
6154
+ }),
6155
+ delete: () => Effect.void,
6156
+ read: ({ output }) => Effect.succeed(output)
6157
+ }));
6158
+ }
6159
+ /**
6160
+ * `BucketAccessKey` → mint-once-stable (the same lifecycle as `ServiceKey`/
6161
+ * `S3Credentials`: reuse `output`'s pair when present, mint only on first
6162
+ * create) — but ALWAYS re-registers the (prior or fresh) pair with the
6163
+ * emulator, so a bucket emulator whose own state was wiped self-heals on the
6164
+ * next converge.
6165
+ */
6166
+ function LocalBucketAccessKeyProvider(_input) {
6167
+ return Provider.effect(BucketAccessKey, Effect.succeed({
6168
+ list: () => Effect.succeed([]),
6169
+ reconcile: ({ news, output, id }) => Effect.tryPromise({
6170
+ try: async () => {
6171
+ const pair = output !== void 0 ? {
6172
+ accessKeyId: output.accessKeyId,
6173
+ secretAccessKey: Redacted.value(output.secretAccessKey)
6174
+ } : mintKeyPair();
6175
+ const client = bucketsClient();
6176
+ const bucketId = bucketIdOfInput(news.bucket);
6177
+ await client.putCredentials(bucketId, pair.accessKeyId, pair.secretAccessKey);
6178
+ return {
6179
+ bucketAccessKeyId: news.name ?? id,
6180
+ bucketId,
6181
+ accessKeyId: pair.accessKeyId,
6182
+ secretAccessKey: Redacted.make(pair.secretAccessKey),
6183
+ endpoint: client.baseUrl,
6184
+ bucketName: bucketId
6185
+ };
6186
+ },
6187
+ catch: (cause) => cause
6188
+ }),
6189
+ delete: () => Effect.void,
6190
+ read: ({ output }) => Effect.succeed(output)
6191
+ }));
6192
+ }
6193
+ /**
6194
+ * The emulators hook's half of the directory's record: before Alchemy runs,
6195
+ * every node of the app gets its emulator instance, one at a time in the
6196
+ * order given.
6197
+ */
6198
+ /**
6199
+ * Ensures each node's instance, in the order given: a recorded instance is
6200
+ * reused (a stopped database is started), a missing one is created again on
6201
+ * its remembered port, and a new node gets a new instance. A new instance
6202
+ * gets the smallest free port, so on a fresh start the ports follow this
6203
+ * order. Returns the nodes whose re-created instance could not get its
6204
+ * remembered port back.
6205
+ */
6206
+ async function ensureAppInstances(scope, nodes) {
6207
+ const changes = [];
6208
+ for (const { address, kind } of nodes) try {
6209
+ const { portChange } = await ensureNodeInstance(scope, address, kind);
6210
+ if (portChange) changes.push(portChange);
6211
+ } catch (cause) {
6212
+ const reason = cause instanceof Error ? cause.message : String(cause);
6213
+ throw new Error(`ensuring the local instance for ${kind} "${address}" failed: ${reason}`, { cause });
6214
+ }
6215
+ return changes;
6216
+ }
6217
+ async function ensureNodeInstance(scope, address, kind) {
6218
+ if (kind === "service") return ensureServiceInstance(scope, address);
6219
+ if (kind === "database") return ensureDatabaseInstance(scope, address, resolvePrismaDevModulePath());
6220
+ await ensureBucketInstance(scope, address, bucketDataDir(scope, address));
6221
+ return {};
6222
+ }
6223
+ /**
6224
+ * Extracts the tar.gz `@internal/lowering`'s `packageComputeArtifact` writes
6225
+ * (local-dev spec § 4) — emulation-only code, so it lives here rather than
6226
+ * beside the writer (production source stays physically separated from
6227
+ * emulation). Reading tar is commodity even though the writer is a
6228
+ * deterministic subset we own (fixed mtimes, sorted entries): the maintained
6229
+ * `tar` package (dependency razor) does the parsing; this module keeps only
6230
+ * the pinned entry filtering (regular files plus safe relative symlinks,
6231
+ * reject devices/path escapes) and the directory-level temp-then-rename.
6232
+ */
6233
+ function unsupportedEntryError(entryPath, type) {
6234
+ return /* @__PURE__ */ new Error(`compute artifact entry "${entryPath}" has type "${type}" — only regular files and safe symlinks are supported; this artifact was not produced by packageComputeArtifact.`);
6235
+ }
6236
+ function pathEscapeError(entryPath) {
6237
+ return /* @__PURE__ */ new Error(`compute artifact entry "${entryPath}" escapes the extraction directory — this artifact was not produced by packageComputeArtifact.`);
6238
+ }
6239
+ const REGULAR_FILE_TYPES = /* @__PURE__ */ new Set([
6240
+ "File",
6241
+ "OldFile",
6242
+ "ContiguousFile"
6243
+ ]);
6244
+ const SYMLINK_TYPE = "SymbolicLink";
6245
+ /**
6246
+ * Extracts `tarGzPath` (a `packageComputeArtifact` tar.gz) into `destDir`,
6247
+ * temp-then-rename at the directory level: entries are written into a
6248
+ * sibling temp directory first, then that directory is renamed onto
6249
+ * `destDir` — a concurrent unpack of the SAME hash races benignly (last
6250
+ * rename wins, both write identical bytes), and a reader of `destDir` never
6251
+ * sees a partial tree.
6252
+ */
6253
+ function extractComputeArtifact(tarGzPath, destDir) {
6254
+ const parentDir = path$1.dirname(destDir);
6255
+ fs$2.mkdirSync(parentDir, { recursive: true });
6256
+ const tmpDir = path$1.join(parentDir, `.${path$1.basename(destDir)}.${crypto$1.randomUUID()}.tmp`);
6257
+ fs$2.mkdirSync(tmpDir, { recursive: true });
6258
+ try {
6259
+ So({
6260
+ file: tarGzPath,
6261
+ cwd: tmpDir,
6262
+ sync: true,
6263
+ onentry: (entry) => {
6264
+ const resolved = path$1.resolve(tmpDir, entry.path);
6265
+ if (!isWithin(tmpDir, resolved)) throw pathEscapeError(entry.path);
6266
+ if (REGULAR_FILE_TYPES.has(entry.type)) return;
6267
+ if (entry.type !== SYMLINK_TYPE) throw unsupportedEntryError(entry.path, entry.type);
6268
+ if (entry.linkpath === void 0) throw unsupportedEntryError(entry.path, `${entry.type} without a target`);
6269
+ if (path$1.isAbsolute(entry.linkpath)) throw pathEscapeError(`${entry.path} -> ${entry.linkpath}`);
6270
+ const linkTarget = path$1.resolve(path$1.dirname(resolved), entry.linkpath);
6271
+ if (!isWithin(tmpDir, linkTarget)) throw pathEscapeError(`${entry.path} -> ${entry.linkpath}`);
6272
+ }
6273
+ });
6274
+ fs$2.rmSync(destDir, {
6275
+ recursive: true,
6276
+ force: true
6277
+ });
6278
+ fs$2.renameSync(tmpDir, destDir);
6279
+ } catch (error) {
6280
+ fs$2.rmSync(tmpDir, {
6281
+ recursive: true,
6282
+ force: true
6283
+ });
6284
+ throw error;
6285
+ }
6286
+ }
6287
+ /**
5996
6288
  * Local compute-cluster providers: upstream alchemy's `Prisma.App` and
5997
6289
  * `Prisma.Deployment` become clients of the machine-scoped Compute emulator;
5998
6290
  * `Prisma.EnvironmentVariable` becomes a row in the dev env store;
@@ -6106,7 +6398,7 @@ async function materializeEnv(devDir, address, port) {
6106
6398
  if (process.env["HOME"] !== void 0) env["HOME"] = process.env["HOME"];
6107
6399
  return env;
6108
6400
  }
6109
- function appScopeOf(input) {
6401
+ function appScopeOf$1(input) {
6110
6402
  return {
6111
6403
  devDir: input.devDir,
6112
6404
  app: appNameOf(input.container)
@@ -6128,7 +6420,7 @@ function LocalAppProvider(input) {
6128
6420
  reconcile: ({ id, news }) => Effect.tryPromise({
6129
6421
  try: async () => {
6130
6422
  const name = news.displayName ?? id;
6131
- const { instance } = await ensureComputeInstance(appScopeOf(input), name);
6423
+ const { instance } = await ensureServiceInstance(appScopeOf$1(input), name);
6132
6424
  const url = localUrl(instance.port);
6133
6425
  return {
6134
6426
  appId: name,
@@ -6147,41 +6439,18 @@ function LocalAppProvider(input) {
6147
6439
  diff: ({ output }) => Effect.tryPromise({
6148
6440
  try: async () => {
6149
6441
  if (output === void 0) return void 0;
6150
- if ((await liveComputeInstance(appScopeOf(input), output.name))?.url === output.appEndpointDomain) return void 0;
6442
+ if ((await liveServiceInstance(appScopeOf$1(input), output.name))?.url === output.appEndpointDomain) return void 0;
6151
6443
  return { action: "update" };
6152
6444
  },
6153
6445
  catch: (cause) => cause
6154
6446
  }),
6155
6447
  delete: ({ output }) => Effect.tryPromise({
6156
- try: () => deleteComputeInstance(appScopeOf(input), output.name),
6448
+ try: () => deleteNodeInstance(appScopeOf$1(input), output.name),
6157
6449
  catch: (cause) => cause
6158
6450
  }),
6159
6451
  read: ({ output }) => Effect.succeed(output)
6160
6452
  }));
6161
6453
  }
6162
- /**
6163
- * Ensures each service's emulator instance one at a time, in the order given,
6164
- * before Alchemy applies the `App` resources concurrently. A new instance gets
6165
- * the smallest free port, so on a fresh start the ports follow this order; a
6166
- * service that already has an instance keeps its port. `serviceAppNames` are
6167
- * the `App` resources' `displayName`s. Returns the services whose instance had
6168
- * to be re-created and could not get its remembered port back.
6169
- */
6170
- async function ensureServiceInstances(container, devDir, serviceAppNames) {
6171
- const scope = {
6172
- devDir,
6173
- app: appNameOf(container)
6174
- };
6175
- const changes = [];
6176
- for (const service of serviceAppNames) try {
6177
- const { portChange } = await ensureComputeInstance(scope, service);
6178
- if (portChange) changes.push(portChange);
6179
- } catch (cause) {
6180
- const reason = cause instanceof Error ? cause.message : String(cause);
6181
- throw new Error(`ensuring the local service for service "${service}" failed: ${reason}`, { cause });
6182
- }
6183
- return changes;
6184
- }
6185
6454
  /** `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. */
6186
6455
  function LocalEnvironmentVariableProvider(input) {
6187
6456
  return Provider.effect(EnvironmentVariable, Effect.succeed({
@@ -6239,7 +6508,7 @@ function LocalDeploymentProvider(input) {
6239
6508
  const artifactDir = path$1.join(input.devDir, "artifacts", artifactHash);
6240
6509
  if (!fs$2.existsSync(artifactDir)) extractComputeArtifact(news.artifactPath, artifactDir);
6241
6510
  const { address, listens } = readComputeManifest(artifactDir);
6242
- const { instance } = await ensureComputeInstance(appScopeOf(input), appId);
6511
+ const { instance } = await ensureServiceInstance(appScopeOf$1(input), appId);
6243
6512
  const env = await materializeEnv(input.devDir, address, instance.port);
6244
6513
  try {
6245
6514
  await computeClient().putDeployment(instance.id, {
@@ -6270,7 +6539,7 @@ function LocalDeploymentProvider(input) {
6270
6539
  diff: ({ output }) => Effect.tryPromise({
6271
6540
  try: async () => {
6272
6541
  if (output === void 0) return void 0;
6273
- if ((await liveComputeInstance(appScopeOf(input), output.appId))?.artifactHash === output.artifactHash) return void 0;
6542
+ if ((await liveServiceInstance(appScopeOf$1(input), output.appId))?.artifactHash === output.artifactHash) return void 0;
6274
6543
  return { action: "update" };
6275
6544
  },
6276
6545
  catch: (cause) => cause
@@ -6304,57 +6573,106 @@ function LocalProjectProvider(_input) {
6304
6573
  delete: () => Effect.void
6305
6574
  }));
6306
6575
  }
6307
- /**
6308
- * Local postgres-cluster providers: upstream alchemy's `Prisma.Database` and
6309
- * `Prisma.Connection` become clients of the `postgres-main` emulator daemon
6310
- * (one named, persistent `@prisma/dev` server per `Database` resource).
6311
- * `PgWarm`/`OrmMigration` are not here — the hosted ones run against whatever
6312
- * URL they are handed. Attributes match upstream's shapes; the daemon's
6313
- * DIRECT connection string maps to `directConnectionString` and
6314
- * `databaseUrl`, everything else is left absent. Instance names come from
6315
- * `@internal/dev-emulators`' own `instanceNameFor` — a locally re-derived
6316
- * slug drifted from the daemon's and broke `Connection`'s lookup.
6317
- */
6576
+ const CONDITION_OF = {
6577
+ running: "running",
6578
+ stopped: "stopped",
6579
+ backoff: "crashing",
6580
+ held: "crashing"
6581
+ };
6582
+ /** A node whose emulator no longer has the instance the record names. */
6583
+ function missing(address, kind) {
6584
+ return {
6585
+ address,
6586
+ kind,
6587
+ emulator: "running",
6588
+ state: "missing",
6589
+ condition: "missing"
6590
+ };
6591
+ }
6592
+ async function locate(address, node) {
6593
+ const kind = node.kind;
6594
+ if (!await isEmulatorRunning(kind)) return {
6595
+ address,
6596
+ kind,
6597
+ emulator: "not-running"
6598
+ };
6599
+ if (node.kind === "service") {
6600
+ const instance = await computeClient().getInstance(node.id);
6601
+ if (instance === void 0) return missing(address, kind);
6602
+ return {
6603
+ address,
6604
+ kind,
6605
+ emulator: "running",
6606
+ url: instance.url,
6607
+ state: instance.status,
6608
+ condition: CONDITION_OF[instance.status],
6609
+ logPath: instance.logPath
6610
+ };
6611
+ }
6612
+ if (node.kind === "database") {
6613
+ const instance = await postgresClient().getInstance(node.id);
6614
+ if (instance === void 0) return missing(address, kind);
6615
+ return {
6616
+ address,
6617
+ kind,
6618
+ emulator: "running",
6619
+ url: instance.url,
6620
+ state: instance.status,
6621
+ condition: instance.status
6622
+ };
6623
+ }
6624
+ const client = bucketsClient();
6625
+ if (await client.getInstance(node.id) === void 0) return missing(address, kind);
6626
+ return {
6627
+ address,
6628
+ kind,
6629
+ emulator: "running",
6630
+ url: `${client.baseUrl}/${node.id}`,
6631
+ state: "running",
6632
+ condition: "running"
6633
+ };
6634
+ }
6635
+ /** The directory's current app; undefined when `prisma dev` never ran there. */
6636
+ async function currentApp(devDir) {
6637
+ const record = await readEmulatorInstances(directoryOf(devDir));
6638
+ if (record === void 0) return void 0;
6639
+ const app = record.current;
6640
+ const nodes = record.apps[app]?.nodes ?? {};
6641
+ return {
6642
+ app,
6643
+ addresses: Object.keys(nodes),
6644
+ locate: async (address) => {
6645
+ const node = nodes[address];
6646
+ return node === void 0 ? void 0 : locate(address, node);
6647
+ },
6648
+ stop: () => stopAppInstances({
6649
+ devDir,
6650
+ app
6651
+ })
6652
+ };
6653
+ }
6318
6654
  /** Reads a database id from upstream's `database` input: a plain string or a resolved `Prisma.Database` attributes record. */
6319
6655
  function databaseIdOfInput(value) {
6320
6656
  if (typeof value === "string") return value;
6321
6657
  if (Predicate.isObject(value) && typeof value["databaseId"] === "string") return value["databaseId"];
6322
6658
  }
6323
- function noPrismaDevError(cause) {
6324
- 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)})`);
6325
- }
6326
6659
  /**
6327
- * Composer owns the local Postgres emulator's version: `@prisma/dev` is a
6328
- * dependency of `@prisma/composer-prisma-cloud` and is resolved from
6329
- * Composer's own package, never from the app. The app's copy (if any) is
6330
- * ignored on purpose — an old `@prisma/dev` pulled in transitively (alchemy
6331
- * pins `^0.20.0`, whose pglite-socket crashes on any message over 64 KiB)
6332
- * would otherwise silently replace the version Composer tested against.
6333
- * The daemon imports the returned path dynamically.
6334
- */
6335
- function resolvePrismaDevModulePath() {
6336
- try {
6337
- return createRequire(import.meta.url).resolve("@prisma/dev");
6338
- } catch (cause) {
6339
- throw noPrismaDevError(cause);
6340
- }
6341
- }
6342
- /**
6343
- * `Prisma.Database` → an ensured `postgres-main` server, one per resource.
6344
- * Stores the daemon's returned url as the `directConnectionString` attribute.
6660
+ * `Prisma.Database` → the database instance the directory's record names for
6661
+ * the resource, started if it is stopped and created when there is none.
6662
+ * Stores its url as the `directConnectionString` attribute. A Database whose
6663
+ * recorded instance changed (re-created after the emulator lost it) is
6664
+ * updated, so its Connection and dependents get the new instance's URL.
6345
6665
  */
6346
6666
  function LocalDatabaseProvider(input) {
6347
6667
  return Provider.effect(Prisma.Database, Effect.succeed({
6348
6668
  list: () => Effect.succeed([]),
6349
6669
  reconcile: ({ id, news }) => Effect.tryPromise({
6350
6670
  try: async () => {
6351
- const app = appNameOf(input.container);
6352
- const name = news.name ?? id;
6353
- const prismaDevModulePath = resolvePrismaDevModulePath();
6354
- const { url } = await postgresClient().ensureDatabase(app, slug(name), prismaDevModulePath);
6355
- const direct = Redacted.make(url);
6671
+ const name = recordKeyOf("database", id, news.name);
6672
+ const { instance } = await ensureDatabaseInstance(appScopeOf(input), name, resolvePrismaDevModulePath());
6673
+ const direct = Redacted.make(instance.url);
6356
6674
  return {
6357
- databaseId: instanceNameFor(app, name),
6675
+ databaseId: instance.id,
6358
6676
  databaseName: name,
6359
6677
  projectId: projectIdOfInput(news.project),
6360
6678
  status: "ready",
@@ -6374,28 +6692,40 @@ function LocalDatabaseProvider(input) {
6374
6692
  },
6375
6693
  catch: (cause) => cause
6376
6694
  }),
6695
+ diff: ({ output }) => Effect.tryPromise({
6696
+ try: async () => {
6697
+ if (output === void 0 || output.databaseName === void 0) return void 0;
6698
+ return await recordedInstanceId(appScopeOf(input), output.databaseName) === output.databaseId ? void 0 : { action: "update" };
6699
+ },
6700
+ catch: (cause) => cause
6701
+ }),
6377
6702
  delete: () => Effect.void
6378
6703
  }));
6379
6704
  }
6705
+ function appScopeOf(input) {
6706
+ return {
6707
+ devDir: input.devDir,
6708
+ app: appNameOf(input.container)
6709
+ };
6710
+ }
6380
6711
  function noRecordedInstanceError(databaseId) {
6381
6712
  return /* @__PURE__ */ new Error(`no local Postgres instance recorded for databaseId "${databaseId}" — the Database provider did not run; converge is corrupt (try --fresh).`);
6382
6713
  }
6383
- /** `Prisma.Connection` → the daemon's live listing, matched by instance name (the Database attributes' `databaseId` IS the instance name). */
6384
- function LocalConnectionProvider(input) {
6714
+ /** `Prisma.Connection` → the database instance the Database attributes' `databaseId` names. */
6715
+ function LocalConnectionProvider(_input) {
6385
6716
  return Provider.effect(Prisma.Connection, Effect.succeed({
6386
6717
  list: () => Effect.succeed([]),
6387
6718
  reconcile: ({ id, news }) => Effect.tryPromise({
6388
6719
  try: async () => {
6389
- const app = appNameOf(input.container);
6390
6720
  const databaseId = databaseIdOfInput(news.database);
6391
6721
  if (databaseId === void 0) throw noRecordedInstanceError(String(news.database));
6392
- const found = (await postgresClient().listDatabases(app)).find((entry) => entry.instanceName === databaseId);
6722
+ const found = await postgresClient().getInstance(databaseId);
6393
6723
  if (found === void 0) throw noRecordedInstanceError(databaseId);
6394
6724
  const direct = Redacted.make(found.url);
6395
6725
  return {
6396
- connectionId: found.instanceName,
6726
+ connectionId: found.id,
6397
6727
  connectionName: news.name ?? id,
6398
- databaseId: found.instanceName,
6728
+ databaseId: found.id,
6399
6729
  kind: "postgres",
6400
6730
  createdAt: DEV_TIMESTAMP,
6401
6731
  directConnectionString: direct,
@@ -6441,10 +6771,8 @@ function resolvePackageEntry(specifier) {
6441
6771
  * ADR-0041 D12). The target extension's `dev/teardown.ts` orchestrates
6442
6772
  * (which app, which emulator clients); this module owns the actual
6443
6773
  * filesystem operations, so the extension's own source stays free of
6444
- * `node:`/`bun:` imports (its invariant 5). Postgres instance removal is
6445
- * NOT here (REVISED — operator review of #162): it is a
6446
- * `postgresClient().deleteApp(app)` call, made directly alongside the
6447
- * compute/buckets clients — no filesystem or child-process work of its own.
6774
+ * `node:`/`bun:` imports (its invariant 5). Emulator instances are not
6775
+ * removed here: the extension deletes them through the directory's record.
6448
6776
  */
6449
6777
  /** `fs.rm(..., { recursive: true, force: true })` for every given path — tolerates absence. */
6450
6778
  function removeLocalPaths(paths) {
@@ -6453,6 +6781,21 @@ function removeLocalPaths(paths) {
6453
6781
  force: true
6454
6782
  });
6455
6783
  }
6784
+ /**
6785
+ * Removes the dev state directory's contents for `--fresh`, except what
6786
+ * other apps run from the directory still use: the record of their emulator
6787
+ * instances and their buckets' objects. This app's objects go.
6788
+ */
6789
+ function removeAppDevState(devDir, app) {
6790
+ let entries;
6791
+ try {
6792
+ entries = fs$2.readdirSync(devDir);
6793
+ } catch {
6794
+ return;
6795
+ }
6796
+ const kept = /* @__PURE__ */ new Set([EMULATOR_INSTANCES_FILE, BUCKETS_DIR]);
6797
+ removeLocalPaths([...entries.filter((entry) => !kept.has(entry)).map((entry) => path$1.join(devDir, entry)), path$1.join(devDir, BUCKETS_DIR, app)]);
6798
+ }
6456
6799
  //#endregion
6457
6800
  //#region ../../1-prisma-cloud/1-extensions/target/dist/local-target.mjs
6458
6801
  const RELIST_INTERVAL_MS = 2e3;
@@ -6500,7 +6843,7 @@ async function* mergedLogs(scope, signal, tail) {
6500
6843
  })();
6501
6844
  };
6502
6845
  const relist = async () => {
6503
- for (const { address, instance } of await computeNodeInstances(scope)) follow(instance.id, address);
6846
+ for (const { address, instance } of await nodeInstancesOf(scope, "service")) follow(instance.id, address);
6504
6847
  };
6505
6848
  await relist();
6506
6849
  const timer = setInterval(() => void relist(), RELIST_INTERVAL_MS);
@@ -6532,7 +6875,7 @@ async function devAttach(input) {
6532
6875
  const client = computeClient();
6533
6876
  return {
6534
6877
  startServices: async () => {
6535
- const nodes = await computeNodeInstances(scope);
6878
+ const nodes = await nodeInstancesOf(scope, "service");
6536
6879
  try {
6537
6880
  await client.startInstances(nodes.map(({ instance }) => instance.id));
6538
6881
  } catch (error) {
@@ -6540,15 +6883,14 @@ async function devAttach(input) {
6540
6883
  }
6541
6884
  },
6542
6885
  endpoints: async () => {
6543
- return (await computeNodeInstances(scope)).map(({ address, instance }) => ({
6886
+ return (await nodeInstancesOf(scope, "service")).map(({ address, instance }) => ({
6544
6887
  address,
6545
6888
  url: `http://localhost:${String(instance.port)}`
6546
6889
  }));
6547
6890
  },
6548
6891
  logs: (signal, opts) => mergedLogs(scope, signal, opts?.tail ?? 0),
6549
- stopServices: async () => {
6550
- const nodes = await computeNodeInstances(scope);
6551
- await client.stopInstances(nodes.map(({ instance }) => instance.id));
6892
+ stopApp: async () => {
6893
+ await stopAppInstances(scope);
6552
6894
  }
6553
6895
  };
6554
6896
  }
@@ -6571,34 +6913,54 @@ function devContainerDescriptor() {
6571
6913
  function daemonEntry(name) {
6572
6914
  return resolvePackageEntry(`@prisma/composer-prisma-cloud/local-target/${name}-main`);
6573
6915
  }
6574
- function usesBuckets(input) {
6575
- return input.graph.nodes.some((n) => n.node.kind === "resource" && n.node.type === "s3");
6576
- }
6577
- function usesPostgres(input) {
6578
- return input.graph.nodes.some((n) => n.node.kind === "resource" && (n.node.type === "raw-postgres" || n.node.type === "postgres"));
6579
- }
6580
- /** The `App` name of each of this extension's services, in `graph.nodes` order: dependencies first, ties in declaration order. */
6581
- function serviceAppNames(input) {
6582
- return input.graph.nodes.filter((n) => n.node.kind === "service" && n.node.extension === "@prisma/composer-prisma-cloud").map((n) => serviceAppName(n.id));
6916
+ const DATABASE_TYPES = /* @__PURE__ */ new Set(["raw-postgres", "postgres"]);
6917
+ const BUCKET_TYPE = "s3";
6918
+ /**
6919
+ * This extension's nodes that run on an emulator, in `graph.nodes` order —
6920
+ * dependencies first, ties in declaration order — keyed by the name their
6921
+ * resource is given: a service by its `App` name, a database and a bucket by
6922
+ * their resource name — the keys the local providers look them up by.
6923
+ */
6924
+ function appNodes(input) {
6925
+ return input.graph.nodes.flatMap((n) => {
6926
+ if (n.node.kind === "service" && n.node.extension === "@prisma/composer-prisma-cloud") return [{
6927
+ address: serviceAppName(n.id),
6928
+ kind: "service"
6929
+ }];
6930
+ if (n.node.kind === "resource" && DATABASE_TYPES.has(n.node.type)) return [{
6931
+ address: resourceName(n.id),
6932
+ kind: "database"
6933
+ }];
6934
+ if (n.node.kind === "resource" && n.node.type === BUCKET_TYPE) return [{
6935
+ address: resourceName(n.id),
6936
+ kind: "bucket"
6937
+ }];
6938
+ return [];
6939
+ });
6583
6940
  }
6584
6941
  async function runDevEmulators(input) {
6942
+ const nodes = appNodes(input);
6585
6943
  const { url: computeUrl } = await ensureDaemon("compute", daemonEntry("compute"));
6586
6944
  console.log(`[dev] compute emulator ready at ${computeUrl}`);
6587
- const retired = await retirePreviousApp({
6588
- devDir: input.devDir,
6589
- app: appNameOf(input.container)
6590
- });
6591
- if (retired !== void 0) console.log(`[dev] removed the local services of "${retired}", the app run here before`);
6592
- const portChanges = await ensureServiceInstances(input.container, input.devDir, serviceAppNames(input));
6593
- for (const { address, previous, given } of portChanges) console.log(`[dev] service "${address}" now uses port ${String(given)}: its previous port ${String(previous)} is in use`);
6594
- if (usesBuckets(input)) {
6945
+ if (nodes.some((node) => node.kind === "bucket")) {
6595
6946
  const { url: bucketsUrl } = await ensureDaemon("buckets", daemonEntry("buckets"));
6596
6947
  console.log(`[dev] buckets emulator ready at ${bucketsUrl}`);
6597
6948
  }
6598
- if (usesPostgres(input)) {
6949
+ if (nodes.some((node) => node.kind === "database")) {
6599
6950
  const { url: postgresUrl } = await ensureDaemon("postgres", daemonEntry("postgres"));
6600
6951
  console.log(`[dev] postgres emulator ready at ${postgresUrl}`);
6601
6952
  }
6953
+ const scope = {
6954
+ devDir: input.devDir,
6955
+ app: appNameOf(input.container)
6956
+ };
6957
+ const previous = await selectApp(scope);
6958
+ if (previous !== void 0) console.log(`[dev] stopped the local services of "${previous}", the app run here before; its data is kept`);
6959
+ const portChanges = await ensureAppInstances(scope, nodes).catch(async (error) => {
6960
+ await stopAppInstances(scope).catch(() => void 0);
6961
+ throw error;
6962
+ });
6963
+ 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`);
6602
6964
  }
6603
6965
  /** `local-placeholder-<16 lowercase hex>` — Web Crypto only, matching the extension's other local mints (ServiceKey, S3Credentials). */
6604
6966
  function mintPlaceholder() {
@@ -6639,22 +7001,25 @@ async function runDevPreflight(input) {
6639
7001
  }));
6640
7002
  if (missing.length > 0) throw missingEnvParamsError(missing);
6641
7003
  }
6642
- async function tolerateUnreachable(action) {
6643
- try {
6644
- await action();
6645
- } catch {}
6646
- }
7004
+ const DAEMON_OF = {
7005
+ service: "compute",
7006
+ database: "postgres",
7007
+ bucket: "buckets"
7008
+ };
6647
7009
  async function runDevTeardown(input) {
6648
7010
  const app = prismaCloudContainerOf(input.container).input.appName;
6649
7011
  const cwd = process.cwd();
6650
7012
  const devDir = `${cwd}/${DEV_DIR}`;
6651
- await tolerateUnreachable(() => postgresClient().deleteApp(app));
7013
+ const scope = {
7014
+ devDir,
7015
+ app
7016
+ };
6652
7017
  try {
6653
- await ensureDaemon("compute", daemonEntry("compute"));
6654
- await deleteRecordedInstances(devDir);
7018
+ for (const kind of await recordedKinds(scope)) await ensureDaemon(DAEMON_OF[kind], daemonEntry(DAEMON_OF[kind]));
7019
+ await deleteAppInstances(scope);
6655
7020
  } finally {
6656
- await tolerateUnreachable(() => bucketsClient().deleteApp(app));
6657
- removeLocalPaths([devDir, `${cwd}/.alchemy/state/${app}/dev`]);
7021
+ removeAppDevState(devDir, app);
7022
+ removeLocalPaths([`${cwd}/.alchemy/state/${app}/dev`]);
6658
7023
  }
6659
7024
  }
6660
7025
  /** `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`. */
@@ -6669,11 +7034,12 @@ function asProvidersLayer(layer) {
6669
7034
  function localTargetDescriptor() {
6670
7035
  return {
6671
7036
  container: devContainerDescriptor(),
6672
- providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(), OrmMigrationProvider(), S3CredentialsProvider(), GeneratedParamProvider(), ServiceKeyProvider())),
7037
+ providers: (input) => asProvidersLayer(Layer.mergeAll(localTargetProviders(input), PgWarmProvider(), LocalOrmMigrationProvider(), S3CredentialsProvider(), GeneratedParamProvider(), ServiceKeyProvider())),
6673
7038
  preflight: (input) => runDevPreflight(input),
6674
7039
  emulators: (input) => runDevEmulators(input),
6675
7040
  attach: (input) => devAttach(input),
6676
- teardown: (input) => runDevTeardown(input)
7041
+ teardown: (input) => runDevTeardown(input),
7042
+ current: (input) => currentApp(input.devDir)
6677
7043
  };
6678
7044
  }
6679
7045
  //#endregion