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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ older cached version. Prisma Compute does not support Deno deployments yet.
- `--package-manager npm|pnpm|yarn|bun|deno`
- `--deploy` / `--no-deploy`
- `--workspace <id-or-name>`
- `--skills <agents>|none`: agents to install skill files for, comma-separated from `claude`, `cursor`, `agents`, `devin` (default: all). `--skills none` writes no `.claude/`, `.cursor/`, `.agents/`, or `.devin/` directories, no `postinstall` hook, and no `skills:sync` script, and records `skills: { agents: [] }` in `prisma.config.ts`. Interactive runs ask instead.
- `--yes`
- `--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. A non-empty standard `migrations` path is protected: use a new directory for a fresh starter, or continue working in the existing project with the Prisma CLI. Custom migration paths are not detected.
- `--verbose`
Expand Down
1 change: 1 addition & 0 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const executeCreateContext = Effect.fn("Create.execute")(function* (context: Cre
provider: context.prismaSetupContext.databaseProvider,
authoring: context.prismaSetupContext.authoring,
packageManager: context.prismaSetupContext.packageManager,
skillAgents: context.prismaSetupContext.skillAgents,
}).pipe(
Effect.andThen(
writeCreateTemplateDependenciesEffect({
Expand Down
1 change: 1 addition & 0 deletions src/create-outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const CreateCancellationStageSchema = Schema.Literals([
"database_provider",
"authoring_style",
"package_manager",
"agent_skills",
"deployment_intent",
"select_workspace",
]);
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { runCreateCommandEffect } from "./commands/create";
import { createCommandFailureResult } from "./result";
import { applicationRuntime } from "./runtime";
import {
agentSkillTargets,
authoringStyles,
createTemplates,
databaseProviderInputs,
Expand Down Expand Up @@ -64,6 +65,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"),
skills: optionalString(
"skills",
`Agents to install skills for, comma-separated (${agentSkillTargets.join(", ")}); 'none' installs no agent skill files`,
),
force: optionalBoolean(
"force",
"Overwrite generated starter and Prisma files; refuses a non-empty migrations path",
Expand All @@ -86,6 +91,7 @@ export const createPrismaCommand = Command.make(
...(options.packageManager ? { packageManager: options.packageManager } : {}),
...(options.deploy !== undefined ? { deploy: options.deploy } : {}),
...(options.workspace ? { workspace: options.workspace } : {}),
...(options.skills ? { skills: options.skills } : {}),
...(options.force !== undefined ? { force: options.force } : {}),
...(options.yes !== undefined ? { yes: options.yes } : {}),
...(options.verbose !== undefined ? { verbose: options.verbose } : {}),
Expand All @@ -103,6 +109,10 @@ export const createPrismaCommand = Command.make(
command: "create-prisma my-app --yes --json",
description: "Create and deploy with machine-readable output",
},
{
command: "create-prisma my-app --yes --skills none",
description: "Create without agent skill files or the skills-sync postinstall hook",
},
]),
);

Expand Down Expand Up @@ -155,6 +165,7 @@ export type {
} from "./result";
export { CREATE_PRISMA_RESULT_SCHEMA_VERSION, CreateCommandResultSchema } from "./result";
export {
AgentSkillTargetSchema,
AuthoringStyleSchema,
CreateCommandInputSchema,
CreateTemplateSchema,
Expand Down
10 changes: 7 additions & 3 deletions src/tasks/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ type PackageJson = {
[key: string]: unknown;
};

function getPrismaScriptMap(packageManager: PackageManager): Record<string, string> {
function getPrismaScriptMap(
packageManager: PackageManager,
skillsSync: boolean,
): Record<string, string> {
if (packageManager === "deno") {
const prismaCommand = (needsDatabase: boolean, ...args: string[]) =>
[
Expand Down Expand Up @@ -53,7 +56,7 @@ function getPrismaScriptMap(packageManager: PackageManager): Record<string, stri
migrate: prismaCommand("db", "migrate"),
"migration:status": prismaCommand("migration", "status"),
"migration:show": prismaCommand("migration", "show"),
"skills:sync": `${prismaCommand("skills", "sync")} || exit 0`,
...(skillsSync ? { "skills:sync": `${prismaCommand("skills", "sync")} || exit 0` } : {}),
};
}

Expand Down Expand Up @@ -158,6 +161,7 @@ export const writePrismaDependenciesEffect = Effect.fn("Dependencies.writePrisma
packageManager: PackageManager,
_authoring: AuthoringStyle,
projectDir = process.cwd(),
options: { skillsSync?: boolean } = {},
) {
const dependencies = [getDbPackages(provider)];
if (provider === "postgres" && packageManager !== "deno") dependencies.push("temporal-polyfill");
Expand All @@ -166,7 +170,7 @@ export const writePrismaDependenciesEffect = Effect.fn("Dependencies.writePrisma
yield* addPackageDependencyEffect({
dependencies,
devDependencies: ["@types/node", "prisma"],
scripts: getPrismaScriptMap(packageManager),
scripts: getPrismaScriptMap(packageManager, options.skillsSync ?? true),
projectDir,
});
});
Expand Down
8 changes: 6 additions & 2 deletions src/tasks/prisma-setup/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export const initializeAgentSkills = Effect.fn("PrismaSetup.initializeSkills")(f
context: PrismaSetupContext,
projectDir: string,
) {
if (context.packageManager === "deno") return;
yield* runPrismaCli(context, projectDir, ["init", "--yes"]);
if (context.skillAgents.length === 0) return;
yield* runPrismaCli(context, projectDir, [
"init",
"--yes",
`--skills=${context.skillAgents.join(",")}`,
]);
});
43 changes: 43 additions & 0 deletions src/tasks/prisma-setup/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import type { Writable } from "node:stream";

import { CreateCancellationError, CreateFailure } from "../../create-outcome";
import {
agentSkillTargets,
AuthoringStyleSchema,
DatabaseProviderSchema,
PackageManagerSchema,
packageManagers,
parseAgentSkillSelection,
type AgentSkillTarget,
type AuthoringStyle,
type CreateTemplate,
type DatabaseProvider,
Expand Down Expand Up @@ -109,6 +112,36 @@ const promptForPackageManager = Effect.fn("Prompts.packageManager")(function* (
return yield* decodePromptValue(PackageManagerSchema, value);
});

const promptForAgentSkills = Effect.fn("Prompts.agentSkills")(function* (output: Writable) {
const value = yield* Effect.tryPromise(() =>
confirm({
message: "Install agent skills for coding assistants (Claude Code, Cursor, Codex, Devin)?",
initialValue: true,
output,
}),
);
if (isCancel(value)) {
yield* Effect.sync(() => cancel("Operation cancelled.", { output }));
return yield* new CreateCancellationError({ stage: "agent_skills" });
}
return value ? agentSkillTargets : [];
});

const resolveRequestedAgentSkills = Effect.fn("PrismaSetup.resolveSkills")(function* (
value: string,
output: Writable,
) {
const selection = parseAgentSkillSelection(value);
if (selection.ok) return selection.agents;
yield* Effect.sync(() => cancel(selection.message, { output }));
return yield* new CreateFailure({
stage: "collect_context",
reason: "invalid_input",
message: selection.message,
errorReported: true,
});
});

const promptForDeployment = Effect.fn("Prompts.deployment")(function* (output: Writable) {
const value = yield* Effect.tryPromise(() =>
confirm({ message: "Deploy to Prisma now?", initialValue: true, output }),
Expand All @@ -126,6 +159,10 @@ export const collectPrismaSetupContextEffect = Effect.fn("PrismaSetup.collectCon
) {
const projectDir = path.resolve(options.projectDir ?? process.cwd());
const { json, output, useDefaults } = resolveExecutionSettings(input);
const requestedSkillAgents =
input.skills === undefined
? undefined
: yield* resolveRequestedAgentSkills(input.skills, output);
const databaseProvider =
input.provider ??
(useDefaults ? DEFAULT_DATABASE_PROVIDER : yield* promptForDatabaseProvider(output));
Expand Down Expand Up @@ -160,6 +197,11 @@ export const collectPrismaSetupContextEffect = Effect.fn("PrismaSetup.collectCon
});
}

const skillAgents: readonly AgentSkillTarget[] =
packageManager === "deno"
? []
: (requestedSkillAgents ??
(useDefaults ? agentSkillTargets : yield* promptForAgentSkills(output)));
const shouldDeploy =
packageManager === "deno"
? false
Expand All @@ -172,6 +214,7 @@ export const collectPrismaSetupContextEffect = Effect.fn("PrismaSetup.collectCon
databaseProvider,
authoring,
packageManager,
skillAgents,
shouldDeploy,
shouldPromptForWorkspace: !useDefaults,
...(input.workspace ? { workspace: input.workspace } : {}),
Expand Down
10 changes: 9 additions & 1 deletion src/tasks/prisma-setup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import type { spinner } from "@clack/prompts";
import type { Writable } from "node:stream";

import type { ComposerDeployResult, CreateNextStep } from "../../result";
import type { AuthoringStyle, CreateTemplate, DatabaseProvider, PackageManager } from "../../types";
import type {
AgentSkillTarget,
AuthoringStyle,
CreateTemplate,
DatabaseProvider,
PackageManager,
} from "../../types";
import type { GitInitializationResult } from "../initialize-git";

export type PrismaSetupRunOptions = {
Expand All @@ -25,6 +31,8 @@ export type PrismaSetupContext = {
databaseProvider: DatabaseProvider;
authoring: AuthoringStyle;
packageManager: PackageManager;
/** Agents whose skill files the project gets; empty means none, no postinstall hook, no sync script. */
skillAgents: readonly AgentSkillTarget[];
shouldDeploy: boolean;
shouldPromptForWorkspace: boolean;
workspace?: string;
Expand Down
17 changes: 11 additions & 6 deletions src/tasks/setup-prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(
provider: context.databaseProvider,
authoring: context.authoring,
packageManager: context.packageManager,
skillAgents: context.skillAgents,
}),
"configure_project",
"project_configuration_failed",
Expand All @@ -62,6 +63,7 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(
context.packageManager,
context.authoring,
projectDir,
{ skillsSync: context.skillAgents.length > 0 },
),
"configure_project",
"project_configuration_failed",
Expand Down Expand Up @@ -97,6 +99,7 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(
provider: context.databaseProvider,
authoring: context.authoring,
packageManager: context.packageManager,
skillAgents: context.skillAgents,
});
yield* ensureComposerTypeScriptOptions(projectDir);
if (context.databaseProvider === "mongo") yield* ensureMongoEnvironment(projectDir);
Expand All @@ -109,12 +112,14 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(
"project_configuration_failed",
);

yield* Effect.sync(() => progress?.message("Installing Prisma agent skills..."));
yield* atCreateStage(
initializeAgentSkills(context, projectDir),
"initialize_agent_skills",
"agent_skills_init_failed",
);
if (context.skillAgents.length > 0) {
yield* Effect.sync(() => progress?.message("Installing Prisma agent skills..."));
yield* atCreateStage(
initializeAgentSkills(context, projectDir),
"initialize_agent_skills",
"agent_skills_init_failed",
);
}

yield* Effect.sync(() => progress?.message("Generating Prisma 8 contract artifacts..."));
yield* atCreateStage(
Expand Down
1 change: 1 addition & 0 deletions src/telemetry/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function getBaseCreateProperties(
"database-provider": context?.prismaSetupContext.databaseProvider ?? input.provider ?? null,
"authoring-style": context?.prismaSetupContext.authoring ?? input.authoring ?? null,
"package-manager": context?.prismaSetupContext.packageManager ?? input.packageManager ?? null,
"agent-skills": context ? [...context.prismaSetupContext.skillAgents] : (input.skills ?? null),
"should-deploy": context?.prismaSetupContext.shouldDeploy ?? input.deploy ?? null,
"target-directory-state": context ? getTargetDirectoryState(context) : null,
};
Expand Down
13 changes: 12 additions & 1 deletion src/templates/render-create-template.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Effect } from "effect";

import { applicationRuntime } from "../runtime";
import type { AuthoringStyle, CreateTemplate, DatabaseProvider, PackageManager } from "../types";
import {
agentSkillTargets,
type AgentSkillTarget,
type AuthoringStyle,
type CreateTemplate,
type DatabaseProvider,
type PackageManager,
} from "../types";
import { renderTemplateTreeEffect, resolveTemplatesDirEffect } from "./shared";

type CreateTemplateContext = {
Expand All @@ -10,6 +17,7 @@ type CreateTemplateContext = {
provider: DatabaseProvider;
authoring: AuthoringStyle;
packageManager?: PackageManager;
skillAgents: readonly AgentSkillTarget[];
tsdownEntry: string | null;
};

Expand All @@ -20,6 +28,8 @@ export type ScaffoldCreateTemplateOptions = {
provider: DatabaseProvider;
authoring: AuthoringStyle;
packageManager?: PackageManager;
/** Agents whose skill files the project gets; defaults to all of them. */
skillAgents?: readonly AgentSkillTarget[];
};

const tsdownEntries: Partial<Record<CreateTemplate, string>> = {
Expand All @@ -36,6 +46,7 @@ function createTemplateContext(options: ScaffoldCreateTemplateOptions): CreateTe
provider: options.provider,
authoring: options.authoring,
packageManager: options.packageManager,
skillAgents: options.skillAgents ?? agentSkillTargets,
tsdownEntry: tsdownEntries[options.template] ?? null,
};
}
Expand Down
45 changes: 45 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const databaseProviders = ["postgres", "mongo"] as const;
export const databaseProviderInputs = ["postgres", "postgresql", "mongo", "mongodb"] as const;
export const packageManagers = ["npm", "pnpm", "yarn", "bun", "deno"] as const;
export const authoringStyles = ["psl", "typescript"] as const;
export const agentSkillTargets = ["claude", "cursor", "agents", "devin"] as const;
export const SKILLS_NONE = "none";
export const createTemplates = [
"minimal",
"hono",
Expand All @@ -27,6 +29,9 @@ export type PackageManager = typeof PackageManagerSchema.Type;
export const AuthoringStyleSchema = Schema.Literals(authoringStyles);
export type AuthoringStyle = typeof AuthoringStyleSchema.Type;

export const AgentSkillTargetSchema = Schema.Literals(agentSkillTargets);
export type AgentSkillTarget = typeof AgentSkillTargetSchema.Type;

export const CreateTemplateSchema = Schema.Literals(createTemplates);
export type CreateTemplate = typeof CreateTemplateSchema.Type;

Expand All @@ -48,6 +53,7 @@ export const PrismaSetupOptionsSchema = Schema.Struct({
packageManager: Schema.optionalKey(PackageManagerSchema),
deploy: OptionalBoolean,
workspace: OptionalNonEmptyTrimmedString,
skills: OptionalNonEmptyTrimmedString,
});

export const PrismaSetupCommandInputSchema = Schema.Struct({
Expand Down Expand Up @@ -81,6 +87,45 @@ export function normalizeDatabaseProvider(value: DatabaseProviderInput): Databas
}
}

export function isAgentSkillTarget(name: string): name is AgentSkillTarget {
return (agentSkillTargets as readonly string[]).includes(name);
}

export type AgentSkillSelection =
| { ok: true; agents: readonly AgentSkillTarget[] }
| { ok: false; message: string };

/** Parses `--skills`: a comma-separated list of agent names, or `none`. */
export function parseAgentSkillSelection(value: string): AgentSkillSelection {
const names = value
.split(",")
.map((name) => name.trim())
.filter((name) => name.length > 0);
const known = agentSkillTargets.join(", ");
if (names.includes(SKILLS_NONE)) {
return names.length === 1
? { ok: true, agents: [] }
: { ok: false, message: `--skills ${SKILLS_NONE} cannot be combined with agent names.` };
}
const agents: AgentSkillTarget[] = [];
for (const name of names) {
if (!isAgentSkillTarget(name)) {
return {
ok: false,
message: `--skills names '${name}', which is not a known agent. Use a comma-separated list of ${known}, or ${SKILLS_NONE}.`,
};
}
if (!agents.includes(name)) agents.push(name);
}
if (agents.length === 0) {
return {
ok: false,
message: `--skills was given no agent names. Use a comma-separated list of ${known}, or ${SKILLS_NONE}.`,
};
}
return { ok: true, agents };
}

export function decodeCreateCommandInputSync(input: unknown): CreateCommandInput {
return Effect.runSync(decodeCreateCommandInput(input));
}
Loading
Loading