Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ older cached version. Prisma Compute does not support Deno deployments yet.
- `--deploy` / `--no-deploy`
- `--workspace <id-or-name>`
- `--yes`
- `--force`
- `--force`: overwrite generated starter and Prisma files in a non-empty directory. This replaces existing Prisma config, contract, and database-client files; back up edits first.
- `--verbose`
- `--json`

Expand Down
1 change: 1 addition & 0 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const executeCreateContext = Effect.fn("Create.execute")(function* (context: Cre
template: context.template,
createdProjectPath: context.targetDirectory,
includeDevNextStep: true,
force: context.force,
initializeGit: !context.targetPathState.exists || context.targetPathState.isEmptyDirectory,
progressSpinner: createSpinner,
});
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ export const createPrismaCommand = Command.make(
),
deploy: optionalBoolean("deploy", "Deploy the generated app to Prisma immediately"),
workspace: optionalString("workspace", "Prisma workspace id or name to deploy into"),
force: optionalBoolean("force", "Allow scaffolding into a non-empty target directory"),
force: optionalBoolean(
"force",
"Overwrite generated starter and Prisma files in a non-empty directory",
),
yes: optionalBoolean("yes", "Skip prompts and accept default choices"),
verbose: optionalBoolean("verbose", "Show verbose command output during setup"),
json: optionalBoolean(
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/composer/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
getLocalPackageBinaryCommand,
getRunScriptCommand,
} from "../../utils/package-manager";
import { decodePrismaCommandResult, runPrismaJsonCommandEffect } from "./prisma-cli";
import { decodePrismaCommandResult, runPrismaJsonCommandEffect } from "../prisma-cli";
import { getWorkspaceLabel } from "./workspace";

