From 3898f2e04abeaaf535e95e4d83eeb269de5f08ca Mon Sep 17 00:00:00 2001 From: willbot Date: Thu, 10 Sep 2026 16:15:56 +0200 Subject: [PATCH] feat: let users opt out of agent skill files at scaffold time Every new project got skill files for Claude Code, Cursor, Codex-style `.agents`, and Devin, a `postinstall` hook that re-syncs them on every install, and a `skills:sync` script, with no way to decline. Interactive runs now ask "Install agent skills for coding assistants?" (default yes) after the package manager prompt. Non-interactive runs take `--skills |none`, matching `prisma init --skills`. With no agents chosen, `prisma.config.ts` records `skills: { agents: [] }`, `prisma init` is not run (so no `postinstall` hook and no skill directories), the `skills:sync` script is omitted, and the Nuxt template's postinstall is `nuxt prepare` alone. With a list of agents, the config names them and `prisma init --skills=` installs only those. `prisma-next.md` stays: it is the human quick reference `prisma orm init` writes, not an agent file. Co-Authored-By: Claude Fable 5.1 Signed-off-by: willbot Signed-off-by: Will Madden --- README.md | 1 + src/commands/create.ts | 1 + src/create-outcome.ts | 1 + src/index.ts | 11 ++ src/tasks/install.ts | 10 +- src/tasks/prisma-setup/commands.ts | 8 +- src/tasks/prisma-setup/context.ts | 43 +++++++ src/tasks/prisma-setup/types.ts | 10 +- src/tasks/setup-prisma.ts | 17 ++- src/telemetry/create.ts | 1 + src/templates/render-create-template.ts | 13 ++- src/types.ts | 45 ++++++++ templates/create/_shared/prisma.config.ts.hbs | 2 +- templates/create/nuxt/package.json.hbs | 2 +- tests/e2e/create-prisma.e2e.test.ts | 43 +++++++ tests/install.test.ts | 42 +++++++ tests/setup-prisma.test.ts | 105 +++++++++++++++++- tests/telemetry.test.ts | 1 + 18 files changed, 340 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a824399..b933331 100644 --- a/README.md +++ b/README.md @@ -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 ` +- `--skills |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` diff --git a/src/commands/create.ts b/src/commands/create.ts index 7076c9a..923138e 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -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({ diff --git a/src/create-outcome.ts b/src/create-outcome.ts index 7f7781a..1c744dd 100644 --- a/src/create-outcome.ts +++ b/src/create-outcome.ts @@ -56,6 +56,7 @@ export const CreateCancellationStageSchema = Schema.Literals([ "database_provider", "authoring_style", "package_manager", + "agent_skills", "deployment_intent", "select_workspace", ]); diff --git a/src/index.ts b/src/index.ts index 5529332..25f5564 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { runCreateCommandEffect } from "./commands/create"; import { createCommandFailureResult } from "./result"; import { applicationRuntime } from "./runtime"; import { + agentSkillTargets, authoringStyles, createTemplates, databaseProviderInputs, @@ -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", @@ -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 } : {}), @@ -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", + }, ]), ); @@ -155,6 +165,7 @@ export type { } from "./result"; export { CREATE_PRISMA_RESULT_SCHEMA_VERSION, CreateCommandResultSchema } from "./result"; export { + AgentSkillTargetSchema, AuthoringStyleSchema, CreateCommandInputSchema, CreateTemplateSchema, diff --git a/src/tasks/install.ts b/src/tasks/install.ts index 57f63c7..9c87981 100644 --- a/src/tasks/install.ts +++ b/src/tasks/install.ts @@ -21,7 +21,10 @@ type PackageJson = { [key: string]: unknown; }; -function getPrismaScriptMap(packageManager: PackageManager): Record { +function getPrismaScriptMap( + packageManager: PackageManager, + skillsSync: boolean, +): Record { if (packageManager === "deno") { const prismaCommand = (needsDatabase: boolean, ...args: string[]) => [ @@ -53,7 +56,7 @@ function getPrismaScriptMap(packageManager: PackageManager): Record + 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 }), @@ -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)); @@ -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 @@ -172,6 +214,7 @@ export const collectPrismaSetupContextEffect = Effect.fn("PrismaSetup.collectCon databaseProvider, authoring, packageManager, + skillAgents, shouldDeploy, shouldPromptForWorkspace: !useDefaults, ...(input.workspace ? { workspace: input.workspace } : {}), diff --git a/src/tasks/prisma-setup/types.ts b/src/tasks/prisma-setup/types.ts index 0f9b502..30c4d44 100644 --- a/src/tasks/prisma-setup/types.ts +++ b/src/tasks/prisma-setup/types.ts @@ -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 = { @@ -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; diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index 1b7788e..c368828 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -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", @@ -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", @@ -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); @@ -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( diff --git a/src/telemetry/create.ts b/src/telemetry/create.ts index 13d8c75..d6a186f 100644 --- a/src/telemetry/create.ts +++ b/src/telemetry/create.ts @@ -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, }; diff --git a/src/templates/render-create-template.ts b/src/templates/render-create-template.ts index e79491c..eb23e93 100644 --- a/src/templates/render-create-template.ts +++ b/src/templates/render-create-template.ts @@ -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 = { @@ -10,6 +17,7 @@ type CreateTemplateContext = { provider: DatabaseProvider; authoring: AuthoringStyle; packageManager?: PackageManager; + skillAgents: readonly AgentSkillTarget[]; tsdownEntry: string | null; }; @@ -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> = { @@ -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, }; } diff --git a/src/types.ts b/src/types.ts index e53bd2f..74b805d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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", @@ -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; @@ -48,6 +53,7 @@ export const PrismaSetupOptionsSchema = Schema.Struct({ packageManager: Schema.optionalKey(PackageManagerSchema), deploy: OptionalBoolean, workspace: OptionalNonEmptyTrimmedString, + skills: OptionalNonEmptyTrimmedString, }); export const PrismaSetupCommandInputSchema = Schema.Struct({ @@ -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)); } diff --git a/templates/create/_shared/prisma.config.ts.hbs b/templates/create/_shared/prisma.config.ts.hbs index 847929e..7f51ab7 100644 --- a/templates/create/_shared/prisma.config.ts.hbs +++ b/templates/create/_shared/prisma.config.ts.hbs @@ -20,7 +20,7 @@ import { defineConfig as ormConfig } from "@prisma/orm-{{#if (eq provider "postg export default definePrismaConfig({ skills: { - agents: ["claude", "cursor", "agents", "devin"], + agents: [{{#each skillAgents}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}], }, orm: ormConfig({ contract: "./src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}", diff --git a/templates/create/nuxt/package.json.hbs b/templates/create/nuxt/package.json.hbs index 9a0c434..9e6080d 100644 --- a/templates/create/nuxt/package.json.hbs +++ b/templates/create/nuxt/package.json.hbs @@ -11,7 +11,7 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare && {{runScriptCommand packageManager "skills:sync"}}", + "postinstall": "nuxt prepare{{#if skillAgents.length}} && {{runScriptCommand packageManager "skills:sync"}}{{/if}}", "typecheck": "nuxt typecheck" }, "dependencies": { diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index 5c0bceb..159d2b7 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -524,6 +524,49 @@ describe("create-prisma e2e", () => { TEST_TIMEOUT, ); + test( + "creates a project without agent skill files when --skills none is passed", + async () => { + const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-no-skills-e2e-")); + tempRoots.push(rootDir); + const { result, exitCode } = await runCreatePrismaJson(rootDir, [ + "no-skills-app", + "--template", + "minimal", + "--provider", + "postgres", + "--authoring", + "psl", + "--package-manager", + "bun", + "--no-deploy", + "--yes", + "--skills", + "none", + "--json", + ]); + + expect(exitCode).toBe(0); + expect(result.ok).toBe(true); + const projectDir = path.join(rootDir, "no-skills-app"); + const packageJson = JSON.parse( + await readFile(path.join(projectDir, "package.json"), "utf8"), + ) as Record; + const configSource = await readFile(path.join(projectDir, "prisma.config.ts"), "utf8"); + + for (const agentDir of [".claude", ".cursor", ".agents", ".devin"]) { + expect(await pathExists(path.join(projectDir, agentDir))).toBe(false); + } + expect(packageJson.scripts.postinstall).toBeUndefined(); + expect(packageJson.scripts["skills:sync"]).toBeUndefined(); + expect(configSource).toContain("agents: [],"); + // prisma-next.md is the human quick reference `prisma orm init` writes; it is not an agent file. + expect(await pathExists(path.join(projectDir, "prisma-next.md"))).toBe(true); + expect(await pathExists(path.join(projectDir, "src/prisma/contract.json"))).toBe(true); + }, + TEST_TIMEOUT, + ); + test( "builds a Next.js app with a TypeScript-authored contract", async () => { diff --git a/tests/install.test.ts b/tests/install.test.ts index 3c3ebbc..1041ee9 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -72,6 +72,16 @@ describe("writePrismaDependencies", () => { }); }); + test("omits the skills:sync script when no agent skills are wanted", async () => { + await withPackageJson(async (projectDir) => { + await writePrismaDependencies("postgres", "pnpm", "psl", projectDir, { skillsSync: false }); + const packageJson = await readPackageJson(projectDir); + + expect(packageJson.scripts?.["skills:sync"]).toBeUndefined(); + expect(packageJson.scripts?.["contract:emit"]).toBe("prisma contract emit"); + }); + }); + test("adds the MongoDB runtime and direct peer dependencies", async () => { await withPackageJson(async (projectDir) => { await writePrismaDependencies("mongo", "bun", "typescript", projectDir); @@ -149,6 +159,38 @@ describe("Composer package-manager commands", () => { }); describe("generated templates", () => { + test("records the agent skills choice in prisma.config.ts and the Nuxt postinstall", async () => { + for (const skillAgents of [[], ["claude", "cursor"]] as const) { + for (const template of ["minimal", "nuxt"] as const) { + const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-skills-")); + try { + await scaffoldCreateTemplate({ + projectDir, + projectName: "skills-app", + template, + provider: "postgres", + authoring: "psl", + packageManager: "npm", + skillAgents, + }); + const prismaConfig = await readFile(path.join(projectDir, "prisma.config.ts"), "utf8"); + const packageJson = await readPackageJson(projectDir); + + expect(prismaConfig).toContain( + `agents: [${skillAgents.map((agent) => `"${agent}"`).join(", ")}],`, + ); + if (template === "nuxt") { + expect(packageJson.scripts?.postinstall).toBe( + skillAgents.length === 0 ? "nuxt prepare" : "nuxt prepare && npm run skills:sync", + ); + } + } finally { + await rm(projectDir, { recursive: true, force: true }); + } + } + } + }); + test("renders Composer into every supported combination", async () => { for (const template of createTemplates) { for (const provider of databaseProviders) { diff --git a/tests/setup-prisma.test.ts b/tests/setup-prisma.test.ts index 25f1ac8..0c2bf2a 100644 --- a/tests/setup-prisma.test.ts +++ b/tests/setup-prisma.test.ts @@ -131,6 +131,52 @@ describe("Prisma setup commands", () => { }); }); + test("runs no prisma init when no agent skills are wanted", async () => { + await withTempProject(async (projectDir) => { + const context = await collectPrismaSetupContext( + { json: true, deploy: false, packageManager: "npm", skills: "none" }, + { projectDir }, + ); + await applicationRuntime.runPromise( + initializeAgentSkills(context, projectDir).pipe( + Effect.provideService(CommandRunner, { + run: () => Effect.die("prisma init must not run when no agents are wanted"), + runChecked: () => Effect.die("prisma init must not run when no agents are wanted"), + }), + ), + ); + }); + }); + + test("passes the chosen agents to prisma init", async () => { + await withTempProject(async (projectDir) => { + const context = await collectPrismaSetupContext( + { json: true, deploy: false, packageManager: "npm", skills: "claude,cursor" }, + { projectDir }, + ); + const commands: CommandSpec[] = []; + await applicationRuntime.runPromise( + initializeAgentSkills(context, projectDir).pipe( + Effect.provideService(CommandRunner, { + run: (spec: CommandSpec) => { + commands.push(spec); + return Effect.succeed({ + exitCode: 0, + stdout: '{"kind":"result","envelope":{"ok":true,"result":{}}}', + stderr: "", + }); + }, + runChecked: () => Effect.die("Expected structured Prisma execution"), + }), + ), + ); + expect(commands).toHaveLength(1); + expect(commands[0]?.args).toEqual( + expect.arrayContaining(["init", "--yes", "--skills=claude,cursor"]), + ); + }); + }); + test("grants overwrite consent only with explicit force", async () => { await withTempProject(async (root) => { const projectDir = path.join(root, "retry app"); @@ -162,7 +208,13 @@ describe("Prisma setup commands", () => { expect(commands[1]?.args[commands[1].args.indexOf("--confirm") + 1]).toBe("retry app"); expect(commands[0]?.args).toContain("--json"); expect(commands[0]?.env?.CI).toBe("1"); - expect(commands[2]?.args.slice(-4)).toEqual(["init", "--yes", "--json", "--no-interactive"]); + expect(commands[2]?.args.slice(-5)).toEqual([ + "init", + "--yes", + "--skills=claude,cursor,agents,devin", + "--json", + "--no-interactive", + ]); expect(commands[2]?.args.filter((arg) => arg === "--no-interactive")).toHaveLength(1); }); }); @@ -219,6 +271,7 @@ describe("collectPrismaSetupContext", () => { databaseProvider: "postgres", authoring: "psl", packageManager: "bun", + skillAgents: ["claude", "cursor", "agents", "devin"], shouldDeploy: false, shouldPromptForWorkspace: false, }); @@ -290,6 +343,7 @@ describe("collectPrismaSetupContext", () => { provider: "postgres", authoring: "psl", packageManager: "bun", + skills: "claude", deploy: true, }, { projectDir }, @@ -298,6 +352,54 @@ describe("collectPrismaSetupContext", () => { }); }); + test("--skills none records that no agent skills are wanted", async () => { + await withTempProject(async (projectDir) => { + const context = await collectPrismaSetupContext( + { yes: true, packageManager: "npm", skills: "none" }, + { projectDir }, + ); + expect(context.skillAgents).toEqual([]); + }); + }); + + test("--skills keeps the listed agents in order without duplicates", async () => { + await withTempProject(async (projectDir) => { + const context = await collectPrismaSetupContext( + { yes: true, packageManager: "npm", skills: "cursor, claude,cursor" }, + { projectDir }, + ); + expect(context.skillAgents).toEqual(["cursor", "claude"]); + }); + }); + + test.each([ + { skills: "zed", fragment: "'zed'" }, + { skills: "none,claude", fragment: "cannot be combined" }, + { skills: ",", fragment: "no agent names" }, + ])("rejects --skills $skills before touching the target", async ({ skills, fragment }) => { + await withTempProject(async (projectDir) => { + const result = await applicationRuntime.runPromise( + runCreateCommandEffect({ + name: path.relative(process.cwd(), path.join(projectDir, "bad-skills-app")), + template: "minimal", + packageManager: "npm", + json: true, + deploy: false, + skills, + }).pipe( + Effect.provideService(CommandRunner, { + run: () => Effect.die("No command may run with an invalid --skills value"), + runChecked: () => Effect.die("No command may run with an invalid --skills value"), + }), + ), + ); + expect(result).toMatchObject({ + ok: false, + error: { stage: "collect_context", message: expect.stringContaining(fragment) }, + }); + }); + }); + test("keeps MongoDB as an explicit provider option", async () => { await withTempProject(async (projectDir) => { const context = await collectPrismaSetupContext( @@ -318,6 +420,7 @@ describe("collectPrismaSetupContext", () => { expect(context).toMatchObject({ databaseProvider: "postgres", packageManager: "deno", + skillAgents: [], shouldDeploy: false, }); }); diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index ea2ea9a..47508ba 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -33,6 +33,7 @@ const createContext: CreatePromptContext = { databaseProvider: "postgres", authoring: "psl", packageManager: "bun", + skillAgents: ["claude", "cursor", "agents", "devin"], shouldDeploy: true, shouldPromptForWorkspace: false, },