@prisma/cli 3.0.0-dev.81.1 → 3.0.0-dev.83.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.
@@ -69,7 +69,8 @@ function hasBranchDatabaseSignal(signal) {
69
69
  }
70
70
  async function runBranchDatabaseSchemaSetup(options) {
71
71
  const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
72
- const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl);
72
+ const prisma = await resolvePrismaInvocation(options.context.runtime.cwd);
73
+ const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl, prisma);
73
74
  for (const command of commands) await runPrismaCommand({
74
75
  context: options.context,
75
76
  ...command,
@@ -87,29 +88,41 @@ async function runBranchDatabaseSchemaSetup(options) {
87
88
  async function scanDirectory(cwd, directory, depth, state, signal) {
88
89
  signal.throwIfAborted();
89
90
  if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
90
- let entries;
91
- try {
92
- entries = await readdir(directory, { withFileTypes: true });
93
- } catch (error) {
94
- if (error.code === "ENOENT") return;
95
- throw error;
96
- }
91
+ const entries = await readDirectoryEntries(directory);
92
+ if (!entries) return;
97
93
  entries.sort((left, right) => left.name.localeCompare(right.name));
98
94
  for (const entry of entries) {
99
95
  signal.throwIfAborted();
100
96
  if (state.filesVisited >= MAX_SCAN_FILES) return;
101
- const entryPath = path.join(directory, entry.name);
102
- if (entry.isDirectory()) {
103
- if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
104
- continue;
105
- }
106
- if (!entry.isFile()) continue;
107
- state.filesVisited += 1;
108
- if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
109
- if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
110
- if (state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entry.name) && await fileContainsDatabaseUrl(entryPath, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
97
+ await scanDirectoryEntry(cwd, directory, entry, depth, state, signal);
111
98
  }
112
99
  }
100
+ async function readDirectoryEntries(directory) {
101
+ try {
102
+ return await readdir(directory, { withFileTypes: true });
103
+ } catch (error) {
104
+ if (error.code === "ENOENT") return null;
105
+ throw error;
106
+ }
107
+ }
108
+ async function scanDirectoryEntry(cwd, directory, entry, depth, state, signal) {
109
+ const entryPath = path.join(directory, entry.name);
110
+ if (entry.isDirectory()) {
111
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
112
+ return;
113
+ }
114
+ if (!entry.isFile()) return;
115
+ state.filesVisited += 1;
116
+ collectBranchDatabaseCandidate(entryPath, entry.name, state);
117
+ if (await shouldRecordDatabaseUrlReference(entryPath, entry.name, state, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
118
+ }
119
+ function collectBranchDatabaseCandidate(entryPath, entryName, state) {
120
+ if (entryName === "schema.prisma") state.schemaCandidates.push(entryPath);
121
+ if (isPrismaNextConfigFile(entryName)) state.prismaNextConfigCandidates.push(entryPath);
122
+ }
123
+ async function shouldRecordDatabaseUrlReference(entryPath, entryName, state, signal) {
124
+ return state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entryName) && await fileContainsDatabaseUrl(entryPath, signal);
125
+ }
113
126
  async function selectPrismaOrmSchema(cwd, candidates, signal) {
114
127
  const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
115
128
  for (const schemaPath of sorted) {
@@ -142,12 +155,15 @@ async function selectPrismaOrmSchema(cwd, candidates, signal) {
142
155
  };
143
156
  }
144
157
  function selectPrismaNextConfig(cwd, candidates, mode) {
145
- const matches = candidates.filter((candidate) => {
146
- const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
147
- return mode === "supported" ? isSupported : !isSupported;
148
- });
158
+ const matches = candidates.filter(mode === "supported" ? isSupportedPrismaNextConfig : isUnsupportedPrismaNextConfig);
149
159
  return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts").map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)).find((candidate) => Boolean(candidate)) ?? null;
150
160
  }
161
+ function isSupportedPrismaNextConfig(candidate) {
162
+ return candidate.target === "postgresql" || candidate.target === "unknown";
163
+ }
164
+ function isUnsupportedPrismaNextConfig(candidate) {
165
+ return !isSupportedPrismaNextConfig(candidate);
166
+ }
151
167
  function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
152
168
  return candidates.map((candidate) => ({
153
169
  absolute: candidate,
@@ -229,28 +245,73 @@ async function readTextFileIfSmall(filePath, signal) {
229
245
  signal
230
246
  });
231
247
  }
232
- function buildSchemaSetupCommands(schema, schemaPath, databaseUrl) {
248
+ const FALLBACK_PRISMA_CLI_VERSION = "6.19.3";
249
+ /**
250
+ * Picks how `prisma` CLI commands are invoked for schema setup. Projects
251
+ * with the CLI installed run their own binary (version-exact). Projects
252
+ * without it fall back to a versioned `npx prisma@<x>` pinned to the
253
+ * installed `@prisma/client` — never bare `npx prisma`, which resolves to
254
+ * latest and can be a major version ahead of the project's schema.
255
+ */
256
+ async function resolvePrismaInvocation(cwd) {
257
+ if (await localPrismaBinExists(cwd)) return {
258
+ argsPrefix: ["--no-install", "prisma"],
259
+ displayPrefix: "npx --no-install prisma"
260
+ };
261
+ const pinned = await readInstalledPrismaClientVersion(cwd) ?? FALLBACK_PRISMA_CLI_VERSION;
262
+ return {
263
+ argsPrefix: ["--yes", `prisma@${pinned}`],
264
+ displayPrefix: `npx prisma@${pinned}`
265
+ };
266
+ }
267
+ /** npm/pnpm name the local CLI shim `prisma` on POSIX and `prisma.cmd`/`prisma.ps1` on Windows. */
268
+ async function localPrismaBinExists(cwd) {
269
+ const binDir = path.join(cwd, "node_modules", ".bin");
270
+ return (await Promise.all([
271
+ "prisma",
272
+ "prisma.cmd",
273
+ "prisma.ps1"
274
+ ].map((name) => fileExists(path.join(binDir, name))))).some(Boolean);
275
+ }
276
+ async function fileExists(filePath) {
277
+ try {
278
+ await access(filePath);
279
+ return true;
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+ async function readInstalledPrismaClientVersion(cwd) {
285
+ try {
286
+ const raw = await readFile(path.join(cwd, "node_modules", "@prisma", "client", "package.json"), { encoding: "utf8" });
287
+ const parsed = JSON.parse(raw);
288
+ if (typeof parsed !== "object" || parsed === null) return null;
289
+ const version = parsed.version;
290
+ return typeof version === "string" && version.length > 0 ? version : null;
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+ function buildSchemaSetupCommands(schema, schemaPath, databaseUrl, prisma) {
233
296
  if (schema.command === "migrate-deploy") return [{
234
297
  args: [
235
- "--no-install",
236
- "prisma",
298
+ ...prisma.argsPrefix,
237
299
  "migrate",
238
300
  "deploy",
239
301
  "--schema",
240
302
  schemaPath
241
303
  ],
242
- displayCommand: "npx --no-install prisma migrate deploy"
304
+ displayCommand: `${prisma.displayPrefix} migrate deploy`
243
305
  }];
244
306
  if (schema.command === "db-push") return [{
245
307
  args: [
246
- "--no-install",
247
- "prisma",
308
+ ...prisma.argsPrefix,
248
309
  "db",
249
310
  "push",
250
311
  "--schema",
251
312
  schemaPath
252
313
  ],
253
- displayCommand: "npx --no-install prisma db push"
314
+ displayCommand: `${prisma.displayPrefix} db push`
254
315
  }];
255
316
  return [{
256
317
  args: [
@@ -1,6 +1,6 @@
1
1
  import { readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
2
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";
3
+ import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { AstroBuild, BunBuild, NuxtBuild } from "@prisma/compute-sdk";
@@ -151,7 +151,7 @@ var PreviewNextjsBuild = class {
151
151
  });
152
152
  await runResolvedBuildCommand(this.#appPath, settings, "Next.js", signal);
153
153
  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}.`);
154
+ if (!await directoryExists(standaloneDir, signal)) return stageNextjsFullTreeFallbackArtifact(this.#appPath, signal);
155
155
  const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
156
156
  try {
157
157
  const artifactDir = path.join(outDir, "app");
@@ -187,6 +187,45 @@ var PreviewNextjsBuild = class {
187
187
  }
188
188
  }
189
189
  };
190
+ const FULL_TREE_NEXT_START_ENTRYPOINT = "prisma-next-start.cjs";
191
+ const FULL_TREE_NEXT_START_SOURCE = [
192
+ "process.chdir(__dirname);",
193
+ "process.env.NODE_ENV = \"production\";",
194
+ "process.argv.push(\"start\", \"-p\", process.env.PORT ?? \"3000\");",
195
+ "require(\"next/dist/bin/next\");",
196
+ ""
197
+ ].join("\n");
198
+ async function stageNextjsFullTreeFallbackArtifact(appPath, signal) {
199
+ const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-")));
200
+ try {
201
+ const artifactDir = path.join(outDir, "app");
202
+ await unsupportedFilesystemBoundary(signal, () => cp(appPath, artifactDir, {
203
+ recursive: true,
204
+ verbatimSymlinks: true,
205
+ filter: (source) => !isExcludedFromFullTreeArtifact(path.basename(source))
206
+ }));
207
+ await unsupportedFilesystemBoundary(signal, () => writeFile(path.join(artifactDir, FULL_TREE_NEXT_START_ENTRYPOINT), FULL_TREE_NEXT_START_SOURCE));
208
+ return {
209
+ directory: artifactDir,
210
+ entrypoint: FULL_TREE_NEXT_START_ENTRYPOINT,
211
+ defaultPortMapping: { http: 3e3 },
212
+ cleanup: () => rm(outDir, {
213
+ recursive: true,
214
+ force: true
215
+ })
216
+ };
217
+ } catch (error) {
218
+ await rm(outDir, {
219
+ recursive: true,
220
+ force: true
221
+ });
222
+ throw error;
223
+ }
224
+ }
225
+ /** Excludes VCS internals and dotenv files (local secrets, superseded by the deploy env). */
226
+ function isExcludedFromFullTreeArtifact(basename) {
227
+ return basename === ".git" || basename === ".env" || basename.startsWith(".env.");
228
+ }
190
229
  var PreviewTanstackStartBuild = class {
191
230
  #appPath;
192
231
  #buildSettings;
@@ -362,23 +401,31 @@ async function normalizeArtifactSymlinks(artifactDir, appPath, signal) {
362
401
  continue;
363
402
  }
364
403
  if (!entry.isSymbolicLink()) continue;
365
- const target = await unsupportedFilesystemBoundary(signal, () => readlink(fullPath));
366
- const resolvedTarget = path.resolve(path.dirname(fullPath), target);
367
- if (isPathWithin(normalizedArtifactDir, resolvedTarget)) continue;
368
- if (!isPathWithin(normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
369
- const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(resolvedTarget));
370
- await unsupportedFilesystemBoundary(signal, () => rm(fullPath, {
371
- force: true,
372
- recursive: true
373
- }));
374
- await unsupportedFilesystemBoundary(signal, () => cp(resolvedTarget, fullPath, {
375
- recursive: targetStat.isDirectory(),
376
- dereference: true
377
- }));
378
- if (targetStat.isDirectory()) await walkDirectory(fullPath);
404
+ if (await materializeArtifactSymlink({
405
+ fullPath,
406
+ normalizedArtifactDir,
407
+ normalizedAppPath,
408
+ signal
409
+ }) === "directory") await walkDirectory(fullPath);
379
410
  }
380
411
  }
381
412
  }
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
+ }
382
429
  function isPathWithin(rootPath, candidatePath) {
383
430
  const relativePath = path.relative(rootPath, candidatePath);
384
431
  return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
@@ -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 };
@@ -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;
@@ -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";
@@ -1,6 +1,6 @@
1
1
  import { CliError, usageError } from "../../shell/errors.js";
2
- import "../../shell/command-arguments.js";
3
2
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
3
+ import "../../shell/command-arguments.js";
4
4
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
5
5
  import { Result, matchError } from "better-result";
6
6
  //#region src/lib/project/setup.ts
@@ -13,7 +13,8 @@ function validateProjectSetupNameText(value, fallback) {
13
13
  }
14
14
  function resolveProjectForSetup(projectRef, projects, workspace) {
15
15
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
16
- if (matches.length === 1) return matches[0];
16
+ const match = matches[0];
17
+ if (matches.length === 1 && match) return match;
17
18
  if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
18
19
  throw projectNotFoundError(projectRef, workspace);
19
20
  }
@@ -1,5 +1,5 @@
1
- import { maskValue, padDisplay, renderSummaryLine } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { maskValue, padDisplay, renderSummaryLine } from "../shell/ui.js";
3
3
  import stringWidth from "string-width";
4
4
  //#region src/output/patterns.ts
5
5
  function renderList(input, ui) {
@@ -56,7 +56,7 @@ function serializeAppDeploy(result) {
56
56
  };
57
57
  }
58
58
  function renderBranchDatabaseDeploySummary(context, result) {
59
- if (!result.branchDatabase || result.branchDatabase.status !== "created") return [];
59
+ if (result.branchDatabase?.status !== "created") return [];
60
60
  return ["", ...renderDeployOutputRows(context.ui, [
61
61
  {
62
62
  label: "Database",
@@ -1,5 +1,5 @@
1
- import { formatColumns } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { formatColumns } from "../shell/ui.js";
3
3
  import { renderResolvedProjectContextBlock } from "./verbose-context.js";
4
4
  //#region src/presenters/branch.ts
5
5
  function renderBranchList(context, descriptor, result) {
@@ -1,5 +1,5 @@
1
- import { formatColumns, renderSummaryLine } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { formatColumns, renderSummaryLine } from "../shell/ui.js";
3
3
  import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
4
4
  import { renderResolvedProjectContextBlock, stripVerboseContext } from "./verbose-context.js";
5
5
  //#region src/presenters/database.ts
@@ -234,7 +234,7 @@ function renderDatabaseConnectionRemove(context, descriptor, result) {
234
234
  }, context.ui);
235
235
  }
236
236
  function serializeDatabaseConnectionRemove(result) {
237
- return result;
237
+ return { connection: result.connection };
238
238
  }
239
239
  function formatStatus(database) {
240
240
  return database.status ?? (database.isDefault ? "default" : "unknown");
@@ -1,5 +1,5 @@
1
- import { padDisplay, renderNextSteps, renderSummaryLine, renderVerboseBlock } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { padDisplay, renderNextSteps, renderSummaryLine, renderVerboseBlock } from "../shell/ui.js";
3
3
  import { formatCommandArgument } from "../shell/command-arguments.js";
4
4
  import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
5
5
  import { renderResolvedProjectContextBlock } from "./verbose-context.js";
@@ -1,8 +1,8 @@
1
1
  import { CliError, authRequiredError, commandCanceledError } from "./errors.js";
2
- import { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess } from "./output.js";
3
2
  import { getCommandDescriptor } from "./command-meta.js";
4
3
  import { resolveGlobalFlags } from "./global-flags.js";
5
4
  import { createCommandContext } from "./runtime.js";
5
+ import { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess } from "./output.js";
6
6
  import { collectCommandDiagnostics } from "../lib/diagnostics.js";
7
7
  import { renderCommandDiagnostics } from "./diagnostics-output.js";
8
8
  import { AuthError } from "@prisma/management-api-sdk";
@@ -18,44 +18,54 @@ function isCancellationError(error) {
18
18
  return error.name === "AbortError" || error.name === "CancelledError";
19
19
  }
20
20
  async function runCommand(runtime, commandName, options, handler, presenter) {
21
- const flags = resolveGlobalFlags(runtime.argv, options);
22
- const context = await createCommandContext(runtime, flags);
21
+ const context = await createCommandContext(runtime, resolveGlobalFlags(runtime.argv, options));
23
22
  const descriptor = getCommandDescriptor(commandName);
24
23
  const startedAt = Date.now();
25
24
  try {
26
- const success = await handler(context);
27
- if (flags.json) {
28
- writeJsonSuccess(context.output, {
29
- ...success,
30
- result: presenter.renderJson ? presenter.renderJson(success.result) : success.result
31
- });
32
- return;
33
- }
34
- const stdout = presenter.renderStdout?.(context, descriptor, success.result) ?? [];
35
- if (flags.quiet) {
36
- if (stdout.length > 0) context.output.stdout.write(`${stdout.join("\n")}\n`);
37
- return;
38
- }
39
- const rendered = presenter.renderHuman(context, descriptor, success.result);
40
- const diagnostics = await renderBestEffortCommandDiagnostics(context, {
41
- enabled: flags.verbose && rendered.length > 0,
42
- durationMs: Date.now() - startedAt
43
- });
44
- const humanLines = [...rendered, ...diagnostics];
45
- if (stdout.length > 0 && humanLines.length > 0) humanLines.push("");
46
- writeHumanLines(context.output, humanLines);
47
- if (stdout.length > 0) context.output.stdout.write(`${stdout.join("\n")}\n`);
25
+ await writeCommandSuccess(context, descriptor, await handler(context), presenter, Date.now() - startedAt);
48
26
  } catch (error) {
49
27
  const cliError = toCliError(error, runtime);
50
28
  if (cliError) {
51
- if (flags.json) writeJsonError(context.output, commandName, cliError);
52
- else writeHumanError(context.output, context.ui, cliError, { trace: flags.trace });
29
+ writeCommandError(context, commandName, cliError);
53
30
  process.exitCode = cliError.exitCode;
54
31
  return;
55
32
  }
56
33
  throw error;
57
34
  }
58
35
  }
36
+ async function writeCommandSuccess(context, descriptor, success, presenter, durationMs) {
37
+ if (context.flags.json) {
38
+ writeJsonSuccess(context.output, {
39
+ ...success,
40
+ result: presenter.renderJson ? presenter.renderJson(success.result) : success.result
41
+ });
42
+ return;
43
+ }
44
+ const stdout = presenter.renderStdout?.(context, descriptor, success.result) ?? [];
45
+ if (context.flags.quiet) {
46
+ writeStdoutLines(context, stdout);
47
+ return;
48
+ }
49
+ const rendered = presenter.renderHuman(context, descriptor, success.result);
50
+ const diagnostics = await renderBestEffortCommandDiagnostics(context, {
51
+ enabled: context.flags.verbose && rendered.length > 0,
52
+ durationMs
53
+ });
54
+ const humanLines = [...rendered, ...diagnostics];
55
+ if (stdout.length > 0 && humanLines.length > 0) humanLines.push("");
56
+ writeHumanLines(context.output, humanLines);
57
+ writeStdoutLines(context, stdout);
58
+ }
59
+ function writeCommandError(context, commandName, cliError) {
60
+ if (context.flags.json) {
61
+ writeJsonError(context.output, commandName, cliError);
62
+ return;
63
+ }
64
+ writeHumanError(context.output, context.ui, cliError, { trace: context.flags.trace });
65
+ }
66
+ function writeStdoutLines(context, lines) {
67
+ if (lines.length > 0) context.output.stdout.write(`${lines.join("\n")}\n`);
68
+ }
59
69
  async function renderBestEffortCommandDiagnostics(context, options) {
60
70
  if (!options.enabled) return [];
61
71
  try {
@@ -1,6 +1,6 @@
1
- import { createShellUi, padDisplay, wrapText } from "./ui.js";
2
1
  import { formatDescriptorLabel, getDescriptorForCommand } from "./command-meta.js";
3
2
  import { COMPACT_GLOBAL_OPTION_FLAGS, resolveGlobalFlags } from "./global-flags.js";
3
+ import { createShellUi, padDisplay, wrapText } from "./ui.js";
4
4
  //#region src/shell/help.ts
5
5
  function renderHelp(command, runtime) {
6
6
  const descriptor = getDescriptorForCommand(command);
@@ -10,28 +10,38 @@ function renderHelp(command, runtime) {
10
10
  const visibleCommands = command.commands.filter((candidate) => candidate.name() !== "help" && !candidate.hidden);
11
11
  const visibleOptions = command.options.filter((candidate) => !candidate.hidden);
12
12
  if (visibleCommands.length > 0) lines.push(...renderCommandRows(rail, ui, visibleCommands));
13
- if (descriptor.longDescription) {
14
- lines.push(`${rail}`);
15
- const wrapped = wrapText(descriptor.longDescription, Math.max(ui.width - 3, 40));
16
- for (const line of wrapped) lines.push(`${rail} ${line}`);
17
- }
18
- if (visibleOptions.length > 0) {
19
- if (visibleCommands.length > 0) lines.push(`${rail}`);
20
- if (visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags))) lines.push(`${rail} Global options:`);
21
- lines.push(...renderOptionRows(rail, ui, visibleOptions));
22
- }
23
- if (descriptor.examples && descriptor.examples.length > 0) {
24
- lines.push(`${rail}`);
25
- lines.push(`${rail} Examples:`);
26
- for (const example of descriptor.examples) lines.push(`${rail} $ ${example}`);
27
- }
28
- if (descriptor.docsPath) {
29
- lines.push(`${rail}`);
30
- lines.push(`${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(descriptor.docsPath)}`);
31
- }
13
+ lines.push(...renderLongDescription(rail, ui, descriptor.longDescription));
14
+ lines.push(...renderVisibleOptions(rail, ui, visibleCommands, visibleOptions));
15
+ lines.push(...renderExamples(rail, descriptor.examples));
16
+ lines.push(...renderDocsPath(rail, ui, descriptor.docsPath));
32
17
  lines.push("");
33
18
  return `${lines.join("\n")}`;
34
19
  }
20
+ function renderLongDescription(rail, ui, longDescription) {
21
+ if (!longDescription) return [];
22
+ return [`${rail}`, ...wrapText(longDescription, Math.max(ui.width - 3, 40)).map((line) => `${rail} ${line}`)];
23
+ }
24
+ function renderVisibleOptions(rail, ui, visibleCommands, visibleOptions) {
25
+ if (visibleOptions.length === 0) return [];
26
+ const lines = visibleCommands.length > 0 ? [`${rail}`] : [];
27
+ if (shouldLabelGlobalOptions(visibleCommands, visibleOptions)) lines.push(`${rail} Global options:`);
28
+ return [...lines, ...renderOptionRows(rail, ui, visibleOptions)];
29
+ }
30
+ function shouldLabelGlobalOptions(visibleCommands, visibleOptions) {
31
+ return visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags));
32
+ }
33
+ function renderExamples(rail, examples) {
34
+ if (!examples || examples.length === 0) return [];
35
+ return [
36
+ `${rail}`,
37
+ `${rail} Examples:`,
38
+ ...examples.map((example) => `${rail} $ ${example}`)
39
+ ];
40
+ }
41
+ function renderDocsPath(rail, ui, docsPath) {
42
+ if (!docsPath) return [];
43
+ return [`${rail}`, `${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(docsPath)}`];
44
+ }
35
45
  function renderCommandRows(rail, ui, commands) {
36
46
  return renderAlignedRows(rail, ui, commands.map((command) => {
37
47
  const descriptor = getDescriptorForCommand(command);
@@ -1,6 +1,6 @@
1
- import { createShellUi } from "./ui.js";
2
1
  import { LocalStateStore } from "../adapters/local-state.js";
3
2
  import { MockApi } from "../adapters/mock-api.js";
3
+ import { createShellUi } from "./ui.js";
4
4
  import { renderHelp } from "./help.js";
5
5
  import path from "node:path";
6
6
  //#region src/shell/runtime.ts