const WhoamiResultSchema = Schema.Struct({
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/composer/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Effect, Schema } from "effect";
import { CreateFailure } from "../../create-outcome";
import { PrismaWorkspaceSchema, type PrismaWorkspace } from "../../result";
import type { PackageManager } from "../../types";
import { decodePrismaCommandResult, runPrismaJsonCommandEffect } from "./prisma-cli";
import { decodePrismaCommandResult, runPrismaJsonCommandEffect } from "../prisma-cli";
import { getWorkspaceLabel } from "./workspace";

const ProjectShowResultSchema = Schema.Struct({
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/deploy-with-composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
ComposerDeployCommandResultSchema,
parseComposerDeployResult,
} from "./composer/deployment-result";
import { decodePrismaCommandResult, runPrismaJsonCommandEffect } from "./composer/prisma-cli";
import { decodePrismaCommandResult, runPrismaJsonCommandEffect } from "./prisma-cli";
import { ensureProjectNameAvailable, getProjectDetails } from "./composer/projects";

export type ComposerDeployExecutionResult =
Expand Down Expand Up @@ -259,6 +259,6 @@ export async function deployNewProjectWithComposer(
}

export { parseComposerDeployResult } from "./composer/deployment-result";
export { parsePrismaCliEnvelope, PrismaCliCommandError } from "./composer/prisma-cli";
export { parsePrismaCliEnvelope, PrismaCliCommandError } from "./prisma-cli";
export { findProjectNameCollisions, getConsoleProjectUrl } from "./composer/projects";
export type { ComposerDeployResult } from "../result";
29 changes: 16 additions & 13 deletions src/tasks/composer/prisma-cli.ts → src/tasks/prisma-cli.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Effect, Schema } from "effect";

import { PrismaCliCommandError } from "../../create-outcome";
import { CommandRunner } from "../../services/command-runner";
import type { PackageManager } from "../../types";
import { getErrorMessage } from "../../utils/errors";
import { getLocalPackageBinaryArgs } from "../../utils/package-manager";
import { PrismaCliCommandError } from "../create-outcome";
import { CommandRunner } from "../services/command-runner";
import type { PackageManager } from "../types";
import { getErrorMessage, redactSecrets } from "../utils/errors";
import { getLocalPackageBinaryArgs } from "../utils/package-manager";

const PrismaCliEnvelopeSchema = Schema.Struct({
ok: Schema.Boolean,
Expand Down Expand Up @@ -47,6 +47,7 @@ export const runPrismaJsonCommandEffect = Effect.fn("PrismaCli.runJson")(functio
packageManager: PackageManager;
projectDir: string;
args: string[];
env?: NodeJS.ProcessEnv;
onStderrLine?: (line: string) => void;
}) {
const runner = yield* CommandRunner;
Expand All @@ -59,7 +60,7 @@ export const runPrismaJsonCommandEffect = Effect.fn("PrismaCli.runJson")(functio
command: invocation.command,
args: invocation.args,
cwd: options.projectDir,
env: process.env,
env: options.env ?? process.env,
...(options.onStderrLine ? { onStderrLine: options.onStderrLine } : {}),
});

Expand All @@ -68,24 +69,26 @@ export const runPrismaJsonCommandEffect = Effect.fn("PrismaCli.runJson")(functio
envelope = parsePrismaCliEnvelope(result.stdout);
} catch (cause) {
return yield* new PrismaCliCommandError({
message: result.stderr.trim() || getErrorMessage(cause),
stderr: result.stderr,
message:
redactSecrets(result.stderr.trim() || result.stdout.trim()) || getErrorMessage(cause),
stderr: redactSecrets(result.stderr),
exitCode: result.exitCode,
});
}

if (result.exitCode !== 0 || !envelope.ok || envelope.result === undefined) {
const summary = envelope.error?.summary ?? envelope.error?.message;
return yield* new PrismaCliCommandError({
message:
message: redactSecrets(
[summary, envelope.error?.why].filter(Boolean).join(": ") ||
result.stderr.trim() ||
"Prisma CLI command failed.",
result.stderr.trim() ||
"Prisma CLI command failed.",
),
...(envelope.commandId || envelope.command
? { command: envelope.commandId ?? envelope.command }
: {}),
...(envelope.error?.code ? { code: envelope.error.code } : {}),
stderr: result.stderr,
stderr: redactSecrets(result.stderr),
exitCode: result.exitCode,
});
}
Expand All @@ -102,4 +105,4 @@ export const decodePrismaCommandResult = <A>(schema: Schema.Codec<A>, value: unk
),
);

export { PrismaCliCommandError } from "../../create-outcome";
export { PrismaCliCommandError } from "../create-outcome";
24 changes: 15 additions & 9 deletions src/tasks/prisma-setup/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import path from "node:path";

import type { AuthoringStyle, DatabaseProvider } from "../../types";
import { getLocalPackageBinaryArgs } from "../../utils/package-manager";
import { runSetupCommand } from "../../utils/run-command";
import { redactSecrets } from "../../utils/errors";
import { runPrismaJsonCommandEffect } from "../prisma-cli";
import type { PrismaSetupContext } from "./types";

const getContractPath = (authoring: AuthoringStyle) =>
Expand All @@ -24,25 +25,30 @@ export const runPrismaCli = Effect.fn("PrismaSetup.runCli")(function* (
log.step([invocation.command, ...invocation.args].join(" "), { output: context.output });
}
});
yield* runSetupCommand({
command: invocation.command,
args: invocation.args,
cwd: projectDir,
yield* runPrismaJsonCommandEffect({
packageManager: context.packageManager,
projectDir,
args,
env: { ...process.env, CI: "1" },
verbose: context.verbose,
json: context.json,
...(context.verbose
? {
onStderrLine: (line: string) =>
log.message(redactSecrets(line), { output: context.output }),
}
: {}),
});
});

