Skip to content

Commit 61f8e8c

Browse files
fix(cli): format new database command hints with the detected package runner
usage/backup/restore/rotate recovery commands, confirmation nextSteps, and result nextSteps now render through the project's package runner (pnpm dlx / bunx / npx -y with @prisma/cli@latest) instead of the hardcoded prisma-cli bin name, matching the agent group's convention. The management provider takes an optional formatter so its error factories stay consistent with the invoking directory.
1 parent bad875c commit 61f8e8c

3 files changed

Lines changed: 123 additions & 24 deletions

File tree

packages/cli/src/controllers/database.ts

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { randomBytes } from "node:crypto";
22

3+
import {
4+
type PrismaCliPackageCommandFormatter,
5+
resolvePrismaCliPackageCommandFormatterSync,
6+
} from "../lib/agent/cli-command";
37
import { requireComputeAuth } from "../lib/auth/guard";
48
import {
59
createManagementDatabaseProvider,
@@ -363,15 +367,26 @@ export async function runDatabaseUsage(
363367
databaseRef: string,
364368
flags: DatabaseUsageFlags,
365369
): Promise<CommandSuccess<DatabaseUsageResult>> {
366-
const from = parseUsageDate(flags.from, "--from");
367-
const to = parseUsageDate(flags.to, "--to");
370+
const formatCommand = resolvePrismaCliPackageCommandFormatterSync(
371+
context.runtime.cwd,
372+
);
373+
const from = parseUsageDate(flags.from, "--from", formatCommand);
374+
const to = parseUsageDate(flags.to, "--to", formatCommand);
368375
if (from && to && Date.parse(from) > Date.parse(to)) {
369376
throw usageError(
370377
"Invalid usage period",
371378
"--from must not be later than --to.",
372379
"Pass a --from date that is on or before the --to date.",
373380
[
374-
"prisma-cli database usage <database> --from 2026-06-01 --to 2026-06-30",
381+
formatCommand([
382+
"database",
383+
"usage",
384+
"<database>",
385+
"--from",
386+
"2026-06-01",
387+
"--to",
388+
"2026-06-30",
389+
]),
375390
],
376391
"database",
377392
);
@@ -416,7 +431,10 @@ export async function runDatabaseBackupList(
416431
databaseRef: string,
417432
flags: DatabaseBackupListFlags,
418433
): Promise<CommandSuccess<DatabaseBackupListResult>> {
419-
const limit = parseBackupLimit(flags.limit);
434+
const limit = parseBackupLimit(
435+
flags.limit,
436+
resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd),
437+
);
420438

421439
const { provider, target } = await requireDatabaseContext(
422440
context,
@@ -456,13 +474,16 @@ export async function runDatabaseRestore(
456474
databaseRef: string,
457475
flags: DatabaseRestoreFlags,
458476
): Promise<CommandSuccess<DatabaseRestoreResult>> {
477+
const formatCommand = resolvePrismaCliPackageCommandFormatterSync(
478+
context.runtime.cwd,
479+
);
459480
const backupId = flags.backupId?.trim();
460481
if (!backupId) {
461482
throw usageError(
462483
"Backup id required",
463484
"Database restore needs the backup to restore from.",
464-
"Pass --backup <backup-id> from prisma-cli database backup list.",
465-
["prisma-cli database backup list <database>"],
485+
`Pass --backup <backup-id> from ${formatCommand(["database", "backup", "list", "<database>"])}.`,
486+
[formatCommand(["database", "backup", "list", "<database>"])],
466487
"database",
467488
);
468489
}
@@ -500,7 +521,7 @@ export async function runDatabaseRestore(
500521
confirm: flags.confirm,
501522
summary: "Confirm database restore",
502523
why: "Restoring immediately and irreversibly overwrites all data in the target database, so it requires the exact target database id.",
503-
nextStep: `prisma-cli database restore ${database.id} --backup ${backupId}${sourceDatabaseArg} --confirm ${database.id}`,
524+
nextStep: `${formatCommand(["database", "restore", database.id, "--backup", backupId])}${sourceDatabaseArg} --confirm ${database.id}`,
504525
});
505526

506527
const restored = await provider.restoreDatabase({
@@ -524,7 +545,7 @@ export async function runDatabaseRestore(
524545
},
525546
},
526547
warnings: [],
527-
nextSteps: [`prisma-cli database show ${database.id}`],
548+
nextSteps: [formatCommand(["database", "show", database.id])],
528549
};
529550
}
530551

@@ -533,14 +554,24 @@ export async function runDatabaseConnectionRotate(
533554
connectionRef: string,
534555
flags: DatabaseConnectionRotateFlags,
535556
): Promise<CommandSuccess<DatabaseConnectionRotateResult>> {
557+
const formatCommand = resolvePrismaCliPackageCommandFormatterSync(
558+
context.runtime.cwd,
559+
);
536560
const connectionId = connectionRef.trim();
537561
if (!connectionId) {
538562
throw usageError(
539563
"Connection id required",
540564
"Database connection rotation needs a connection id.",
541565
"Pass the connection id to rotate.",
542566
[
543-
"prisma-cli database connection rotate <connection-id> --confirm <connection-id>",
567+
formatCommand([
568+
"database",
569+
"connection",
570+
"rotate",
571+
"<connection-id>",
572+
"--confirm",
573+
"<connection-id>",
574+
]),
544575
],
545576
"database",
546577
);
@@ -553,6 +584,14 @@ export async function runDatabaseConnectionRotate(
553584
confirm: flags.confirm,
554585
summary: "Confirm database connection rotation",
555586
why: "Rotating revokes the previous credentials and breaks clients still using them, so it requires the exact connection id.",
587+
nextStep: formatCommand([
588+
"database",
589+
"connection",
590+
"rotate",
591+
connectionId,
592+
"--confirm",
593+
connectionId,
594+
]),
556595
});
557596

558597
const provider = await requireDatabaseProviderOnly(context);
@@ -578,6 +617,7 @@ const USAGE_DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
578617
function parseUsageDate(
579618
value: string | undefined,
580619
flagName: string,
620+
formatCommand: PrismaCliPackageCommandFormatter,
581621
): string | undefined {
582622
if (value === undefined) {
583623
return undefined;
@@ -603,7 +643,17 @@ function parseUsageDate(
603643
"Invalid usage period",
604644
`${flagName} must be an ISO date such as 2026-06-01 or an ISO datetime such as 2026-06-01T12:00:00Z.`,
605645
`Pass an ISO date or datetime to ${flagName}.`,
606-
["prisma-cli database usage <database> --from 2026-06-01 --to 2026-06-30"],
646+
[
647+
formatCommand([
648+
"database",
649+
"usage",
650+
"<database>",
651+
"--from",
652+
"2026-06-01",
653+
"--to",
654+
"2026-06-30",
655+
]),
656+
],
607657
"database",
608658
);
609659
}
@@ -616,7 +666,10 @@ function isValidCalendarDate(datePart: string): boolean {
616666
);
617667
}
618668

619-
function parseBackupLimit(value: string | undefined): number | undefined {
669+
function parseBackupLimit(
670+
value: string | undefined,
671+
formatCommand: PrismaCliPackageCommandFormatter,
672+
): number | undefined {
620673
if (value === undefined) {
621674
return undefined;
622675
}
@@ -627,7 +680,16 @@ function parseBackupLimit(value: string | undefined): number | undefined {
627680
"Invalid backup limit",
628681
"--limit must be an integer between 1 and 100.",
629682
"Pass a --limit between 1 and 100.",
630-
["prisma-cli database backup list <database> --limit 50"],
683+
[
684+
formatCommand([
685+
"database",
686+
"backup",
687+
"list",
688+
"<database>",
689+
"--limit",
690+
"50",
691+
]),
692+
],
631693
"database",
632694
);
633695
}
@@ -668,7 +730,11 @@ async function requireDatabaseContext(
668730
}
669731

670732
return {
671-
provider: createManagementDatabaseProvider(client),
733+
provider: createManagementDatabaseProvider(client, {
734+
formatCommand: resolvePrismaCliPackageCommandFormatterSync(
735+
context.runtime.cwd,
736+
),
737+
}),
672738
target: targetResult.value,
673739
};
674740
}
@@ -703,7 +769,11 @@ async function requireDatabaseProviderOnly(
703769
if (!client) {
704770
throw authRequiredError();
705771
}
706-
return createManagementDatabaseProvider(client);
772+
return createManagementDatabaseProvider(client, {
773+
formatCommand: resolvePrismaCliPackageCommandFormatterSync(
774+
context.runtime.cwd,
775+
),
776+
});
707777
}
708778

709779
return createFixtureDatabaseProvider(context);
@@ -805,7 +875,11 @@ function createFixtureDatabaseProvider(
805875
throw databaseNotFoundError(options.targetDatabaseId);
806876
}
807877
if (restored.outcome === "backup-not-found") {
808-
throw backupNotFoundError(options.backupId, options.sourceDatabaseId);
878+
throw backupNotFoundError(
879+
options.backupId,
880+
options.sourceDatabaseId,
881+
resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd),
882+
);
809883
}
810884
return normalizeDatabase(restored.database, options.projectId);
811885
},
@@ -981,15 +1055,22 @@ function databaseAmbiguousError(
9811055
function backupNotFoundError(
9821056
backupId: string,
9831057
sourceDatabaseId: string,
1058+
formatCommand: PrismaCliPackageCommandFormatter,
9841059
): CliError {
1060+
const listCommand = formatCommand([
1061+
"database",
1062+
"backup",
1063+
"list",
1064+
sourceDatabaseId,
1065+
]);
9851066
return new CliError({
9861067
code: "DATABASE_BACKUP_NOT_FOUND",
9871068
domain: "database",
9881069
summary: "Database backup not found",
9891070
why: `No backup matched "${backupId}" for database "${sourceDatabaseId}".`,
990-
fix: "Pass a backup id from prisma-cli database backup list.",
1071+
fix: `Pass a backup id from ${listCommand}.`,
9911072
exitCode: 1,
992-
nextSteps: [`prisma-cli database backup list ${sourceDatabaseId}`],
1073+
nextSteps: [listCommand],
9931074
});
9941075
}
9951076

packages/cli/src/lib/database/provider.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// biome-ignore-all lint/performance/noAwaitInLoops: Database pagination requests must run sequentially.
22
import type { ManagementApiClient } from "@prisma/management-api-sdk";
33

4+
import { formatPrismaCliCommand } from "../../shell/cli-command";
45
import { CliError } from "../../shell/errors";
56
import type {
67
DatabaseConnectionSummary,
78
DatabaseSummary,
89
} from "../../types/database";
10+
import type { PrismaCliPackageCommandFormatter } from "../agent/cli-command";
911

1012
export interface DatabaseCreateInput {
1113
projectId: string;
@@ -207,7 +209,11 @@ interface RawDatabaseRecord {
207209

208210
export function createManagementDatabaseProvider(
209211
client: ManagementApiClient,
212+
options?: { formatCommand?: PrismaCliPackageCommandFormatter },
210213
): DatabaseProvider {
214+
const formatCommand =
215+
options?.formatCommand ?? ((args) => formatPrismaCliCommand(args));
216+
211217
return {
212218
async listDatabases(options) {
213219
const databases: RawDatabaseRecord[] = [];
@@ -445,12 +451,16 @@ export function createManagementDatabaseProvider(
445451
},
446452
);
447453
if (result.response?.status === 409) {
448-
throw restoreConflictError(options.targetDatabaseId, result.error);
454+
throw restoreConflictError(
455+
options.targetDatabaseId,
456+
result.error,
457+
formatCommand,
458+
);
449459
}
450460
// Target and source databases are resolved before this call, so a 404
451461
// here identifies the backup.
452462
if (result.response?.status === 404) {
453-
throw restoreBackupNotFoundError(options, result.error);
463+
throw restoreBackupNotFoundError(options, result.error, formatCommand);
454464
}
455465
if (result.error || !result.data) {
456466
throw databaseApiError(
@@ -698,23 +708,31 @@ function backupsUnsupportedError(
698708
function restoreBackupNotFoundError(
699709
options: { backupId: string; sourceDatabaseId: string },
700710
error: RawApiErrorBody | undefined,
711+
formatCommand: PrismaCliPackageCommandFormatter,
701712
): CliError {
713+
const listCommand = formatCommand([
714+
"database",
715+
"backup",
716+
"list",
717+
options.sourceDatabaseId,
718+
]);
702719
return new CliError({
703720
code: "DATABASE_BACKUP_NOT_FOUND",
704721
domain: "database",
705722
summary: "Database backup not found",
706723
why:
707724
error?.error?.message ??
708725
`No backup matched "${options.backupId}" for database "${options.sourceDatabaseId}".`,
709-
fix: "Pass a backup id from prisma-cli database backup list.",
726+
fix: `Pass a backup id from ${listCommand}.`,
710727
exitCode: 1,
711-
nextSteps: [`prisma-cli database backup list ${options.sourceDatabaseId}`],
728+
nextSteps: [listCommand],
712729
});
713730
}
714731

715732
function restoreConflictError(
716733
targetDatabaseId: string,
717734
error: RawApiErrorBody | undefined,
735+
formatCommand: PrismaCliPackageCommandFormatter,
718736
): CliError {
719737
return new CliError({
720738
code: "DATABASE_RESTORE_CONFLICT",
@@ -725,7 +743,7 @@ function restoreConflictError(
725743
`Database "${targetDatabaseId}" is provisioning or already recovering.`,
726744
fix: "Wait for the database to become ready, then retry the restore.",
727745
exitCode: 1,
728-
nextSteps: [`prisma-cli database show ${targetDatabaseId}`],
746+
nextSteps: [formatCommand(["database", "show", targetDatabaseId])],
729747
});
730748
}
731749

packages/cli/tests/database.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ describe("database commands", () => {
798798
backupId: "bkp_101",
799799
},
800800
},
801-
nextSteps: ["prisma-cli database show db_123"],
801+
nextSteps: ["npx -y @prisma/cli@latest database show db_123"],
802802
});
803803
});
804804

@@ -868,7 +868,7 @@ describe("database commands", () => {
868868
expect(result.exitCode).toBe(2);
869869
expect(payload.error.code).toBe("CONFIRMATION_REQUIRED");
870870
expect(payload.nextSteps).toEqual([
871-
"prisma-cli database restore db_123 --backup bkp_201 --source-database db_456 --confirm db_123",
871+
"npx -y @prisma/cli@latest database restore db_123 --backup bkp_201 --source-database db_456 --confirm db_123",
872872
]);
873873
});
874874

0 commit comments

Comments
 (0)