@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.
- package/dist/adapters/token-storage.js +2 -2
- package/dist/cli.js +7 -7
- package/dist/cli2.js +3 -3
- package/dist/commands/app/index.js +64 -51
- package/dist/controllers/app-env.js +7 -6
- package/dist/controllers/app.js +477 -163
- package/dist/controllers/branch.js +1 -1
- package/dist/controllers/database.js +1 -1
- package/dist/controllers/project.js +4 -4
- package/dist/lib/app/branch-database-deploy.js +12 -60
- package/dist/lib/app/branch-database.js +1 -102
- package/dist/lib/app/compute-config.js +147 -0
- package/dist/lib/app/deploy-plan.js +58 -0
- package/dist/lib/app/local-dev.js +3 -60
- package/dist/lib/app/preview-build-settings.js +95 -104
- package/dist/lib/app/preview-build.js +61 -35
- package/dist/lib/app/preview-provider.js +23 -19
- package/dist/lib/app/production-deploy-gate.js +3 -3
- package/dist/lib/auth/login.js +31 -24
- package/dist/lib/diagnostics.js +1 -1
- package/dist/lib/fs/home-path.js +24 -0
- package/dist/lib/git/local-branch.js +15 -3
- package/dist/lib/project/resolution.js +2 -2
- package/dist/lib/project/setup.js +10 -7
- package/dist/output/patterns.js +1 -1
- package/dist/presenters/app.js +44 -39
- package/dist/presenters/branch.js +1 -1
- package/dist/presenters/database.js +2 -2
- package/dist/presenters/project.js +3 -9
- package/dist/shell/command-runner.js +37 -27
- package/dist/shell/diagnostics-output.js +2 -8
- package/dist/shell/help.js +30 -20
- package/dist/shell/runtime.js +8 -4
- package/dist/shell/ui.js +19 -2
- package/package.json +17 -3
|
@@ -1,64 +1,102 @@
|
|
|
1
|
-
import { CliError } from "../../shell/errors.js";
|
|
2
1
|
import { readBunPackageJson } from "./bun-project.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
38
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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: [
|
|
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,
|
|
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 {
|
|
3
|
-
import {
|
|
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))
|
|
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
|
|
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
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
513
|
-
|
|
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
|
}
|
package/dist/lib/auth/login.js
CHANGED
|
@@ -106,35 +106,42 @@ async function consumePastedCallback(options) {
|
|
|
106
106
|
});
|
|
107
107
|
try {
|
|
108
108
|
for (;;) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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;
|
package/dist/lib/diagnostics.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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({
|