@prisma/cli 3.0.0-beta.10 → 3.0.0-beta.11

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,64 +1,102 @@
1
- import { CliError } from "../../shell/errors.js";
2
1
  import { readBunPackageJson } from "./bun-project.js";
3
- import { readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import { sourceRootLineage } from "@prisma/compute-sdk/config";
3
+ import { readFile, readdir, stat } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { exec } from "node:child_process";
6
6
  import { parseModule } from "magicast";
7
7
  //#region src/lib/app/preview-build-settings.ts
8
+ /** Legacy build-settings file: no longer read or written, only detected for migration. */
8
9
  const PRISMA_APP_CONFIG_FILENAME = "prisma.app.json";
9
- const PRISMA_APP_CONFIG_SCHEMA_URL = "https://pris.ly/schemas/prisma-app-config.v1.json";
10
- async function resolveOrCreatePreviewBuildSettings(options) {
10
+ /**
11
+ * Detects a leftover `prisma.app.json`. The file is no longer used: one that
12
+ * matches the effective settings is reported for deletion, one with custom
13
+ * values must be migrated to the compute config so builds never silently
14
+ * change.
15
+ */
16
+ async function detectLegacyBuildSettings(options) {
11
17
  const configPath = path.join(options.appPath, PRISMA_APP_CONFIG_FILENAME);
12
- const existing = await readPreviewBuildSettingsConfig(configPath, options.signal);
13
- if (existing) return {
14
- status: "used",
15
- configPath,
16
- relativeConfigPath: PRISMA_APP_CONFIG_FILENAME,
17
- settings: {
18
- buildCommand: existing.buildCommand,
19
- buildCommandSource: null,
20
- outputDirectory: existing.outputDirectory,
21
- outputDirectorySource: null
22
- }
23
- };
24
- const settings = await resolvePreviewBuildSettings(options);
25
- const config = {
26
- $schema: PRISMA_APP_CONFIG_SCHEMA_URL,
27
- buildCommand: settings.buildCommand,
28
- outputDirectory: settings.outputDirectory
29
- };
18
+ let content;
30
19
  try {
31
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, {
20
+ options.signal?.throwIfAborted();
21
+ content = await readFile(configPath, {
32
22
  encoding: "utf8",
33
- flag: "wx",
34
23
  signal: options.signal
35
24
  });
36
25
  } catch (error) {
37
- if (error.code === "EEXIST") {
38
- const raced = await readPreviewBuildSettingsConfig(configPath, options.signal);
39
- if (raced) return {
40
- status: "used",
41
- configPath,
42
- relativeConfigPath: PRISMA_APP_CONFIG_FILENAME,
43
- settings: {
44
- buildCommand: raced.buildCommand,
45
- buildCommandSource: null,
46
- outputDirectory: raced.outputDirectory,
47
- outputDirectorySource: null
48
- }
49
- };
50
- }
51
- throw error;
26
+ if (options.signal?.aborted) throw error;
27
+ return { kind: "absent" };
52
28
  }
53
- return {
54
- status: "created",
29
+ let legacy;
30
+ try {
31
+ const parsed = JSON.parse(content);
32
+ const buildCommand = parsed.buildCommand === null || typeof parsed.buildCommand === "string" ? typeof parsed.buildCommand === "string" ? parsed.buildCommand.trim() || null : null : void 0;
33
+ const outputDirectory = typeof parsed.outputDirectory === "string" ? normalizeRelativePath(parsed.outputDirectory) : void 0;
34
+ if (buildCommand === void 0 || !outputDirectory) return {
35
+ kind: "invalid",
36
+ configPath
37
+ };
38
+ legacy = {
39
+ buildCommand,
40
+ outputDirectory
41
+ };
42
+ } catch {
43
+ return {
44
+ kind: "invalid",
45
+ configPath
46
+ };
47
+ }
48
+ return legacy.buildCommand === options.effective.buildCommand && legacy.outputDirectory === options.effective.outputDirectory ? {
49
+ kind: "matching",
50
+ configPath
51
+ } : {
52
+ kind: "custom",
55
53
  configPath,
56
- relativeConfigPath: PRISMA_APP_CONFIG_FILENAME,
57
- settings
54
+ ...legacy
55
+ };
56
+ }
57
+ /** Resolves build settings purely from framework inference; nothing is read or written. */
58
+ async function resolveInferredPreviewBuildSettings(options) {
59
+ return {
60
+ status: "inferred",
61
+ configPath: null,
62
+ relativeConfigPath: null,
63
+ settings: await resolvePreviewBuildSettings(options)
64
+ };
65
+ }
66
+ /**
67
+ * Resolves build settings when the compute config owns them: configured
68
+ * fields win, omitted fields fall back to framework defaults.
69
+ */
70
+ async function resolveConfiguredPreviewBuildSettings(options) {
71
+ const configFilename = path.basename(options.configPath);
72
+ const source = `set by ${configFilename}`;
73
+ const fallback = options.configured.command === void 0 || options.configured.outputDirectory === void 0 ? await resolvePreviewBuildSettings(options) : null;
74
+ return {
75
+ status: "config",
76
+ configPath: options.configPath,
77
+ relativeConfigPath: configFilename,
78
+ settings: {
79
+ buildCommand: options.configured.command !== void 0 ? options.configured.command : fallback.buildCommand,
80
+ buildCommandSource: options.configured.command !== void 0 ? source : fallback.buildCommandSource,
81
+ outputDirectory: options.configured.outputDirectory ?? fallback.outputDirectory,
82
+ outputDirectorySource: options.configured.outputDirectory !== void 0 ? source : fallback.outputDirectorySource
83
+ }
58
84
  };
59
85
  }
60
86
  async function resolvePreviewBuildSettings(options) {
61
87
  switch (options.buildType) {
88
+ case "nuxt": return {
89
+ buildCommand: "nuxt build",
90
+ buildCommandSource: "Nuxt default",
91
+ outputDirectory: ".output",
92
+ outputDirectorySource: "Nuxt output"
93
+ };
94
+ case "astro": return {
95
+ buildCommand: "astro build",
96
+ buildCommandSource: "Astro default",
97
+ outputDirectory: "dist",
98
+ outputDirectorySource: "Astro output"
99
+ };
62
100
  case "nextjs": {
63
101
  const packageJson = await readBunPackageJson(options.appPath, options.signal);
64
102
  const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, {
@@ -104,57 +142,6 @@ async function resolvePreviewBuildSettings(options) {
104
142
  }
105
143
  }
106
144
  }
107
- async function readPreviewBuildSettingsConfig(configPath, signal) {
108
- let content;
109
- try {
110
- content = await readFile(configPath, {
111
- encoding: "utf8",
112
- signal
113
- });
114
- } catch (error) {
115
- if (error.code === "ENOENT") return null;
116
- throw error;
117
- }
118
- let parsed;
119
- try {
120
- parsed = JSON.parse(content);
121
- } catch (error) {
122
- throw invalidPrismaAppConfigError(configPath, `The file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
123
- }
124
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw invalidPrismaAppConfigError(configPath, "The file must contain a JSON object.");
125
- const raw = parsed;
126
- if ("$schema" in raw && raw.$schema !== void 0 && typeof raw.$schema !== "string") throw invalidPrismaAppConfigError(configPath, "The $schema field must be a string when present.");
127
- if (raw.buildCommand !== null && typeof raw.buildCommand !== "string") throw invalidPrismaAppConfigError(configPath, "The buildCommand field must be a string or null.");
128
- let buildCommand = null;
129
- if (typeof raw.buildCommand === "string") {
130
- buildCommand = raw.buildCommand.trim();
131
- if (buildCommand.length === 0) throw invalidPrismaAppConfigError(configPath, "The buildCommand field must not be an empty string. Use null to skip the build step.");
132
- }
133
- const outputDirectory = normalizeConfigOutputDirectory(configPath, raw.outputDirectory);
134
- return {
135
- buildCommand,
136
- outputDirectory
137
- };
138
- }
139
- function normalizeConfigOutputDirectory(configPath, value) {
140
- if (typeof value !== "string" || value.trim().length === 0) throw invalidPrismaAppConfigError(configPath, "The outputDirectory field must be a non-empty string.");
141
- const normalized = normalizeRelativePath(value);
142
- if (!normalized) throw invalidPrismaAppConfigError(configPath, "The outputDirectory field must be a relative path inside the app directory.");
143
- return normalized;
144
- }
145
- function invalidPrismaAppConfigError(configPath, why) {
146
- return new CliError({
147
- code: "APP_CONFIG_INVALID",
148
- domain: "app",
149
- summary: `Invalid ${PRISMA_APP_CONFIG_FILENAME}`,
150
- why,
151
- fix: `Edit ${PRISMA_APP_CONFIG_FILENAME} so buildCommand is a string or null and outputDirectory is a relative path inside the app root. Delete the file and rerun prisma-cli app deploy to regenerate defaults.`,
152
- where: configPath,
153
- meta: { configPath },
154
- exitCode: 2,
155
- nextSteps: ["prisma-cli app deploy"]
156
- });
157
- }
158
145
  async function hasRootFile(appPath, filenames, signal) {
159
146
  let entries;
160
147
  try {
@@ -203,12 +190,14 @@ function readBuildScript(packageJson) {
203
190
  return buildScript.length > 0 ? buildScript : null;
204
191
  }
205
192
  async function resolvePackageManager(appPath, packageJson, signal) {
206
- const fromPackageManager = packageManagerFromPackageJson(packageJson?.packageManager);
207
- if (fromPackageManager) return fromPackageManager;
208
- if (await pathExists(path.join(appPath, "bun.lock"), signal) || await pathExists(path.join(appPath, "bun.lockb"), signal)) return "bun";
209
- if (await pathExists(path.join(appPath, "pnpm-lock.yaml"), signal)) return "pnpm";
210
- if (await pathExists(path.join(appPath, "yarn.lock"), signal)) return "yarn";
211
- if (await pathExists(path.join(appPath, "package-lock.json"), signal)) return "npm";
193
+ for (const directory of await sourceRootLineage(appPath, signal)) {
194
+ const fromPackageManager = packageManagerFromPackageJson((directory === path.resolve(appPath) ? packageJson : await readBunPackageJson(directory, signal))?.packageManager);
195
+ if (fromPackageManager) return fromPackageManager;
196
+ if (await pathExists(path.join(directory, "bun.lock"), signal) || await pathExists(path.join(directory, "bun.lockb"), signal)) return "bun";
197
+ if (await pathExists(path.join(directory, "pnpm-lock.yaml"), signal)) return "pnpm";
198
+ if (await pathExists(path.join(directory, "yarn.lock"), signal)) return "yarn";
199
+ if (await pathExists(path.join(directory, "package-lock.json"), signal)) return "npm";
200
+ }
212
201
  }
213
202
  function packageManagerFromPackageJson(value) {
214
203
  if (typeof value !== "string") return null;
@@ -217,15 +206,16 @@ function packageManagerFromPackageJson(value) {
217
206
  }
218
207
  async function runResolvedBuildCommand(appPath, settings, failurePrefix, signal) {
219
208
  if (!settings.buildCommand) return;
220
- await execBuildCommand(settings.buildCommand, appPath, failurePrefix, signal);
209
+ const binDirs = (await sourceRootLineage(appPath, signal)).map((directory) => path.join(directory, "node_modules", ".bin"));
210
+ await execBuildCommand(settings.buildCommand, appPath, binDirs, failurePrefix, signal);
221
211
  }
222
- function execBuildCommand(command, cwd, failurePrefix, signal) {
212
+ function execBuildCommand(command, cwd, binDirs, failurePrefix, signal) {
223
213
  return new Promise((resolve, reject) => {
224
214
  const child = exec(command, {
225
215
  cwd,
226
216
  env: {
227
217
  ...process.env,
228
- PATH: [path.join(cwd, "node_modules", ".bin"), process.env.PATH].filter(Boolean).join(path.delimiter)
218
+ PATH: [...binDirs, process.env.PATH].filter(Boolean).join(path.delimiter)
229
219
  },
230
220
  maxBuffer: 10 * 1024 * 1024,
231
221
  signal
@@ -365,6 +355,7 @@ function propertyKeyName(value) {
365
355
  function normalizeRelativePath(value) {
366
356
  const raw = value.trim().replace(/\\/g, "/");
367
357
  if (raw.length === 0 || raw.split("/").includes("..")) return;
358
+ if (/^[A-Za-z]:/.test(raw)) return;
368
359
  const normalized = path.posix.normalize(raw);
369
360
  const segments = normalized.split("/");
370
361
  if (path.win32.isAbsolute(value) || path.posix.isAbsolute(normalized) || segments.includes("..")) return;
@@ -382,4 +373,4 @@ async function pathExists(targetPath, signal) {
382
373
  }
383
374
  }
384
375
  //#endregion
385
- export { PRISMA_APP_CONFIG_FILENAME, PRISMA_APP_CONFIG_SCHEMA_URL, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolveOrCreatePreviewBuildSettings, resolvePreviewBuildSettings, runResolvedBuildCommand };
376
+ export { PRISMA_APP_CONFIG_FILENAME, detectLegacyBuildSettings, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolveConfiguredPreviewBuildSettings, resolveInferredPreviewBuildSettings, resolvePreviewBuildSettings, runResolvedBuildCommand };
@@ -1,6 +1,7 @@
1
1
  import { readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
2
- import { PRISMA_APP_CONFIG_FILENAME, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolvePreviewBuildSettings, runResolvedBuildCommand } from "./preview-build-settings.js";
3
- import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
2
+ import { hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, resolvePreviewBuildSettings, runResolvedBuildCommand } from "./preview-build-settings.js";
3
+ import { resolveSourceRoot } from "@prisma/compute-sdk/config";
4
+ import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readdir, readlink, rm, stat, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import os from "node:os";
6
7
  import { AstroBuild, BunBuild, NuxtBuild } from "@prisma/compute-sdk";
@@ -151,7 +152,7 @@ var PreviewNextjsBuild = class {
151
152
  });
152
153
  await runResolvedBuildCommand(this.#appPath, settings, "Next.js", signal);
153
154
  const standaloneDir = path.join(this.#appPath, settings.outputDirectory);
154
- if (!await directoryExists(standaloneDir, signal)) throw new Error(`Next.js build did not produce standalone output at ${settings.outputDirectory}. Add output: "standalone" to your next.config file, or update ${PRISMA_APP_CONFIG_FILENAME}.`);
155
+ if (!await directoryExists(standaloneDir, signal)) return stageNextjsFullTreeFallbackArtifact(this.#appPath, signal);
155
156
  const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
156
157
  try {
157
158
  const artifactDir = path.join(outDir, "app");
@@ -187,6 +188,45 @@ var PreviewNextjsBuild = class {
187
188
  }
188
189
  }
189
190
  };
191
+ const FULL_TREE_NEXT_START_ENTRYPOINT = "prisma-next-start.cjs";
192
+ const FULL_TREE_NEXT_START_SOURCE = [
193
+ "process.chdir(__dirname);",
194
+ "process.env.NODE_ENV = \"production\";",
195
+ "process.argv.push(\"start\", \"-p\", process.env.PORT ?? \"3000\");",
196
+ "require(\"next/dist/bin/next\");",
197
+ ""
198
+ ].join("\n");
199
+ async function stageNextjsFullTreeFallbackArtifact(appPath, signal) {
200
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
201
+ try {
202
+ const artifactDir = path.join(outDir, "app");
203
+ await unsupportedFilesystemBoundary(signal, () => cp(appPath, artifactDir, {
204
+ recursive: true,
205
+ verbatimSymlinks: true,
206
+ filter: (source) => !isExcludedFromFullTreeArtifact(path.basename(source))
207
+ }));
208
+ await unsupportedFilesystemBoundary(signal, () => writeFile(path.join(artifactDir, FULL_TREE_NEXT_START_ENTRYPOINT), FULL_TREE_NEXT_START_SOURCE));
209
+ return {
210
+ directory: artifactDir,
211
+ entrypoint: FULL_TREE_NEXT_START_ENTRYPOINT,
212
+ defaultPortMapping: { http: 3e3 },
213
+ cleanup: () => rm(outDir, {
214
+ recursive: true,
215
+ force: true
216
+ })
217
+ };
218
+ } catch (error) {
219
+ await rm(outDir, {
220
+ recursive: true,
221
+ force: true
222
+ });
223
+ throw error;
224
+ }
225
+ }
226
+ /** Excludes VCS internals and dotenv files (local secrets, superseded by the deploy env). */
227
+ function isExcludedFromFullTreeArtifact(basename) {
228
+ return basename === ".git" || basename === ".env" || basename.startsWith(".env.");
229
+ }
190
230
  var PreviewTanstackStartBuild = class {
191
231
  #appPath;
192
232
  #buildSettings;
@@ -207,7 +247,7 @@ var PreviewTanstackStartBuild = class {
207
247
  const outputDir = path.join(this.#appPath, settings.outputDirectory);
208
248
  const entrypoint = "server/index.mjs";
209
249
  const entryPath = path.join(outputDir, entrypoint);
210
- if (!(await unsupportedFilesystemBoundary(signal, () => stat(entryPath).catch(() => null)))?.isFile()) throw new Error(`TanStack Start build did not produce a Nitro node server entrypoint at ${joinPosix(settings.outputDirectory, entrypoint)}. Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset, or update ${PRISMA_APP_CONFIG_FILENAME}.`);
250
+ if (!(await unsupportedFilesystemBoundary(signal, () => stat(entryPath).catch(() => null)))?.isFile()) throw new Error(`TanStack Start build did not produce a Nitro node server entrypoint at ${joinPosix(settings.outputDirectory, entrypoint)}. Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset, or set build.outputDirectory in prisma.compute.ts.`);
211
251
  const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
212
252
  try {
213
253
  const artifactDir = path.join(outDir, "app");
@@ -276,7 +316,7 @@ async function stageNextjsStandaloneArtifact(options) {
276
316
  sourceRoot: await resolveSourceRoot(appRoot, options.signal),
277
317
  signal: options.signal
278
318
  });
279
- await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"), options.signal);
319
+ await hoistIsolatedStoreDependencies(path.join(artifactRoot, "node_modules"), options.signal);
280
320
  }
281
321
  async function copyNextjsStaticAssets(options) {
282
322
  const serverSubpath = nextjsServerSubpath(options.entrypoint);
@@ -318,12 +358,21 @@ function nextjsServerSubpath(entrypoint) {
318
358
  const dir = path.posix.dirname(entrypoint);
319
359
  return dir === "." ? "" : dir;
320
360
  }
321
- async function hoistPnpmDependencies(nodeModulesDir, signal) {
322
- const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
323
- if (!await directoryExists(pnpmNodeModulesDir, signal)) return;
324
- const entries = await unsupportedFilesystemBoundary(signal, () => readdir(pnpmNodeModulesDir, { withFileTypes: true }));
361
+ /**
362
+ * pnpm and bun (isolated linker) both keep packages in a virtual store with a
363
+ * shared symlink farm (`.pnpm/node_modules`, `.bun/node_modules`). Hoist the
364
+ * farm entries to the artifact's node_modules root so Node-style resolution
365
+ * works after symlinks are materialized.
366
+ */
367
+ async function hoistIsolatedStoreDependencies(nodeModulesDir, signal) {
368
+ await hoistStoreDependencies(nodeModulesDir, path.join(nodeModulesDir, ".pnpm", "node_modules"), signal);
369
+ await hoistStoreDependencies(nodeModulesDir, path.join(nodeModulesDir, ".bun", "node_modules"), signal);
370
+ }
371
+ async function hoistStoreDependencies(nodeModulesDir, storeNodeModulesDir, signal) {
372
+ if (!await directoryExists(storeNodeModulesDir, signal)) return;
373
+ const entries = await unsupportedFilesystemBoundary(signal, () => readdir(storeNodeModulesDir, { withFileTypes: true }));
325
374
  for (const entry of entries) {
326
- const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
375
+ const sourcePath = path.join(storeNodeModulesDir, entry.name);
327
376
  if (entry.name.startsWith("@") && entry.isDirectory()) {
328
377
  const scopedEntries = await unsupportedFilesystemBoundary(signal, () => readdir(sourcePath, { withFileTypes: true }));
329
378
  for (const scopedEntry of scopedEntries) {
@@ -331,7 +380,7 @@ async function hoistPnpmDependencies(nodeModulesDir, signal) {
331
380
  if (await pathExists(scopedDestination, signal)) continue;
332
381
  await unsupportedFilesystemBoundary(signal, () => mkdir(path.dirname(scopedDestination), { recursive: true }));
333
382
  await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
334
- standaloneRoot: pnpmNodeModulesDir,
383
+ standaloneRoot: storeNodeModulesDir,
335
384
  appRoot: nodeModulesDir,
336
385
  sourceRoot: nodeModulesDir,
337
386
  signal
@@ -342,7 +391,7 @@ async function hoistPnpmDependencies(nodeModulesDir, signal) {
342
391
  const destinationPath = path.join(nodeModulesDir, entry.name);
343
392
  if (await pathExists(destinationPath, signal)) continue;
344
393
  await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
345
- standaloneRoot: pnpmNodeModulesDir,
394
+ standaloneRoot: storeNodeModulesDir,
346
395
  appRoot: nodeModulesDir,
347
396
  sourceRoot: nodeModulesDir,
348
397
  signal
@@ -442,29 +491,6 @@ async function directoryExists(targetPath, signal) {
442
491
  return false;
443
492
  }
444
493
  }
445
- async function resolveSourceRoot(appRoot, signal) {
446
- let current = path.resolve(appRoot);
447
- while (true) {
448
- if (await pathExists(path.join(current, ".git"), signal) || await pathExists(path.join(current, "pnpm-workspace.yaml"), signal) || await pathExists(path.join(current, "bun.lock"), signal) || await pathExists(path.join(current, "bun.lockb"), signal) || await packageJsonDeclaresWorkspaces(current, signal)) return current;
449
- const parent = path.dirname(current);
450
- if (parent === current) return path.resolve(appRoot);
451
- current = parent;
452
- }
453
- }
454
- async function packageJsonDeclaresWorkspaces(directory, signal) {
455
- signal?.throwIfAborted();
456
- try {
457
- const content = await readFile(path.join(directory, "package.json"), {
458
- encoding: "utf8",
459
- signal
460
- });
461
- const parsed = JSON.parse(content);
462
- return Boolean(parsed.workspaces);
463
- } catch (error) {
464
- if (signal?.aborted) throw error;
465
- return false;
466
- }
467
- }
468
494
  async function unsupportedFilesystemBoundary(signal, operation) {
469
495
  signal?.throwIfAborted();
470
496
  const result = await operation();
@@ -509,29 +509,33 @@ async function findAppForDeployment(sdk, deploymentId, signal) {
509
509
  });
510
510
  if (servicesResult.isErr()) throw new Error(servicesResult.error.message);
511
511
  for (const service of servicesResult.value) {
512
- const detailResult = await sdk.showService({
513
- serviceId: service.id,
514
- signal
515
- });
516
- if (detailResult.isErr()) throw new Error(detailResult.error.message);
517
- const app = {
518
- id: detailResult.value.id,
519
- name: detailResult.value.name,
520
- region: detailResult.value.region ?? null,
521
- liveDeploymentId: detailResult.value.latestVersionId ?? null,
522
- liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null)
523
- };
524
- if (app.liveDeploymentId === deploymentId) return app;
525
- const versionsResult = await sdk.listVersions({
526
- serviceId: service.id,
527
- signal
528
- });
529
- if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
530
- if (versionsResult.value.some((version) => version.id === deploymentId)) return app;
512
+ const app = await findServiceAppForDeployment(sdk, service.id, deploymentId, signal);
513
+ if (app) return app;
531
514
  }
532
515
  }
533
516
  return null;
534
517
  }
518
+ async function findServiceAppForDeployment(sdk, serviceId, deploymentId, signal) {
519
+ const detailResult = await sdk.showService({
520
+ serviceId,
521
+ signal
522
+ });
523
+ if (detailResult.isErr()) throw new Error(detailResult.error.message);
524
+ const app = {
525
+ id: detailResult.value.id,
526
+ name: detailResult.value.name,
527
+ region: detailResult.value.region ?? null,
528
+ liveDeploymentId: detailResult.value.latestVersionId ?? null,
529
+ liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null)
530
+ };
531
+ if (app.liveDeploymentId === deploymentId) return app;
532
+ const versionsResult = await sdk.listVersions({
533
+ serviceId,
534
+ signal
535
+ });
536
+ if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
537
+ return versionsResult.value.some((version) => version.id === deploymentId) ? app : null;
538
+ }
535
539
  function toAbsoluteUrl(url) {
536
540
  if (!url) return null;
537
541
  return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { canPrompt } from "../../shell/runtime.js";
3
2
  import { confirmPrompt } from "../../shell/prompt.js";
3
+ import { canPrompt } from "../../shell/runtime.js";
4
4
  //#region src/lib/app/production-deploy-gate.ts
5
5
  async function enforceProductionDeployGate(context, provider, options) {
6
6
  if (options.branchKind !== "production") return { firstProductionDeploy: false };
@@ -104,12 +104,12 @@ function productionDeployRequiresFlagError() {
104
104
  "",
105
105
  "Production deploys require explicit intent. Re-run with:",
106
106
  "",
107
- " prisma app deploy --prod",
107
+ " prisma-cli app deploy --prod",
108
108
  "",
109
109
  "Or deploy a preview from a feature branch:",
110
110
  "",
111
111
  " git checkout -b <branch-name>",
112
- " prisma app deploy"
112
+ " prisma-cli app deploy"
113
113
  ]
114
114
  });
115
115
  }
@@ -106,35 +106,42 @@ async function consumePastedCallback(options) {
106
106
  });
107
107
  try {
108
108
  for (;;) {
109
- let answer;
110
- try {
111
- answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
112
- } catch (error) {
113
- if (error?.name === "AbortError") return;
114
- throw error;
115
- }
116
- const trimmed = answer.trim().replace(/^["']|["']$/g, "");
117
- let url;
118
- try {
119
- if (!trimmed) throw new Error("empty input");
120
- url = new URL(trimmed);
121
- } catch {
122
- options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
123
- continue;
124
- }
125
- try {
126
- await options.complete(url);
127
- return;
128
- } catch (error) {
129
- const message = error instanceof Error ? error.message : String(error);
130
- options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
131
- continue;
132
- }
109
+ const url = await readPastedCallbackUrl(rl, options);
110
+ if (url === null) return;
111
+ if (url === void 0) continue;
112
+ if (await tryCompletePastedCallback(url, options)) return;
133
113
  }
134
114
  } finally {
135
115
  rl.close();
136
116
  }
137
117
  }
118
+ async function readPastedCallbackUrl(rl, options) {
119
+ let answer;
120
+ try {
121
+ answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
122
+ } catch (error) {
123
+ if (error?.name === "AbortError") return null;
124
+ throw error;
125
+ }
126
+ const trimmed = answer.trim().replace(/^["']|["']$/g, "");
127
+ try {
128
+ if (!trimmed) throw new Error("empty input");
129
+ return new URL(trimmed);
130
+ } catch {
131
+ options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
132
+ return;
133
+ }
134
+ }
135
+ async function tryCompletePastedCallback(url, options) {
136
+ try {
137
+ await options.complete(url);
138
+ return true;
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
142
+ return false;
143
+ }
144
+ }
138
145
  var LoginState = class {
139
146
  latestVerifier;
140
147
  latestState;
@@ -3,7 +3,7 @@ import { resolveStateDir } from "../shell/runtime.js";
3
3
  import { readLocalGitState } from "./git/local-status.js";
4
4
  //#region src/lib/diagnostics.ts
5
5
  async function collectCommandDiagnostics(context, options = {}) {
6
- const stateDir = resolveStateDir(context.runtime);
6
+ const stateDir = await resolveStateDir(context.runtime);
7
7
  return {
8
8
  cwd: context.runtime.cwd,
9
9
  stateFilePath: resolveLocalStateFilePath(stateDir),
@@ -0,0 +1,24 @@
1
+ import path from "node:path";
2
+ //#region src/lib/fs/home-path.ts
3
+ /**
4
+ * Shortens a path under the user's home directory to `~/...` for display,
5
+ * posix-style on every platform. Falls back to the Windows home variables
6
+ * when `HOME` is unset (native cmd/PowerShell sessions).
7
+ */
8
+ function shortenHomePath(value, env) {
9
+ const resolved = path.resolve(value);
10
+ const home = resolveHomeDirectory(env);
11
+ if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
12
+ const relative = path.relative(home, resolved).split(path.sep).join("/");
13
+ return relative ? `~/${relative}` : "~";
14
+ }
15
+ return resolved;
16
+ }
17
+ function resolveHomeDirectory(env) {
18
+ if (env.HOME) return path.resolve(env.HOME);
19
+ if (env.USERPROFILE) return path.resolve(env.USERPROFILE);
20
+ if (env.HOMEDRIVE && env.HOMEPATH) return path.resolve(`${env.HOMEDRIVE}${env.HOMEPATH}`);
21
+ return null;
22
+ }
23
+ //#endregion
24
+ export { shortenHomePath };
@@ -1,9 +1,22 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  //#region src/lib/git/local-branch.ts
4
+ /**
5
+ * Resolves the checked-out branch the way git does: the nearest `.git`
6
+ * (directory or worktree file) from `cwd` upward owns the answer, so
7
+ * monorepo commands run from inside a package see the repository branch.
8
+ * Returns null for detached HEAD or when no repository contains `cwd`.
9
+ */
4
10
  async function readLocalGitBranch(cwd, signal) {
5
- const headPath = await resolveGitHeadPath(path.join(cwd, ".git"), signal);
6
- if (!headPath) return null;
11
+ for (let directory = path.resolve(cwd);;) {
12
+ const headPath = await resolveGitHeadPath(path.join(directory, ".git"), signal);
13
+ if (headPath) return readBranchFromHead(headPath, signal);
14
+ const parent = path.dirname(directory);
15
+ if (parent === directory) return null;
16
+ directory = parent;
17
+ }
18
+ }
19
+ async function readBranchFromHead(headPath, signal) {
7
20
  try {
8
21
  const head = (await readFile(headPath, {
9
22
  encoding: "utf8",
@@ -12,7 +25,6 @@ async function readLocalGitBranch(cwd, signal) {
12
25
  if (head.startsWith("ref: refs/heads/")) return head.slice(16);
13
26
  } catch (error) {
14
27
  if (signal.aborted) throw error;
15
- return null;
16
28
  }
17
29
  return null;
18
30
  }
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { formatCommandArgument } from "../../shell/command-arguments.js";
3
2
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
3
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { Result, TaggedError, matchError } from "better-result";
@@ -339,7 +339,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
339
339
  }
340
340
  async function readImplicitLocalPin(options, settings) {
341
341
  if (options.explicitProject || settings.allowEnvProjectId && options.envProjectId) return Result.ok(null);
342
- const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
342
+ const localPinResult = await readLocalResolutionPin(options.projectDir ?? options.context.runtime.cwd, options.context.runtime.signal);
343
343
  if (localPinResult.isErr()) return Result.err(localPinReadErrorToProjectError(localPinResult.error));
344
344
  const localPin = localPinResult.value;
345
345
  if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) return Result.err(new LocalProjectWorkspaceMismatchError({