export const runPrismaInit = Effect.fn("PrismaSetup.init")(function* (
context: PrismaSetupContext,
projectDir: string,
force = false,
) {
yield* runPrismaCli(context, projectDir, [
"orm",
"init",
"--yes",
"--no-interactive",
...(force ? ["--confirm", path.basename(projectDir).trim() || projectDir.trim()] : []),
"--target",
getInitTarget(context.databaseProvider),
"--authoring",
Expand All @@ -62,5 +68,5 @@ export const initializeAgentSkills = Effect.fn("PrismaSetup.initializeSkills")(f
projectDir: string,
) {
if (context.packageManager === "deno") return;
yield* runPrismaCli(context, projectDir, ["init", "--yes", "--no-interactive"]);
yield* runPrismaCli(context, projectDir, ["init", "--yes"]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
1 change: 1 addition & 0 deletions src/tasks/prisma-setup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type PrismaSetupRunOptions = {
createdProjectPath?: string;
includeDevNextStep?: boolean;
initializeGit?: boolean;
force?: boolean;
progressSpinner?: ReturnType<typeof spinner>;
};

Expand Down
2 changes: 1 addition & 1 deletion src/tasks/setup-prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(

yield* Effect.sync(() => progress?.message("Preparing Prisma 8 project files..."));
yield* atCreateStage(
runPrismaInit(context, projectDir),
runPrismaInit(context, projectDir, options.force),
"initialize_prisma",
"prisma_init_failed",
);
Expand Down
3 changes: 3 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PrismaCliCommandError } from "../create-outcome";

export function redactSecrets(message: string): string {
return message
.replace(
Expand All @@ -12,6 +14,7 @@ export function redactSecrets(message: string): string {
}

export function getErrorMessage(error: unknown): string {
if (error instanceof PrismaCliCommandError) return redactSecrets(error.message);
if (error instanceof Error && "stderr" in error) {
const stderr = String((error as { stderr?: string }).stderr ?? "").trim();
if (stderr) return redactSecrets(stderr);
Expand Down
10 changes: 10 additions & 0 deletions tests/deploy-with-composer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ describe("redactSecrets", () => {

expect(getErrorMessage(error)).toBe("DATABASE_URL=<redacted>");
});

test("prefers a structured Prisma error over package-manager stderr", () => {
const error = new PrismaCliCommandError({
message: "Explicit overwrite consent is required",
code: "CLI.CONSENT_REQUIRED",
stderr: 'error: "prisma" exited with code 2',
exitCode: 2,
});
expect(getErrorMessage(error)).toBe("Explicit overwrite consent is required");
});
});

describe("findProjectNameCollisions", () => {
Expand Down
46 changes: 46 additions & 0 deletions tests/e2e/create-prisma.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,52 @@ afterEach(async () => {
});

describe("create-prisma e2e", () => {
test(
"resumes a partially scaffolded app only with explicit --force",
async () => {
const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-force-e2e-"));
tempRoots.push(rootDir);
const args = [
"retry app",
"--template",
"minimal",
"--authoring",
"psl",
"--package-manager",
"bun",
"--no-deploy",
"--json",
];
const projectDir = path.join(rootDir, "retry app");
await scaffoldCreateTemplate({
projectDir,
projectName: "retry-app",
template: "minimal",
provider: "postgres",
authoring: "psl",
packageManager: "bun",
});
const contractPath = path.join(projectDir, "src/prisma/contract.prisma");
await writeFile(contractPath, `${TEST_PSL_CONTRACT}\n// User modification\n`);
await writeFile(path.join(projectDir, "keep.txt"), "Unrelated user file\n");

const refused = await runCreatePrismaJson(rootDir, args);
expect(refused.exitCode).toBe(1);
expect(refused.result).toMatchObject({ ok: false, error: { stage: "collect_context" } });
expect(await readFile(contractPath, "utf8")).toContain("// User modification");

const retried = await runCreatePrismaJson(rootDir, [...args, "--force"]);
expect(retried.result).toMatchObject({ ok: true });
expect(retried.exitCode).toBe(0);
expect(await readFile(contractPath, "utf8")).not.toContain("// User modification");
expect(await readFile(path.join(projectDir, "keep.txt"), "utf8")).toBe(
"Unrelated user file\n",
);
await runCommand(projectDir, ["bun", "run", "build"]);
},
TEST_TIMEOUT,
);

test("returns a non-zero exit code when project setup fails", async () => {
const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-exit-code-e2e-"));
tempRoots.push(rootDir);
Expand Down
Loading
Loading