@prisma/cli 3.0.0-dev.86.1 → 3.0.0-dev.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,9 @@
1
- import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
2
- import { access } from "node:fs/promises";
1
+ import { resolveBunEntrypoint } from "./bun-project.js";
2
+ import "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  //#region src/lib/app/local-dev.ts
6
- const NEXT_CONFIG_FILENAMES = [
7
- "next.config.js",
8
- "next.config.mjs",
9
- "next.config.ts",
10
- "next.config.mts"
11
- ];
12
6
  const DEFAULT_LOCAL_DEV_PORT = 3e3;
13
- async function resolveLocalBuildType(appPath, buildType, signal) {
14
- if (buildType === "bun" || buildType === "nextjs") return buildType;
15
- if (buildType !== "auto") return null;
16
- return detectLocalBuildType(appPath, signal);
17
- }
18
- async function detectLocalBuildType(appPath, signal) {
19
- if (await isNextProject(appPath, signal)) return "nextjs";
20
- if (await isBunProject(appPath, signal)) return "bun";
21
- return null;
22
- }
23
7
  async function runLocalApp(options) {
24
8
  const spawnImpl = options.spawnImpl ?? spawn;
25
9
  if (options.buildType === "nextjs") {
@@ -92,47 +76,6 @@ async function runLocalApp(options) {
92
76
  signal: command.signal
93
77
  };
94
78
  }
95
- async function isNextProject(appPath, signal) {
96
- for (const fileName of NEXT_CONFIG_FILENAMES) {
97
- signal?.throwIfAborted();
98
- try {
99
- await access(path.join(appPath, fileName));
100
- signal?.throwIfAborted();
101
- return true;
102
- } catch (error) {
103
- if (signal?.aborted) throw error;
104
- }
105
- }
106
- return hasDependency(await readBunPackageJson(appPath, signal), "next");
107
- }
108
- async function isBunProject(appPath, signal) {
109
- signal?.throwIfAborted();
110
- try {
111
- await access(path.join(appPath, "bun.lock"));
112
- signal?.throwIfAborted();
113
- return true;
114
- } catch (error) {
115
- if (signal?.aborted) throw error;
116
- }
117
- signal?.throwIfAborted();
118
- try {
119
- await access(path.join(appPath, "bun.lockb"));
120
- signal?.throwIfAborted();
121
- return true;
122
- } catch (error) {
123
- if (signal?.aborted) throw error;
124
- }
125
- const packageJson = await readBunPackageJson(appPath, signal);
126
- if (!packageJson) return false;
127
- const hasEntrypoint = typeof readBunPackageEntrypoint(packageJson) === "string";
128
- const hasBunDependency = hasDependency(packageJson, "@types/bun") || hasDependency(packageJson, "bun");
129
- const usesBunScripts = (typeof packageJson.scripts === "object" && packageJson.scripts !== null ? Object.values(packageJson.scripts) : []).some((value) => typeof value === "string" && /\bbun\b/.test(value));
130
- return hasEntrypoint && (hasBunDependency || usesBunScripts);
131
- }
132
- function hasDependency(packageJson, dependencyName) {
133
- if (!packageJson) return false;
134
- return [packageJson.dependencies, packageJson.devDependencies].some((group) => typeof group === "object" && group !== null && dependencyName in group);
135
- }
136
79
  async function runWithFallback(candidates, options, spawnImpl, missingCommandMessage) {
137
80
  for (const candidate of candidates) try {
138
81
  const result = await spawnCommand(candidate, options, spawnImpl);
@@ -163,4 +106,4 @@ function spawnCommand(candidate, options, spawnImpl) {
163
106
  });
164
107
  }
165
108
  //#endregion
166
- export { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp };
109
+ export { DEFAULT_LOCAL_DEV_PORT, runLocalApp };
@@ -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, writeFile } 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";
@@ -246,7 +247,7 @@ var PreviewTanstackStartBuild = class {
246
247
  const outputDir = path.join(this.#appPath, settings.outputDirectory);
247
248
  const entrypoint = "server/index.mjs";
248
249
  const entryPath = path.join(outputDir, entrypoint);
249
- 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.`);
250
251
  const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
251
252
  try {
252
253
  const artifactDir = path.join(outDir, "app");
@@ -315,7 +316,7 @@ async function stageNextjsStandaloneArtifact(options) {
315
316
  sourceRoot: await resolveSourceRoot(appRoot, options.signal),
316
317
  signal: options.signal
317
318
  });
318
- await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"), options.signal);
319
+ await hoistIsolatedStoreDependencies(path.join(artifactRoot, "node_modules"), options.signal);
319
320
  }
320
321
  async function copyNextjsStaticAssets(options) {
321
322
  const serverSubpath = nextjsServerSubpath(options.entrypoint);
@@ -357,12 +358,21 @@ function nextjsServerSubpath(entrypoint) {
357
358
  const dir = path.posix.dirname(entrypoint);
358
359
  return dir === "." ? "" : dir;
359
360
  }
360
- async function hoistPnpmDependencies(nodeModulesDir, signal) {
361
- const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
362
- if (!await directoryExists(pnpmNodeModulesDir, signal)) return;
363
- 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 }));
364
374
  for (const entry of entries) {
365
- const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
375
+ const sourcePath = path.join(storeNodeModulesDir, entry.name);
366
376
  if (entry.name.startsWith("@") && entry.isDirectory()) {
367
377
  const scopedEntries = await unsupportedFilesystemBoundary(signal, () => readdir(sourcePath, { withFileTypes: true }));
368
378
  for (const scopedEntry of scopedEntries) {
@@ -370,7 +380,7 @@ async function hoistPnpmDependencies(nodeModulesDir, signal) {
370
380
  if (await pathExists(scopedDestination, signal)) continue;
371
381
  await unsupportedFilesystemBoundary(signal, () => mkdir(path.dirname(scopedDestination), { recursive: true }));
372
382
  await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
373
- standaloneRoot: pnpmNodeModulesDir,
383
+ standaloneRoot: storeNodeModulesDir,
374
384
  appRoot: nodeModulesDir,
375
385
  sourceRoot: nodeModulesDir,
376
386
  signal
@@ -381,7 +391,7 @@ async function hoistPnpmDependencies(nodeModulesDir, signal) {
381
391
  const destinationPath = path.join(nodeModulesDir, entry.name);
382
392
  if (await pathExists(destinationPath, signal)) continue;
383
393
  await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
384
- standaloneRoot: pnpmNodeModulesDir,
394
+ standaloneRoot: storeNodeModulesDir,
385
395
  appRoot: nodeModulesDir,
386
396
  sourceRoot: nodeModulesDir,
387
397
  signal
@@ -401,31 +411,23 @@ async function normalizeArtifactSymlinks(artifactDir, appPath, signal) {
401
411
  continue;
402
412
  }
403
413
  if (!entry.isSymbolicLink()) continue;
404
- if (await materializeArtifactSymlink({
405
- fullPath,
406
- normalizedArtifactDir,
407
- normalizedAppPath,
408
- signal
409
- }) === "directory") await walkDirectory(fullPath);
414
+ const target = await unsupportedFilesystemBoundary(signal, () => readlink(fullPath));
415
+ const resolvedTarget = path.resolve(path.dirname(fullPath), target);
416
+ if (isPathWithin(normalizedArtifactDir, resolvedTarget)) continue;
417
+ if (!isPathWithin(normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
418
+ const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(resolvedTarget));
419
+ await unsupportedFilesystemBoundary(signal, () => rm(fullPath, {
420
+ force: true,
421
+ recursive: true
422
+ }));
423
+ await unsupportedFilesystemBoundary(signal, () => cp(resolvedTarget, fullPath, {
424
+ recursive: targetStat.isDirectory(),
425
+ dereference: true
426
+ }));
427
+ if (targetStat.isDirectory()) await walkDirectory(fullPath);
410
428
  }
411
429
  }
412
430
  }
413
- async function materializeArtifactSymlink(options) {
414
- const target = await unsupportedFilesystemBoundary(options.signal, () => readlink(options.fullPath));
415
- const resolvedTarget = path.resolve(path.dirname(options.fullPath), target);
416
- if (isPathWithin(options.normalizedArtifactDir, resolvedTarget)) return "internal";
417
- if (!isPathWithin(options.normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
418
- const targetStat = await unsupportedFilesystemBoundary(options.signal, () => stat(resolvedTarget));
419
- await unsupportedFilesystemBoundary(options.signal, () => rm(options.fullPath, {
420
- force: true,
421
- recursive: true
422
- }));
423
- await unsupportedFilesystemBoundary(options.signal, () => cp(resolvedTarget, options.fullPath, {
424
- recursive: targetStat.isDirectory(),
425
- dereference: true
426
- }));
427
- return targetStat.isDirectory() ? "directory" : "file";
428
- }
429
431
  function isPathWithin(rootPath, candidatePath) {
430
432
  const relativePath = path.relative(rootPath, candidatePath);
431
433
  return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
@@ -489,29 +491,6 @@ async function directoryExists(targetPath, signal) {
489
491
  return false;
490
492
  }
491
493
  }
492
- async function resolveSourceRoot(appRoot, signal) {
493
- let current = path.resolve(appRoot);
494
- while (true) {
495
- 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;
496
- const parent = path.dirname(current);
497
- if (parent === current) return path.resolve(appRoot);
498
- current = parent;
499
- }
500
- }
501
- async function packageJsonDeclaresWorkspaces(directory, signal) {
502
- signal?.throwIfAborted();
503
- try {
504
- const content = await readFile(path.join(directory, "package.json"), {
505
- encoding: "utf8",
506
- signal
507
- });
508
- const parsed = JSON.parse(content);
509
- return Boolean(parsed.workspaces);
510
- } catch (error) {
511
- if (signal?.aborted) throw error;
512
- return false;
513
- }
514
- }
515
494
  async function unsupportedFilesystemBoundary(signal, operation) {
516
495
  signal?.throwIfAborted();
517
496
  const result = await operation();
@@ -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
  }
@@ -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
  }
@@ -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({
@@ -1,7 +1,9 @@
1
1
  import { CliError, usageError } from "../../shell/errors.js";
2
+ import { shortenHomePath } from "../fs/home-path.js";
2
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
3
4
  import "../../shell/command-arguments.js";
4
5
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
6
+ import path from "node:path";
5
7
  import { Result, matchError } from "better-result";
6
8
  //#region src/lib/project/setup.ts
7
9
  function isValidProjectSetupName(projectName) {
@@ -13,22 +15,21 @@ function validateProjectSetupNameText(value, fallback) {
13
15
  }
14
16
  function resolveProjectForSetup(projectRef, projects, workspace) {
15
17
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
16
- const match = matches[0];
17
- if (matches.length === 1 && match) return match;
18
+ if (matches.length === 1) return matches[0];
18
19
  if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
19
20
  throw projectNotFoundError(projectRef, workspace);
20
21
  }
21
- async function bindProjectToDirectory(context, workspace, project, action) {
22
+ async function bindProjectToDirectory(context, workspace, project, action, directory = context.runtime.cwd) {
22
23
  return Result.gen(async function* () {
23
- yield* Result.await(writeLocalResolutionPin(context.runtime.cwd, {
24
+ yield* Result.await(writeLocalResolutionPin(directory, {
24
25
  workspaceId: workspace.id,
25
26
  projectId: project.id
26
27
  }, context.runtime.signal));
27
- yield* Result.await(ensureLocalResolutionPinGitignore(context.runtime.cwd, context.runtime.signal));
28
+ yield* Result.await(ensureLocalResolutionPinGitignore(directory, context.runtime.signal));
28
29
  return Result.ok({
29
30
  workspace,
30
31
  project,
31
- directory: formatSetupDirectory(context.runtime.cwd),
32
+ directory: formatSetupDirectory(directory, context),
32
33
  localPin: {
33
34
  path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
34
35
  written: true
@@ -110,8 +111,9 @@ function projectCreateFailedError(error, projectName, workspace, options) {
110
111
  nextSteps: options.nextSteps
111
112
  });
112
113
  }
113
- function formatSetupDirectory(cwd) {
114
- const basename = cwd.split(/[\\/]/).filter(Boolean).pop();
114
+ function formatSetupDirectory(directory, context) {
115
+ if (path.resolve(directory) !== path.resolve(context.runtime.cwd)) return shortenHomePath(directory, context.runtime.env);
116
+ const basename = directory.split(/[\\/]/).filter(Boolean).pop();
115
117
  return basename ? `./${basename}` : ".";
116
118
  }
117
119
  function extractHttpStatus(error) {