create-prisma 0.1.4 → 0.2.0
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.
- package/README.md +55 -16
- package/dist/cli.mjs +1 -1
- package/dist/{create-Dz9GFGFQ.mjs → create-DgN5mAMV.mjs} +1031 -444
- package/dist/index.d.mts +31 -57
- package/dist/index.mjs +10 -21
- package/package.json +2 -2
- package/templates/create/astro/.yarnrc.yml.hbs +3 -0
- package/templates/create/astro/README.md.hbs +35 -0
- package/templates/create/astro/astro.config.mjs +5 -0
- package/templates/create/astro/deno.json.hbs +5 -0
- package/templates/create/astro/package.json.hbs +22 -0
- package/templates/{init → create/astro}/prisma/schema.prisma.hbs +4 -1
- package/templates/create/astro/prisma/seed.ts.hbs +38 -0
- package/templates/{init/prisma.config.ts.hbs → create/astro/prisma.config.ts} +1 -0
- package/templates/create/astro/public/favicon.svg +14 -0
- package/templates/create/astro/src/env.d.ts +9 -0
- package/templates/create/astro/src/lib/prisma.ts.hbs +53 -0
- package/templates/create/astro/src/pages/api/users.ts.hbs +42 -0
- package/templates/create/astro/src/pages/index.astro.hbs +236 -0
- package/templates/create/astro/tsconfig.json +5 -0
- package/templates/create/hono/.yarnrc.yml.hbs +3 -0
- package/templates/create/hono/README.md.hbs +13 -48
- package/templates/create/hono/deno.json.hbs +5 -0
- package/templates/create/hono/package.json.hbs +3 -0
- package/templates/create/hono/prisma/schema.prisma.hbs +21 -0
- package/templates/create/hono/prisma/seed.ts.hbs +38 -0
- package/templates/create/hono/prisma.config.ts +13 -0
- package/templates/create/hono/src/index.ts.hbs +1 -1
- package/templates/{init/prisma/index.ts.hbs → create/hono/src/lib/prisma.ts.hbs} +14 -13
- package/templates/create/hono/tsconfig.json +1 -2
- package/templates/create/next/.yarnrc.yml.hbs +3 -0
- package/templates/create/next/README.md.hbs +8 -23
- package/templates/create/next/deno.json.hbs +12 -0
- package/templates/create/next/package.json.hbs +4 -0
- package/templates/create/next/prisma/schema.prisma.hbs +21 -0
- package/templates/create/next/prisma/seed.ts.hbs +38 -0
- package/templates/create/next/prisma.config.ts +13 -0
- package/templates/create/next/src/app/globals.css +136 -0
- package/templates/create/next/src/app/page.tsx.hbs +104 -0
- package/templates/create/next/src/lib/prisma.ts.hbs +53 -0
- package/templates/create/nuxt/.yarnrc.yml.hbs +3 -0
- package/templates/create/nuxt/README.md.hbs +32 -0
- package/templates/create/nuxt/app/app.vue +4 -0
- package/templates/create/nuxt/app/pages/index.vue.hbs +230 -0
- package/templates/create/nuxt/deno.json.hbs +5 -0
- package/templates/create/nuxt/nuxt.config.ts +5 -0
- package/templates/create/nuxt/package.json.hbs +27 -0
- package/templates/create/nuxt/prisma/schema.prisma.hbs +21 -0
- package/templates/create/nuxt/prisma/seed.ts.hbs +38 -0
- package/templates/create/nuxt/prisma.config.ts +13 -0
- package/templates/create/nuxt/public/robots.txt +2 -0
- package/templates/create/nuxt/server/api/users.get.ts.hbs +36 -0
- package/templates/create/nuxt/server/utils/prisma.ts.hbs +53 -0
- package/templates/create/nuxt/tsconfig.json +17 -0
- package/templates/create/svelte/.vscode/extensions.json +3 -0
- package/templates/create/svelte/.yarnrc.yml.hbs +3 -0
- package/templates/create/svelte/README.md.hbs +34 -0
- package/templates/create/svelte/deno.json.hbs +5 -0
- package/templates/create/svelte/package.json.hbs +28 -0
- package/templates/create/svelte/prisma/schema.prisma.hbs +21 -0
- package/templates/create/svelte/prisma/seed.ts.hbs +87 -0
- package/templates/create/svelte/prisma.config.ts +13 -0
- package/templates/create/svelte/src/app.d.ts +13 -0
- package/templates/create/svelte/src/app.html +11 -0
- package/templates/create/svelte/src/lib/assets/favicon.svg +1 -0
- package/templates/create/svelte/src/lib/index.ts +1 -0
- package/templates/create/svelte/src/lib/server/prisma.ts.hbs +53 -0
- package/templates/create/svelte/src/routes/+layout.svelte +11 -0
- package/templates/create/svelte/src/routes/+page.server.ts.hbs +28 -0
- package/templates/create/svelte/src/routes/+page.svelte.hbs +350 -0
- package/templates/create/svelte/static/robots.txt +3 -0
- package/templates/create/svelte/svelte.config.js +13 -0
- package/templates/create/svelte/tsconfig.json +20 -0
- package/templates/create/svelte/vite.config.ts +6 -0
- package/templates/create/turborepo/.yarnrc.yml.hbs +3 -0
- package/templates/create/turborepo/README.md.hbs +29 -0
- package/templates/create/turborepo/apps/api/package.json.hbs +21 -0
- package/templates/create/turborepo/apps/api/src/index.ts.hbs +45 -0
- package/templates/create/turborepo/apps/api/tsconfig.json +17 -0
- package/templates/create/turborepo/deno.json.hbs +5 -0
- package/templates/create/turborepo/package.json.hbs +24 -0
- package/templates/create/turborepo/packages/db/package.json.hbs +17 -0
- package/templates/create/turborepo/packages/db/prisma/schema.prisma.hbs +21 -0
- package/templates/create/turborepo/packages/db/prisma/seed.ts.hbs +38 -0
- package/templates/create/turborepo/packages/db/prisma.config.ts +13 -0
- package/templates/create/turborepo/packages/db/src/client.ts.hbs +58 -0
- package/templates/create/turborepo/packages/db/src/index.ts +2 -0
- package/templates/create/turborepo/packages/db/tsconfig.json +19 -0
- package/templates/create/turborepo/turbo.json +34 -0
- package/templates/create/next/app/globals.css +0 -47
- package/templates/create/next/app/page.tsx.hbs +0 -46
- /package/templates/create/next/{app → src/app}/layout.tsx.hbs +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
2
|
+
import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
3
3
|
import fs from "fs-extra";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import Handlebars from "handlebars";
|
|
@@ -9,82 +9,17 @@ import { z } from "zod";
|
|
|
9
9
|
import { execa } from "execa";
|
|
10
10
|
import { styleText } from "node:util";
|
|
11
11
|
|
|
12
|
-
//#region src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
function resolveTemplatesDir(relativeTemplatesDir) {
|
|
25
|
-
const currentFilePath = fileURLToPath(import.meta.url);
|
|
26
|
-
const packageRoot = findPackageRoot(path.dirname(currentFilePath));
|
|
27
|
-
const templatePath = path.join(packageRoot, relativeTemplatesDir);
|
|
28
|
-
if (!existsSync(templatePath)) throw new Error(`Template directory not found at: ${templatePath}`);
|
|
29
|
-
return templatePath;
|
|
30
|
-
}
|
|
31
|
-
async function getTemplateFilesRecursively(dir) {
|
|
32
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
33
|
-
return (await Promise.all(entries.map(async (entry) => {
|
|
34
|
-
const entryPath = path.join(dir, entry.name);
|
|
35
|
-
if (entry.isDirectory()) return getTemplateFilesRecursively(entryPath);
|
|
36
|
-
if (!entry.isFile()) return [];
|
|
37
|
-
return [entryPath];
|
|
38
|
-
}))).flat();
|
|
39
|
-
}
|
|
40
|
-
function stripHbsExtension(filePath) {
|
|
41
|
-
return filePath.endsWith(".hbs") ? filePath.slice(0, -4) : filePath;
|
|
42
|
-
}
|
|
43
|
-
function ensureTrailingNewline(content) {
|
|
44
|
-
return content.endsWith("\n") ? content : `${content}\n`;
|
|
45
|
-
}
|
|
46
|
-
async function renderTemplateFile(opts) {
|
|
47
|
-
const { templateFilePath, outputPath, context } = opts;
|
|
48
|
-
const templateContent = await fs.readFile(templateFilePath, "utf8");
|
|
49
|
-
const outputContent = templateFilePath.endsWith(".hbs") ? Handlebars.compile(templateContent, {
|
|
50
|
-
noEscape: true,
|
|
51
|
-
strict: true
|
|
52
|
-
})(context) : templateContent;
|
|
53
|
-
await fs.outputFile(outputPath, ensureTrailingNewline(outputContent), "utf8");
|
|
54
|
-
}
|
|
55
|
-
async function renderTemplateTree(opts) {
|
|
56
|
-
const { templateRoot, outputDir, context } = opts;
|
|
57
|
-
const templateFiles = await getTemplateFilesRecursively(templateRoot);
|
|
58
|
-
for (const templateFilePath of templateFiles) {
|
|
59
|
-
const relativeOutputPath = stripHbsExtension(path.relative(templateRoot, templateFilePath));
|
|
60
|
-
await renderTemplateFile({
|
|
61
|
-
templateFilePath,
|
|
62
|
-
outputPath: path.join(outputDir, relativeOutputPath),
|
|
63
|
-
context
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
//#endregion
|
|
69
|
-
//#region src/templates/render-create-template.ts
|
|
70
|
-
function getCreateTemplateDir(template) {
|
|
71
|
-
return resolveTemplatesDir(`templates/create/${template}`);
|
|
72
|
-
}
|
|
73
|
-
function createTemplateContext$1(projectName, schemaPreset, packageManager) {
|
|
74
|
-
return {
|
|
75
|
-
projectName,
|
|
76
|
-
schemaPreset,
|
|
77
|
-
packageManager
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
async function scaffoldCreateTemplate(opts) {
|
|
81
|
-
const { projectDir, projectName, template, schemaPreset, packageManager } = opts;
|
|
82
|
-
await renderTemplateTree({
|
|
83
|
-
templateRoot: getCreateTemplateDir(template),
|
|
84
|
-
outputDir: projectDir,
|
|
85
|
-
context: createTemplateContext$1(projectName, schemaPreset, packageManager)
|
|
86
|
-
});
|
|
87
|
-
}
|
|
12
|
+
//#region src/constants/dependencies.ts
|
|
13
|
+
const dependencyVersionMap = {
|
|
14
|
+
"@prisma/client": "^7.4.0",
|
|
15
|
+
"@prisma/adapter-pg": "^7.4.0",
|
|
16
|
+
"@prisma/adapter-mariadb": "^7.4.0",
|
|
17
|
+
"@prisma/adapter-better-sqlite3": "^7.4.0",
|
|
18
|
+
"@prisma/adapter-mssql": "^7.4.0",
|
|
19
|
+
dotenv: "^17.2.3",
|
|
20
|
+
"node-gyp": "^11.5.0",
|
|
21
|
+
prisma: "^7.4.0"
|
|
22
|
+
};
|
|
88
23
|
|
|
89
24
|
//#endregion
|
|
90
25
|
//#region src/types.ts
|
|
@@ -98,14 +33,45 @@ const databaseProviders = [
|
|
|
98
33
|
const packageManagers = [
|
|
99
34
|
"npm",
|
|
100
35
|
"pnpm",
|
|
101
|
-
"
|
|
36
|
+
"yarn",
|
|
37
|
+
"bun",
|
|
38
|
+
"deno"
|
|
102
39
|
];
|
|
103
40
|
const schemaPresets = ["empty", "basic"];
|
|
104
|
-
const createTemplates = [
|
|
41
|
+
const createTemplates = [
|
|
42
|
+
"hono",
|
|
43
|
+
"next",
|
|
44
|
+
"svelte",
|
|
45
|
+
"astro",
|
|
46
|
+
"nuxt",
|
|
47
|
+
"turborepo"
|
|
48
|
+
];
|
|
49
|
+
const createAddons = [
|
|
50
|
+
"skills",
|
|
51
|
+
"mcp",
|
|
52
|
+
"extension"
|
|
53
|
+
];
|
|
54
|
+
const addonInstallScopes = ["project", "global"];
|
|
55
|
+
const extensionTargets = [
|
|
56
|
+
"vscode",
|
|
57
|
+
"cursor",
|
|
58
|
+
"windsurf"
|
|
59
|
+
];
|
|
60
|
+
const prismaSkillNames = [
|
|
61
|
+
"prisma-cli",
|
|
62
|
+
"prisma-client-api",
|
|
63
|
+
"prisma-database-setup",
|
|
64
|
+
"prisma-upgrade-v7",
|
|
65
|
+
"prisma-postgres"
|
|
66
|
+
];
|
|
105
67
|
const DatabaseProviderSchema = z.enum(databaseProviders);
|
|
106
68
|
const PackageManagerSchema = z.enum(packageManagers);
|
|
107
69
|
const SchemaPresetSchema = z.enum(schemaPresets);
|
|
108
70
|
const CreateTemplateSchema = z.enum(createTemplates);
|
|
71
|
+
const CreateAddonSchema = z.enum(createAddons);
|
|
72
|
+
const AddonInstallScopeSchema = z.enum(addonInstallScopes);
|
|
73
|
+
const ExtensionTargetSchema = z.enum(extensionTargets);
|
|
74
|
+
const PrismaSkillNameSchema = z.enum(prismaSkillNames);
|
|
109
75
|
const DatabaseUrlSchema = z.string().trim().min(1, "Please enter a valid database URL");
|
|
110
76
|
const CommonCommandOptionsSchema = z.object({
|
|
111
77
|
yes: z.boolean().optional().describe("Skip prompts and accept default choices"),
|
|
@@ -120,19 +86,30 @@ const PrismaSetupOptionsSchema = z.object({
|
|
|
120
86
|
generate: z.boolean().optional().describe("Generate Prisma Client after scaffolding"),
|
|
121
87
|
schemaPreset: SchemaPresetSchema.optional().describe("Schema preset to scaffold in prisma/schema.prisma")
|
|
122
88
|
});
|
|
123
|
-
const
|
|
89
|
+
const PrismaSetupCommandInputSchema = CommonCommandOptionsSchema.extend(PrismaSetupOptionsSchema.shape);
|
|
124
90
|
const CreateScaffoldOptionsSchema = z.object({
|
|
125
91
|
name: z.string().trim().min(1, "Please enter a valid project name").optional().describe("Project name / directory"),
|
|
126
92
|
template: CreateTemplateSchema.optional().describe("Project template"),
|
|
93
|
+
skills: z.boolean().optional().describe("Enable skills addon"),
|
|
94
|
+
mcp: z.boolean().optional().describe("Enable MCP addon"),
|
|
95
|
+
extension: z.boolean().optional().describe("Enable extension addon"),
|
|
127
96
|
force: z.boolean().optional().describe("Allow scaffolding into a non-empty target directory")
|
|
128
97
|
});
|
|
129
|
-
const CreateCommandInputSchema =
|
|
98
|
+
const CreateCommandInputSchema = PrismaSetupCommandInputSchema.extend(CreateScaffoldOptionsSchema.shape);
|
|
130
99
|
|
|
131
100
|
//#endregion
|
|
132
101
|
//#region src/utils/package-manager.ts
|
|
102
|
+
const packageManagerManifestValues = {
|
|
103
|
+
npm: "npm@10.9.0",
|
|
104
|
+
pnpm: "pnpm@10.16.1",
|
|
105
|
+
yarn: "yarn@4.13.0",
|
|
106
|
+
bun: "bun@1.3.9"
|
|
107
|
+
};
|
|
133
108
|
function parseUserAgent(userAgent) {
|
|
134
109
|
if (userAgent?.startsWith("pnpm")) return "pnpm";
|
|
110
|
+
if (userAgent?.startsWith("yarn")) return "yarn";
|
|
135
111
|
if (userAgent?.startsWith("bun")) return "bun";
|
|
112
|
+
if (userAgent?.startsWith("deno")) return "deno";
|
|
136
113
|
if (userAgent?.startsWith("npm")) return "npm";
|
|
137
114
|
return null;
|
|
138
115
|
}
|
|
@@ -147,12 +124,20 @@ async function detectFromPackageJson(projectDir) {
|
|
|
147
124
|
if (!await fs.pathExists(packageJsonPath)) return null;
|
|
148
125
|
return parsePackageManagerField((await fs.readJson(packageJsonPath)).packageManager);
|
|
149
126
|
}
|
|
127
|
+
async function detectFromDenoConfig(projectDir) {
|
|
128
|
+
for (const configFile of ["deno.json", "deno.jsonc"]) if (await fs.pathExists(path.join(projectDir, configFile))) return "deno";
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
150
131
|
async function detectFromLockfile(projectDir) {
|
|
151
132
|
for (const check of [
|
|
152
133
|
{
|
|
153
134
|
manager: "pnpm",
|
|
154
135
|
lockfile: "pnpm-lock.yaml"
|
|
155
136
|
},
|
|
137
|
+
{
|
|
138
|
+
manager: "yarn",
|
|
139
|
+
lockfile: "yarn.lock"
|
|
140
|
+
},
|
|
156
141
|
{
|
|
157
142
|
manager: "bun",
|
|
158
143
|
lockfile: "bun.lockb"
|
|
@@ -168,6 +153,10 @@ async function detectFromLockfile(projectDir) {
|
|
|
168
153
|
{
|
|
169
154
|
manager: "npm",
|
|
170
155
|
lockfile: "npm-shrinkwrap.json"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
manager: "deno",
|
|
159
|
+
lockfile: "deno.lock"
|
|
171
160
|
}
|
|
172
161
|
]) if (await fs.pathExists(path.join(projectDir, check.lockfile))) return check.manager;
|
|
173
162
|
return null;
|
|
@@ -177,49 +166,75 @@ async function detectPackageManager(projectDir = process.cwd()) {
|
|
|
177
166
|
if (fromPackageJson) return fromPackageJson;
|
|
178
167
|
const fromLockfile = await detectFromLockfile(projectDir);
|
|
179
168
|
if (fromLockfile) return fromLockfile;
|
|
169
|
+
const fromDenoConfig = await detectFromDenoConfig(projectDir);
|
|
170
|
+
if (fromDenoConfig) return fromDenoConfig;
|
|
180
171
|
const fromUserAgent = parseUserAgent(process.env.npm_config_user_agent);
|
|
181
172
|
if (fromUserAgent) return fromUserAgent;
|
|
182
173
|
return "npm";
|
|
183
174
|
}
|
|
175
|
+
function getPackageManagerManifestValue(packageManager) {
|
|
176
|
+
if (!packageManager || packageManager === "deno") return;
|
|
177
|
+
return packageManagerManifestValues[packageManager];
|
|
178
|
+
}
|
|
179
|
+
function getDenoPrismaSpecifier() {
|
|
180
|
+
return `npm:prisma@${dependencyVersionMap.prisma.replace(/^[^0-9]*/, "")}`;
|
|
181
|
+
}
|
|
184
182
|
function getInstallCommand(packageManager) {
|
|
183
|
+
if (packageManager === "deno") return "deno install --allow-scripts";
|
|
185
184
|
return `${packageManager} install`;
|
|
186
185
|
}
|
|
187
186
|
function getRunScriptCommand(packageManager, scriptName) {
|
|
188
187
|
switch (packageManager) {
|
|
188
|
+
case "deno": return `deno task ${scriptName}`;
|
|
189
189
|
case "bun": return `bun run ${scriptName}`;
|
|
190
190
|
case "pnpm": return `pnpm run ${scriptName}`;
|
|
191
|
+
case "yarn": return `yarn run ${scriptName}`;
|
|
191
192
|
default: return `npm run ${scriptName}`;
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
function getInstallArgs(packageManager) {
|
|
196
|
+
if (packageManager === "deno") return {
|
|
197
|
+
command: "deno",
|
|
198
|
+
args: ["install", "--allow-scripts"]
|
|
199
|
+
};
|
|
195
200
|
return {
|
|
196
201
|
command: packageManager,
|
|
197
202
|
args: ["install"]
|
|
198
203
|
};
|
|
199
204
|
}
|
|
200
|
-
function
|
|
205
|
+
function getPackageExecutionArgs(packageManager, commandArgs) {
|
|
201
206
|
switch (packageManager) {
|
|
202
207
|
case "pnpm": return {
|
|
203
208
|
command: "pnpm",
|
|
204
|
-
args: ["dlx"]
|
|
209
|
+
args: ["dlx", ...commandArgs]
|
|
210
|
+
};
|
|
211
|
+
case "yarn": return {
|
|
212
|
+
command: "yarn",
|
|
213
|
+
args: ["dlx", ...commandArgs]
|
|
205
214
|
};
|
|
206
215
|
case "bun": return {
|
|
207
216
|
command: "bunx",
|
|
208
|
-
args: []
|
|
217
|
+
args: [...commandArgs]
|
|
209
218
|
};
|
|
219
|
+
case "deno": {
|
|
220
|
+
const [packageName, ...args] = commandArgs;
|
|
221
|
+
if (!packageName) throw new Error("Package execution requires a package name.");
|
|
222
|
+
return {
|
|
223
|
+
command: "deno",
|
|
224
|
+
args: [
|
|
225
|
+
"run",
|
|
226
|
+
"-A",
|
|
227
|
+
`npm:${packageName}`,
|
|
228
|
+
...args
|
|
229
|
+
]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
210
232
|
default: return {
|
|
211
233
|
command: "npx",
|
|
212
|
-
args: []
|
|
234
|
+
args: [...commandArgs]
|
|
213
235
|
};
|
|
214
236
|
}
|
|
215
237
|
}
|
|
216
|
-
function getPackageExecutionArgs(packageManager, commandArgs) {
|
|
217
|
-
const executor = getPackageExecutor(packageManager);
|
|
218
|
-
return {
|
|
219
|
-
command: executor.command,
|
|
220
|
-
args: [...executor.args, ...commandArgs]
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
238
|
function getPackageExecutionCommand(packageManager, commandArgs) {
|
|
224
239
|
const execution = getPackageExecutionArgs(packageManager, commandArgs);
|
|
225
240
|
return [execution.command, ...execution.args].join(" ");
|
|
@@ -230,6 +245,15 @@ function getPrismaCliArgs(packageManager, prismaArgs) {
|
|
|
230
245
|
"prisma",
|
|
231
246
|
...prismaArgs
|
|
232
247
|
]);
|
|
248
|
+
if (packageManager === "deno") return {
|
|
249
|
+
command: "deno",
|
|
250
|
+
args: [
|
|
251
|
+
"run",
|
|
252
|
+
"-A",
|
|
253
|
+
getDenoPrismaSpecifier(),
|
|
254
|
+
...prismaArgs
|
|
255
|
+
]
|
|
256
|
+
};
|
|
233
257
|
return getPackageExecutionArgs(packageManager, ["prisma", ...prismaArgs]);
|
|
234
258
|
}
|
|
235
259
|
function getPrismaCliCommand(packageManager, prismaArgs) {
|
|
@@ -238,259 +262,88 @@ function getPrismaCliCommand(packageManager, prismaArgs) {
|
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
//#endregion
|
|
241
|
-
//#region src/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
throw new Error(`Unable to
|
|
265
|
+
//#region src/templates/shared.ts
|
|
266
|
+
Handlebars.registerHelper("eq", (left, right) => left === right);
|
|
267
|
+
Handlebars.registerHelper("installCommand", (packageManager) => packageManager ? getInstallCommand(packageManager) : "");
|
|
268
|
+
Handlebars.registerHelper("runScriptCommand", (packageManager, scriptName) => packageManager ? getRunScriptCommand(packageManager, scriptName) : "");
|
|
269
|
+
Handlebars.registerHelper("packageManagerManifestValue", (packageManager) => getPackageManagerManifestValue(packageManager) ?? "");
|
|
270
|
+
function findPackageRoot(startDir) {
|
|
271
|
+
let currentDir = startDir;
|
|
272
|
+
while (true) {
|
|
273
|
+
if (existsSync(path.join(currentDir, "package.json"))) return currentDir;
|
|
274
|
+
const parentDir = path.dirname(currentDir);
|
|
275
|
+
if (parentDir === currentDir) break;
|
|
276
|
+
currentDir = parentDir;
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Unable to locate package root from: ${startDir}`);
|
|
255
279
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
280
|
+
function resolveTemplatesDir(relativeTemplatesDir) {
|
|
281
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
282
|
+
const packageRoot = findPackageRoot(path.dirname(currentFilePath));
|
|
283
|
+
const templatePath = path.join(packageRoot, relativeTemplatesDir);
|
|
284
|
+
if (!existsSync(templatePath)) throw new Error(`Template directory not found at: ${templatePath}`);
|
|
285
|
+
return templatePath;
|
|
259
286
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
287
|
+
async function getTemplateFilesRecursively(dir) {
|
|
288
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
289
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
290
|
+
const entryPath = path.join(dir, entry.name);
|
|
291
|
+
if (entry.isDirectory()) return getTemplateFilesRecursively(entryPath);
|
|
292
|
+
if (!entry.isFile()) return [];
|
|
293
|
+
return [entryPath];
|
|
294
|
+
}))).flat();
|
|
264
295
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const commandString = getCreateDbCommand(packageManager);
|
|
268
|
-
let stdout;
|
|
269
|
-
try {
|
|
270
|
-
stdout = (await execa(command.command, command.args, {
|
|
271
|
-
cwd: projectDir,
|
|
272
|
-
stdio: "pipe"
|
|
273
|
-
})).stdout;
|
|
274
|
-
} catch (error) {
|
|
275
|
-
if (error instanceof Error && "stderr" in error) {
|
|
276
|
-
const stderr = String(error.stderr ?? "").trim();
|
|
277
|
-
const message = stderr.length > 0 ? stderr : error.message;
|
|
278
|
-
throw new Error(`Failed to run ${commandString}: ${message}`);
|
|
279
|
-
}
|
|
280
|
-
throw error;
|
|
281
|
-
}
|
|
282
|
-
const payload = parseCreateDbJson(stdout);
|
|
283
|
-
if (payload.success === false) throw new Error(extractErrorMessage(payload, "create-db reported failure."));
|
|
284
|
-
const databaseUrl = pickConnectionString(payload);
|
|
285
|
-
if (!databaseUrl) throw new Error("create-db did not return a connection string.");
|
|
286
|
-
return {
|
|
287
|
-
databaseUrl,
|
|
288
|
-
claimUrl: typeof payload.claimUrl === "string" && payload.claimUrl.length > 0 ? payload.claimUrl : typeof payload.claimURL === "string" && payload.claimURL.length > 0 ? payload.claimURL : void 0
|
|
289
|
-
};
|
|
296
|
+
function stripHbsExtension(filePath) {
|
|
297
|
+
return filePath.endsWith(".hbs") ? filePath.slice(0, -4) : filePath;
|
|
290
298
|
}
|
|
291
|
-
function
|
|
292
|
-
return
|
|
299
|
+
function ensureTrailingNewline(content) {
|
|
300
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
301
|
+
}
|
|
302
|
+
async function renderTemplateFile(opts) {
|
|
303
|
+
const { templateFilePath, outputPath, context } = opts;
|
|
304
|
+
const templateContent = await fs.readFile(templateFilePath, "utf8");
|
|
305
|
+
const outputContent = templateFilePath.endsWith(".hbs") ? Handlebars.compile(templateContent, {
|
|
306
|
+
noEscape: true,
|
|
307
|
+
strict: true
|
|
308
|
+
})(context) : templateContent;
|
|
309
|
+
if (templateFilePath.endsWith(".hbs") && outputContent.trim().length === 0) return;
|
|
310
|
+
await fs.outputFile(outputPath, ensureTrailingNewline(outputContent), "utf8");
|
|
311
|
+
}
|
|
312
|
+
async function renderTemplateTree(opts) {
|
|
313
|
+
const { templateRoot, outputDir, context } = opts;
|
|
314
|
+
const templateFiles = await getTemplateFilesRecursively(templateRoot);
|
|
315
|
+
for (const templateFilePath of templateFiles) {
|
|
316
|
+
const relativeOutputPath = stripHbsExtension(path.relative(templateRoot, templateFilePath));
|
|
317
|
+
await renderTemplateFile({
|
|
318
|
+
templateFilePath,
|
|
319
|
+
outputPath: path.join(outputDir, relativeOutputPath),
|
|
320
|
+
context
|
|
321
|
+
});
|
|
322
|
+
}
|
|
293
323
|
}
|
|
294
324
|
|
|
295
325
|
//#endregion
|
|
296
|
-
//#region src/templates/render-
|
|
297
|
-
function
|
|
298
|
-
return resolveTemplatesDir(
|
|
326
|
+
//#region src/templates/render-create-template.ts
|
|
327
|
+
function getCreateTemplateDir(template) {
|
|
328
|
+
return resolveTemplatesDir(`templates/create/${template}`);
|
|
299
329
|
}
|
|
300
|
-
function createTemplateContext(provider,
|
|
330
|
+
function createTemplateContext(projectName, provider, schemaPreset, packageManager) {
|
|
301
331
|
return {
|
|
302
|
-
|
|
332
|
+
projectName,
|
|
303
333
|
provider,
|
|
304
|
-
schemaPreset
|
|
334
|
+
schemaPreset,
|
|
335
|
+
packageManager
|
|
305
336
|
};
|
|
306
337
|
}
|
|
307
|
-
async function
|
|
338
|
+
async function scaffoldCreateTemplate(opts) {
|
|
339
|
+
const { projectDir, projectName, template, provider, schemaPreset, packageManager } = opts;
|
|
308
340
|
await renderTemplateTree({
|
|
309
|
-
templateRoot:
|
|
341
|
+
templateRoot: getCreateTemplateDir(template),
|
|
310
342
|
outputDir: projectDir,
|
|
311
|
-
context: createTemplateContext(provider,
|
|
343
|
+
context: createTemplateContext(projectName, provider, schemaPreset, packageManager)
|
|
312
344
|
});
|
|
313
|
-
return {
|
|
314
|
-
schemaPath: path.join(projectDir, "prisma/schema.prisma"),
|
|
315
|
-
configPath: path.join(projectDir, "prisma.config.ts"),
|
|
316
|
-
singletonPath: path.join(projectDir, "prisma/index.ts")
|
|
317
|
-
};
|
|
318
345
|
}
|
|
319
346
|
|
|
320
|
-
//#endregion
|
|
321
|
-
//#region src/tasks/init-prisma.ts
|
|
322
|
-
const prismaManagedFiles = [
|
|
323
|
-
"schema.prisma",
|
|
324
|
-
"prisma/schema.prisma",
|
|
325
|
-
"prisma/index.ts",
|
|
326
|
-
"prisma.config.ts"
|
|
327
|
-
];
|
|
328
|
-
const prismaTemplateFiles = [
|
|
329
|
-
"prisma/schema.prisma",
|
|
330
|
-
"prisma/index.ts",
|
|
331
|
-
"prisma.config.ts"
|
|
332
|
-
];
|
|
333
|
-
var PrismaAlreadyInitializedError = class extends Error {
|
|
334
|
-
existingFiles;
|
|
335
|
-
constructor(existingFiles) {
|
|
336
|
-
super(`This project already appears to be Prisma-initialized: ${existingFiles.join(", ")}`);
|
|
337
|
-
this.name = "PrismaAlreadyInitializedError";
|
|
338
|
-
this.existingFiles = existingFiles;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
function findExistingPrismaFiles(projectDir = process.cwd()) {
|
|
342
|
-
return prismaManagedFiles.map((relativePath) => path.join(projectDir, relativePath)).filter((absolutePath) => fs.existsSync(absolutePath));
|
|
343
|
-
}
|
|
344
|
-
function canReusePrismaFiles(projectDir = process.cwd()) {
|
|
345
|
-
return prismaTemplateFiles.every((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
|
|
346
|
-
}
|
|
347
|
-
function getDefaultDatabaseUrl(provider) {
|
|
348
|
-
switch (provider) {
|
|
349
|
-
case "postgresql": return "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public";
|
|
350
|
-
case "cockroachdb": return "postgresql://johndoe:randompassword@localhost:26257/mydb?schema=public";
|
|
351
|
-
case "mysql": return "mysql://johndoe:randompassword@localhost:3306/mydb";
|
|
352
|
-
case "sqlite": return "file:./dev.db";
|
|
353
|
-
case "sqlserver": return "sqlserver://localhost:1433;database=mydb;user=SA;password=randompassword;";
|
|
354
|
-
default: {
|
|
355
|
-
const exhaustiveCheck = provider;
|
|
356
|
-
throw new Error(`Unsupported provider: ${String(exhaustiveCheck)}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
function escapeRegExp(value) {
|
|
361
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
362
|
-
}
|
|
363
|
-
function hasEnvVar(content, envVarName) {
|
|
364
|
-
const escapedName = escapeRegExp(envVarName);
|
|
365
|
-
return new RegExp(`(^|\\n)\\s*${escapedName}\\s*=`).test(content);
|
|
366
|
-
}
|
|
367
|
-
function hasEnvComment(content, comment) {
|
|
368
|
-
const escapedComment = escapeRegExp(comment);
|
|
369
|
-
return new RegExp(`(^|\\n)\\s*#\\s*${escapedComment}\\s*(?=\\n|$)`).test(content);
|
|
370
|
-
}
|
|
371
|
-
async function ensureEnvVarInEnv(projectDir, envVarName, envVarValue, opts) {
|
|
372
|
-
const envPath = path.join(projectDir, ".env");
|
|
373
|
-
const envLine = `${envVarName}="${envVarValue}"`;
|
|
374
|
-
if (!await fs.pathExists(envPath)) {
|
|
375
|
-
await fs.writeFile(envPath, `${envLine}\n`, "utf8");
|
|
376
|
-
return {
|
|
377
|
-
envPath,
|
|
378
|
-
status: "created"
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
const existingContent = await fs.readFile(envPath, "utf8");
|
|
382
|
-
if (hasEnvVar(existingContent, envVarName)) {
|
|
383
|
-
if (opts.mode === "keep-existing") return {
|
|
384
|
-
envPath,
|
|
385
|
-
status: "existing"
|
|
386
|
-
};
|
|
387
|
-
const escapedName = escapeRegExp(envVarName);
|
|
388
|
-
const lineRegex = new RegExp(`(^|\\n)\\s*${escapedName}\\s*=.*(?=\\n|$)`, "m");
|
|
389
|
-
const updatedContent = existingContent.replace(lineRegex, `$1${envLine}`);
|
|
390
|
-
if (updatedContent === existingContent) return {
|
|
391
|
-
envPath,
|
|
392
|
-
status: "existing"
|
|
393
|
-
};
|
|
394
|
-
await fs.writeFile(envPath, updatedContent, "utf8");
|
|
395
|
-
return {
|
|
396
|
-
envPath,
|
|
397
|
-
status: "updated"
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
const insertion = `${existingContent.endsWith("\n") ? "" : "\n"}${opts.comment ? `\n# ${opts.comment}\n` : "\n"}${envLine}\n`;
|
|
401
|
-
await fs.appendFile(envPath, insertion, "utf8");
|
|
402
|
-
return {
|
|
403
|
-
envPath,
|
|
404
|
-
status: "appended"
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
async function ensureEnvComment(projectDir, comment) {
|
|
408
|
-
const envPath = path.join(projectDir, ".env");
|
|
409
|
-
const commentLine = `# ${comment}`;
|
|
410
|
-
if (!await fs.pathExists(envPath)) {
|
|
411
|
-
await fs.writeFile(envPath, `${commentLine}\n`, "utf8");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const existingContent = await fs.readFile(envPath, "utf8");
|
|
415
|
-
if (hasEnvComment(existingContent, comment)) return;
|
|
416
|
-
const separator = existingContent.endsWith("\n") ? "" : "\n";
|
|
417
|
-
await fs.appendFile(envPath, `${separator}${commentLine}\n`, "utf8");
|
|
418
|
-
}
|
|
419
|
-
function hasGitignoreEntry(content, entry) {
|
|
420
|
-
const escapedEntry = escapeRegExp(entry);
|
|
421
|
-
const escapedWithLeadingSlash = escapeRegExp(`/${entry}`);
|
|
422
|
-
return new RegExp(`(^|\\n)\\s*(?:${escapedEntry}|${escapedWithLeadingSlash})\\s*(?=\\n|$)`).test(content);
|
|
423
|
-
}
|
|
424
|
-
async function ensureGitignoreEntry(projectDir, entry) {
|
|
425
|
-
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
426
|
-
if (!await fs.pathExists(gitignorePath)) {
|
|
427
|
-
await fs.writeFile(gitignorePath, `${entry}\n`, "utf8");
|
|
428
|
-
return {
|
|
429
|
-
gitignorePath,
|
|
430
|
-
status: "created"
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
const existingContent = await fs.readFile(gitignorePath, "utf8");
|
|
434
|
-
if (hasGitignoreEntry(existingContent, entry)) return {
|
|
435
|
-
gitignorePath,
|
|
436
|
-
status: "existing"
|
|
437
|
-
};
|
|
438
|
-
const separator = existingContent.endsWith("\n") ? "" : "\n";
|
|
439
|
-
await fs.appendFile(gitignorePath, `${separator}${entry}\n`, "utf8");
|
|
440
|
-
return {
|
|
441
|
-
gitignorePath,
|
|
442
|
-
status: "appended"
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
async function initializePrismaFiles(options) {
|
|
446
|
-
const projectDir = options.projectDir ?? process.cwd();
|
|
447
|
-
const prismaFilesMode = options.prismaFilesMode ?? "create";
|
|
448
|
-
const existingPrismaFiles = findExistingPrismaFiles(projectDir);
|
|
449
|
-
if (prismaFilesMode === "create" && existingPrismaFiles.length > 0) throw new PrismaAlreadyInitializedError(existingPrismaFiles);
|
|
450
|
-
if (prismaFilesMode === "reuse" && existingPrismaFiles.length === 0) throw new Error("Cannot reuse Prisma files because no existing Prisma files were found.");
|
|
451
|
-
if (prismaFilesMode === "reuse" && !canReusePrismaFiles(projectDir)) throw new Error("Cannot reuse Prisma files because required files are missing.");
|
|
452
|
-
const schemaPath = path.join(projectDir, "prisma/schema.prisma");
|
|
453
|
-
const configPath = path.join(projectDir, "prisma.config.ts");
|
|
454
|
-
const singletonPath = path.join(projectDir, "prisma/index.ts");
|
|
455
|
-
if (prismaFilesMode !== "reuse") await scaffoldInitTemplates(projectDir, options.provider, "DATABASE_URL", options.schemaPreset ?? "empty");
|
|
456
|
-
const envResult = await ensureEnvVarInEnv(projectDir, "DATABASE_URL", options.databaseUrl ?? getDefaultDatabaseUrl(options.provider), {
|
|
457
|
-
mode: options.databaseUrl ? "upsert" : "keep-existing",
|
|
458
|
-
comment: "Added by create-prisma init"
|
|
459
|
-
});
|
|
460
|
-
let claimEnvStatus;
|
|
461
|
-
if (options.claimUrl) {
|
|
462
|
-
claimEnvStatus = (await ensureEnvVarInEnv(projectDir, "CLAIM_URL", options.claimUrl, {
|
|
463
|
-
mode: "upsert",
|
|
464
|
-
comment: PRISMA_POSTGRES_TEMPORARY_NOTICE
|
|
465
|
-
})).status;
|
|
466
|
-
await ensureEnvComment(projectDir, PRISMA_POSTGRES_TEMPORARY_NOTICE);
|
|
467
|
-
}
|
|
468
|
-
const gitignoreResult = await ensureGitignoreEntry(projectDir, "prisma/generated");
|
|
469
|
-
return {
|
|
470
|
-
schemaPath,
|
|
471
|
-
configPath,
|
|
472
|
-
singletonPath,
|
|
473
|
-
prismaFilesMode,
|
|
474
|
-
envPath: envResult.envPath,
|
|
475
|
-
envStatus: envResult.status,
|
|
476
|
-
gitignorePath: gitignoreResult.gitignorePath,
|
|
477
|
-
gitignoreStatus: gitignoreResult.status,
|
|
478
|
-
claimEnvStatus
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
//#endregion
|
|
483
|
-
//#region src/constants/dependencies.ts
|
|
484
|
-
const dependencyVersionMap = {
|
|
485
|
-
"@prisma/client": "^7.4.0",
|
|
486
|
-
"@prisma/adapter-pg": "^7.4.0",
|
|
487
|
-
"@prisma/adapter-mariadb": "^7.4.0",
|
|
488
|
-
"@prisma/adapter-better-sqlite3": "^7.4.0",
|
|
489
|
-
"@prisma/adapter-mssql": "^7.4.0",
|
|
490
|
-
dotenv: "^17.2.3",
|
|
491
|
-
prisma: "^7.4.0"
|
|
492
|
-
};
|
|
493
|
-
|
|
494
347
|
//#endregion
|
|
495
348
|
//#region src/constants/db-packages.ts
|
|
496
349
|
function getDbPackages(provider) {
|
|
@@ -509,11 +362,23 @@ function getDbPackages(provider) {
|
|
|
509
362
|
|
|
510
363
|
//#endregion
|
|
511
364
|
//#region src/tasks/install.ts
|
|
512
|
-
|
|
513
|
-
"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
365
|
+
function getPrismaScriptMap(packageManager) {
|
|
366
|
+
if (packageManager === "deno") {
|
|
367
|
+
const prismaSpecifier = getDenoPrismaSpecifier();
|
|
368
|
+
return {
|
|
369
|
+
"db:generate": `deno run -A ${prismaSpecifier} generate`,
|
|
370
|
+
"db:push": `deno run -A ${prismaSpecifier} db push`,
|
|
371
|
+
"db:migrate": `deno run -A ${prismaSpecifier} migrate dev`,
|
|
372
|
+
"db:seed": `deno run -A ${prismaSpecifier} db seed`
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
"db:generate": "prisma generate",
|
|
377
|
+
"db:push": "prisma db push",
|
|
378
|
+
"db:migrate": "prisma migrate dev",
|
|
379
|
+
"db:seed": "prisma db seed"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
517
382
|
function getVersion(packageName) {
|
|
518
383
|
return dependencyVersionMap[packageName];
|
|
519
384
|
}
|
|
@@ -565,11 +430,13 @@ async function addPackageDependency(opts) {
|
|
|
565
430
|
existingScripts
|
|
566
431
|
};
|
|
567
432
|
}
|
|
568
|
-
async function writePrismaDependencies(provider, projectDir = process.cwd()) {
|
|
433
|
+
async function writePrismaDependencies(provider, packageManager, projectDir = process.cwd()) {
|
|
569
434
|
const dependencies = ["@prisma/client", "dotenv"];
|
|
570
435
|
const devDependencies = ["prisma"];
|
|
571
436
|
const { adapterPackage } = getDbPackages(provider);
|
|
572
437
|
dependencies.push(adapterPackage);
|
|
438
|
+
if (provider === "sqlite" && packageManager === "deno") devDependencies.push("node-gyp");
|
|
439
|
+
const prismaScriptMap = getPrismaScriptMap(packageManager);
|
|
573
440
|
const scriptWriteResult = await addPackageDependency({
|
|
574
441
|
dependencies,
|
|
575
442
|
devDependencies,
|
|
@@ -595,19 +462,83 @@ async function installProjectDependencies(packageManager, projectDir = process.c
|
|
|
595
462
|
}
|
|
596
463
|
|
|
597
464
|
//#endregion
|
|
598
|
-
//#region src/
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
465
|
+
//#region src/tasks/prisma-postgres.ts
|
|
466
|
+
const PRISMA_POSTGRES_TEMPORARY_NOTICE = "Prisma Postgres is temporary for 24 hours. Claim this database before it expires using CLAIM_URL.";
|
|
467
|
+
const CREATE_DB_COMMAND_ARGS = ["create-db@latest", "--json"];
|
|
468
|
+
function parseCreateDbJson(rawOutput) {
|
|
469
|
+
const trimmed = rawOutput.trim();
|
|
470
|
+
if (!trimmed) throw new Error("create-db returned empty output.");
|
|
471
|
+
const jsonCandidates = [trimmed];
|
|
472
|
+
const firstBrace = trimmed.indexOf("{");
|
|
473
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
474
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) jsonCandidates.push(trimmed.slice(firstBrace, lastBrace + 1));
|
|
475
|
+
for (const candidate of jsonCandidates) try {
|
|
476
|
+
return JSON.parse(candidate);
|
|
477
|
+
} catch {}
|
|
478
|
+
throw new Error(`Unable to parse create-db JSON output: ${trimmed}`);
|
|
479
|
+
}
|
|
480
|
+
function pickConnectionString(payload) {
|
|
481
|
+
if (typeof payload.connectionString === "string" && payload.connectionString.length > 0) return payload.connectionString;
|
|
482
|
+
if (typeof payload.databaseUrl === "string" && payload.databaseUrl.length > 0) return payload.databaseUrl;
|
|
483
|
+
}
|
|
484
|
+
function extractErrorMessage(payload, fallback) {
|
|
485
|
+
if (typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
486
|
+
if (typeof payload.error === "string" && payload.error.length > 0) return payload.error;
|
|
487
|
+
return fallback;
|
|
488
|
+
}
|
|
489
|
+
async function provisionPrismaPostgres(packageManager, projectDir = process.cwd()) {
|
|
490
|
+
const command = getPackageExecutionArgs(packageManager, [...CREATE_DB_COMMAND_ARGS]);
|
|
491
|
+
const commandString = getCreateDbCommand(packageManager);
|
|
492
|
+
let stdout;
|
|
493
|
+
try {
|
|
494
|
+
stdout = (await execa(command.command, command.args, {
|
|
495
|
+
cwd: projectDir,
|
|
496
|
+
stdio: "pipe"
|
|
497
|
+
})).stdout;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof Error && "stderr" in error) {
|
|
500
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
501
|
+
const message = stderr.length > 0 ? stderr : error.message;
|
|
502
|
+
throw new Error(`Failed to run ${commandString}: ${message}`);
|
|
503
|
+
}
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
const payload = parseCreateDbJson(stdout);
|
|
507
|
+
if (payload.success === false) throw new Error(extractErrorMessage(payload, "create-db reported failure."));
|
|
508
|
+
const databaseUrl = pickConnectionString(payload);
|
|
509
|
+
if (!databaseUrl) throw new Error("create-db did not return a connection string.");
|
|
510
|
+
return {
|
|
511
|
+
databaseUrl,
|
|
512
|
+
claimUrl: typeof payload.claimUrl === "string" && payload.claimUrl.length > 0 ? payload.claimUrl : typeof payload.claimURL === "string" && payload.claimURL.length > 0 ? payload.claimURL : void 0
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function getCreateDbCommand(packageManager) {
|
|
516
|
+
return getPackageExecutionCommand(packageManager, [...CREATE_DB_COMMAND_ARGS]);
|
|
602
517
|
}
|
|
603
518
|
|
|
604
519
|
//#endregion
|
|
605
|
-
//#region src/
|
|
520
|
+
//#region src/tasks/setup-prisma.ts
|
|
606
521
|
const DEFAULT_DATABASE_PROVIDER = "postgresql";
|
|
607
522
|
const DEFAULT_SCHEMA_PRESET$1 = "empty";
|
|
608
523
|
const DEFAULT_PRISMA_POSTGRES = true;
|
|
609
524
|
const DEFAULT_INSTALL = true;
|
|
610
525
|
const DEFAULT_GENERATE = true;
|
|
526
|
+
const requiredPrismaFileGroups = [
|
|
527
|
+
["prisma/schema.prisma", "packages/db/prisma/schema.prisma"],
|
|
528
|
+
["prisma/seed.ts", "packages/db/prisma/seed.ts"],
|
|
529
|
+
["prisma.config.ts", "packages/db/prisma.config.ts"],
|
|
530
|
+
[
|
|
531
|
+
"src/lib/prisma.ts",
|
|
532
|
+
"src/lib/server/prisma.ts",
|
|
533
|
+
"server/utils/prisma.ts",
|
|
534
|
+
"packages/db/src/client.ts"
|
|
535
|
+
]
|
|
536
|
+
];
|
|
537
|
+
async function resolvePrismaProjectDir(projectDir) {
|
|
538
|
+
const monorepoDbDir = path.join(projectDir, "packages/db");
|
|
539
|
+
if (await fs.pathExists(path.join(monorepoDbDir, "prisma/schema.prisma"))) return monorepoDbDir;
|
|
540
|
+
return projectDir;
|
|
541
|
+
}
|
|
611
542
|
async function promptForDatabaseProvider() {
|
|
612
543
|
const databaseProvider = await select({
|
|
613
544
|
message: "Select your database",
|
|
@@ -645,6 +576,7 @@ async function promptForDatabaseProvider() {
|
|
|
645
576
|
function getPackageManagerHint(option, detected) {
|
|
646
577
|
if (option === detected) return "Detected";
|
|
647
578
|
if (option === "bun") return "Fast runtime + package manager";
|
|
579
|
+
if (option === "deno") return "Runtime + package manager";
|
|
648
580
|
}
|
|
649
581
|
async function promptForPackageManager(detectedPackageManager) {
|
|
650
582
|
const packageManager = await select({
|
|
@@ -661,10 +593,20 @@ async function promptForPackageManager(detectedPackageManager) {
|
|
|
661
593
|
label: "pnpm",
|
|
662
594
|
hint: getPackageManagerHint("pnpm", detectedPackageManager)
|
|
663
595
|
},
|
|
596
|
+
{
|
|
597
|
+
value: "yarn",
|
|
598
|
+
label: "yarn",
|
|
599
|
+
hint: getPackageManagerHint("yarn", detectedPackageManager)
|
|
600
|
+
},
|
|
664
601
|
{
|
|
665
602
|
value: "bun",
|
|
666
603
|
label: "bun",
|
|
667
604
|
hint: getPackageManagerHint("bun", detectedPackageManager)
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
value: "deno",
|
|
608
|
+
label: "deno",
|
|
609
|
+
hint: getPackageManagerHint("deno", detectedPackageManager)
|
|
668
610
|
}
|
|
669
611
|
]
|
|
670
612
|
});
|
|
@@ -696,50 +638,6 @@ async function promptForPrismaPostgres() {
|
|
|
696
638
|
}
|
|
697
639
|
return Boolean(shouldUsePrismaPostgres);
|
|
698
640
|
}
|
|
699
|
-
async function promptForPrismaFilesMode(existingFiles, canReuseExistingPrismaFiles, baseDir) {
|
|
700
|
-
const mode = await select({
|
|
701
|
-
message: `Prisma already exists (${existingFiles.map((filePath) => formatCreatedPath(filePath, baseDir)).join(", ")}). How should we continue?`,
|
|
702
|
-
initialValue: canReuseExistingPrismaFiles ? "reuse" : "overwrite",
|
|
703
|
-
options: canReuseExistingPrismaFiles ? [
|
|
704
|
-
{
|
|
705
|
-
value: "reuse",
|
|
706
|
-
label: "Keep existing Prisma files",
|
|
707
|
-
hint: "Recommended"
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
value: "overwrite",
|
|
711
|
-
label: "Overwrite Prisma files"
|
|
712
|
-
},
|
|
713
|
-
{
|
|
714
|
-
value: "cancel",
|
|
715
|
-
label: "Cancel"
|
|
716
|
-
}
|
|
717
|
-
] : [{
|
|
718
|
-
value: "overwrite",
|
|
719
|
-
label: "Repair and overwrite Prisma files",
|
|
720
|
-
hint: "Recommended"
|
|
721
|
-
}, {
|
|
722
|
-
value: "cancel",
|
|
723
|
-
label: "Cancel"
|
|
724
|
-
}]
|
|
725
|
-
});
|
|
726
|
-
if (isCancel(mode)) {
|
|
727
|
-
cancel("Operation cancelled.");
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (mode === "cancel") {
|
|
731
|
-
cancel("Operation cancelled.");
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (mode !== "reuse" && mode !== "overwrite") {
|
|
735
|
-
cancel("Operation cancelled.");
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
return mode;
|
|
739
|
-
}
|
|
740
|
-
function formatCreatedPath(filePath, baseDir) {
|
|
741
|
-
return path.relative(baseDir, filePath);
|
|
742
|
-
}
|
|
743
641
|
function getCommandErrorMessage(error) {
|
|
744
642
|
if (error instanceof Error && "stderr" in error) {
|
|
745
643
|
const stderr = String(error.stderr ?? "").trim();
|
|
@@ -747,25 +645,14 @@ function getCommandErrorMessage(error) {
|
|
|
747
645
|
}
|
|
748
646
|
return error instanceof Error ? error.message : String(error);
|
|
749
647
|
}
|
|
750
|
-
async function
|
|
648
|
+
async function collectPrismaSetupContext(input, options = {}) {
|
|
751
649
|
const projectDir = path.resolve(options.projectDir ?? process.cwd());
|
|
752
|
-
const input = InitCommandInputSchema.parse(rawInput);
|
|
753
650
|
const useDefaults = input.yes === true;
|
|
754
651
|
const verbose = input.verbose === true;
|
|
755
652
|
const shouldGenerate = input.generate ?? DEFAULT_GENERATE;
|
|
756
|
-
if (!options.skipIntro) intro(getCreatePrismaIntro());
|
|
757
|
-
let prismaFilesMode = "create";
|
|
758
|
-
const existingPrismaFiles = findExistingPrismaFiles(projectDir);
|
|
759
|
-
const canReuseExistingPrismaFiles = canReusePrismaFiles(projectDir);
|
|
760
|
-
if (existingPrismaFiles.length > 0) if (useDefaults) prismaFilesMode = canReuseExistingPrismaFiles ? "reuse" : "overwrite";
|
|
761
|
-
else {
|
|
762
|
-
const selectedMode = await promptForPrismaFilesMode(existingPrismaFiles, canReuseExistingPrismaFiles, projectDir);
|
|
763
|
-
if (!selectedMode) return;
|
|
764
|
-
prismaFilesMode = selectedMode;
|
|
765
|
-
}
|
|
766
653
|
const databaseProvider = input.provider ?? (useDefaults ? DEFAULT_DATABASE_PROVIDER : await promptForDatabaseProvider());
|
|
767
654
|
if (!databaseProvider) return;
|
|
768
|
-
const schemaPreset = input.schemaPreset ?? DEFAULT_SCHEMA_PRESET$1;
|
|
655
|
+
const schemaPreset = input.schemaPreset ?? options.defaultSchemaPreset ?? DEFAULT_SCHEMA_PRESET$1;
|
|
769
656
|
const databaseUrl = input.databaseUrl;
|
|
770
657
|
let shouldUsePrismaPostgres = false;
|
|
771
658
|
if (databaseProvider === "postgresql" && !databaseUrl) {
|
|
@@ -782,7 +669,6 @@ async function collectInitContext(rawInput = {}, options = {}) {
|
|
|
782
669
|
projectDir,
|
|
783
670
|
verbose,
|
|
784
671
|
shouldGenerate,
|
|
785
|
-
prismaFilesMode,
|
|
786
672
|
databaseProvider,
|
|
787
673
|
schemaPreset,
|
|
788
674
|
databaseUrl,
|
|
@@ -791,6 +677,158 @@ async function collectInitContext(rawInput = {}, options = {}) {
|
|
|
791
677
|
shouldInstall
|
|
792
678
|
};
|
|
793
679
|
}
|
|
680
|
+
function getDefaultDatabaseUrl(provider) {
|
|
681
|
+
switch (provider) {
|
|
682
|
+
case "postgresql": return "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public";
|
|
683
|
+
case "cockroachdb": return "postgresql://johndoe:randompassword@localhost:26257/mydb?schema=public";
|
|
684
|
+
case "mysql": return "mysql://johndoe:randompassword@localhost:3306/mydb";
|
|
685
|
+
case "sqlite": return "file:./dev.db";
|
|
686
|
+
case "sqlserver": return "sqlserver://localhost:1433;database=mydb;user=SA;password=randompassword;";
|
|
687
|
+
default: {
|
|
688
|
+
const exhaustiveCheck = provider;
|
|
689
|
+
throw new Error(`Unsupported provider: ${String(exhaustiveCheck)}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function escapeRegExp(value) {
|
|
694
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
695
|
+
}
|
|
696
|
+
function escapeEnvValue(value) {
|
|
697
|
+
if (/[\r\n]/.test(value)) throw new Error("Environment variable values must be single-line.");
|
|
698
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
699
|
+
}
|
|
700
|
+
function hasEnvVar(content, envVarName) {
|
|
701
|
+
const escapedName = escapeRegExp(envVarName);
|
|
702
|
+
return new RegExp(`(^|\\n)\\s*${escapedName}\\s*=`).test(content);
|
|
703
|
+
}
|
|
704
|
+
function hasEnvComment(content, comment) {
|
|
705
|
+
const escapedComment = escapeRegExp(comment);
|
|
706
|
+
return new RegExp(`(^|\\n)\\s*#\\s*${escapedComment}\\s*(?=\\n|$)`).test(content);
|
|
707
|
+
}
|
|
708
|
+
async function ensureEnvVarInEnv(projectDir, envVarName, envVarValue, opts) {
|
|
709
|
+
const envPath = path.join(projectDir, ".env");
|
|
710
|
+
const envLine = `${envVarName}="${escapeEnvValue(envVarValue)}"`;
|
|
711
|
+
if (!await fs.pathExists(envPath)) {
|
|
712
|
+
const content = opts.comment ? `# ${opts.comment}\n${envLine}\n` : `${envLine}\n`;
|
|
713
|
+
await fs.writeFile(envPath, content, "utf8");
|
|
714
|
+
return {
|
|
715
|
+
envPath,
|
|
716
|
+
status: "created"
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const existingContent = await fs.readFile(envPath, "utf8");
|
|
720
|
+
if (hasEnvVar(existingContent, envVarName)) {
|
|
721
|
+
if (opts.mode === "keep-existing") return {
|
|
722
|
+
envPath,
|
|
723
|
+
status: "existing"
|
|
724
|
+
};
|
|
725
|
+
const escapedName = escapeRegExp(envVarName);
|
|
726
|
+
const lineRegex = new RegExp(`(^|\\n)\\s*${escapedName}\\s*=.*(?=\\n|$)`, "gm");
|
|
727
|
+
const updatedContent = existingContent.replace(lineRegex, `$1${envLine}`);
|
|
728
|
+
if (updatedContent === existingContent) return {
|
|
729
|
+
envPath,
|
|
730
|
+
status: "existing"
|
|
731
|
+
};
|
|
732
|
+
await fs.writeFile(envPath, updatedContent, "utf8");
|
|
733
|
+
return {
|
|
734
|
+
envPath,
|
|
735
|
+
status: "updated"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const insertion = `${existingContent.endsWith("\n") ? "" : "\n"}${opts.comment ? `\n# ${opts.comment}\n` : "\n"}${envLine}\n`;
|
|
739
|
+
await fs.appendFile(envPath, insertion, "utf8");
|
|
740
|
+
return {
|
|
741
|
+
envPath,
|
|
742
|
+
status: "appended"
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
async function ensureEnvComment(projectDir, comment) {
|
|
746
|
+
const envPath = path.join(projectDir, ".env");
|
|
747
|
+
const commentLine = `# ${comment}`;
|
|
748
|
+
if (!await fs.pathExists(envPath)) {
|
|
749
|
+
await fs.writeFile(envPath, `${commentLine}\n`, "utf8");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const existingContent = await fs.readFile(envPath, "utf8");
|
|
753
|
+
if (hasEnvComment(existingContent, comment)) return;
|
|
754
|
+
const separator = existingContent.endsWith("\n") ? "" : "\n";
|
|
755
|
+
await fs.appendFile(envPath, `${separator}${commentLine}\n`, "utf8");
|
|
756
|
+
}
|
|
757
|
+
function hasGitignoreEntry(content, entry) {
|
|
758
|
+
const escapedEntry = escapeRegExp(entry);
|
|
759
|
+
const escapedWithLeadingSlash = escapeRegExp(`/${entry}`);
|
|
760
|
+
const escapedWithTrailingSlash = escapeRegExp(`${entry}/`);
|
|
761
|
+
const escapedWithLeadingAndTrailingSlash = escapeRegExp(`/${entry}/`);
|
|
762
|
+
return new RegExp(`(^|\\n)\\s*(?:${escapedEntry}|${escapedWithLeadingSlash}|${escapedWithTrailingSlash}|${escapedWithLeadingAndTrailingSlash})\\s*(?=\\n|$)`).test(content);
|
|
763
|
+
}
|
|
764
|
+
async function ensureGitignoreEntry(projectDir, entry) {
|
|
765
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
766
|
+
if (!await fs.pathExists(gitignorePath)) {
|
|
767
|
+
await fs.writeFile(gitignorePath, `${entry}\n`, "utf8");
|
|
768
|
+
return {
|
|
769
|
+
gitignorePath,
|
|
770
|
+
status: "created"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const existingContent = await fs.readFile(gitignorePath, "utf8");
|
|
774
|
+
if (hasGitignoreEntry(existingContent, entry)) return {
|
|
775
|
+
gitignorePath,
|
|
776
|
+
status: "existing"
|
|
777
|
+
};
|
|
778
|
+
const separator = existingContent.endsWith("\n") ? "" : "\n";
|
|
779
|
+
await fs.appendFile(gitignorePath, `${separator}${entry}\n`, "utf8");
|
|
780
|
+
return {
|
|
781
|
+
gitignorePath,
|
|
782
|
+
status: "appended"
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async function ensureRequiredPrismaFiles(projectDir) {
|
|
786
|
+
const missingFiles = [];
|
|
787
|
+
for (const candidates of requiredPrismaFileGroups) {
|
|
788
|
+
let foundCandidate = false;
|
|
789
|
+
for (const relativePath of candidates) {
|
|
790
|
+
const absolutePath = path.join(projectDir, relativePath);
|
|
791
|
+
if (await fs.pathExists(absolutePath)) {
|
|
792
|
+
foundCandidate = true;
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (!foundCandidate) missingFiles.push(candidates.join(" or "));
|
|
797
|
+
}
|
|
798
|
+
if (missingFiles.length > 0) throw new Error(`Template is missing required Prisma files: ${missingFiles.join(", ")}`);
|
|
799
|
+
}
|
|
800
|
+
async function finalizePrismaFiles(options) {
|
|
801
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
802
|
+
const prismaProjectDir = await resolvePrismaProjectDir(projectDir);
|
|
803
|
+
const schemaPath = path.join(prismaProjectDir, "prisma/schema.prisma");
|
|
804
|
+
const configPath = path.join(prismaProjectDir, "prisma.config.ts");
|
|
805
|
+
await ensureRequiredPrismaFiles(projectDir);
|
|
806
|
+
const singletonPath = await fs.pathExists(path.join(prismaProjectDir, "src/lib/prisma.ts")) ? path.join(prismaProjectDir, "src/lib/prisma.ts") : await fs.pathExists(path.join(prismaProjectDir, "src/lib/server/prisma.ts")) ? path.join(prismaProjectDir, "src/lib/server/prisma.ts") : await fs.pathExists(path.join(prismaProjectDir, "server/utils/prisma.ts")) ? path.join(prismaProjectDir, "server/utils/prisma.ts") : path.join(prismaProjectDir, "src/client.ts");
|
|
807
|
+
const generatedDir = await fs.pathExists(path.join(prismaProjectDir, "server/utils/prisma.ts")) ? "server/generated" : "src/generated";
|
|
808
|
+
const envResult = await ensureEnvVarInEnv(prismaProjectDir, "DATABASE_URL", options.databaseUrl ?? getDefaultDatabaseUrl(options.provider), {
|
|
809
|
+
mode: options.databaseUrl ? "upsert" : "keep-existing",
|
|
810
|
+
comment: "Added by create-prisma"
|
|
811
|
+
});
|
|
812
|
+
let claimEnvStatus;
|
|
813
|
+
if (options.claimUrl) {
|
|
814
|
+
claimEnvStatus = (await ensureEnvVarInEnv(prismaProjectDir, "CLAIM_URL", options.claimUrl, {
|
|
815
|
+
mode: "upsert",
|
|
816
|
+
comment: PRISMA_POSTGRES_TEMPORARY_NOTICE
|
|
817
|
+
})).status;
|
|
818
|
+
await ensureEnvComment(prismaProjectDir, PRISMA_POSTGRES_TEMPORARY_NOTICE);
|
|
819
|
+
}
|
|
820
|
+
const gitignoreResult = await ensureGitignoreEntry(prismaProjectDir, generatedDir);
|
|
821
|
+
return {
|
|
822
|
+
schemaPath,
|
|
823
|
+
configPath,
|
|
824
|
+
singletonPath,
|
|
825
|
+
envPath: envResult.envPath,
|
|
826
|
+
envStatus: envResult.status,
|
|
827
|
+
gitignorePath: gitignoreResult.gitignorePath,
|
|
828
|
+
gitignoreStatus: gitignoreResult.status,
|
|
829
|
+
claimEnvStatus
|
|
830
|
+
};
|
|
831
|
+
}
|
|
794
832
|
async function provisionPrismaPostgresIfNeeded(context, projectDir) {
|
|
795
833
|
if (!context.shouldUsePrismaPostgres) return { databaseUrl: context.databaseUrl };
|
|
796
834
|
const createDbCommand = getCreateDbCommand(context.packageManager);
|
|
@@ -813,8 +851,9 @@ async function provisionPrismaPostgresIfNeeded(context, projectDir) {
|
|
|
813
851
|
}
|
|
814
852
|
}
|
|
815
853
|
async function writeDependenciesForContext(context, projectDir) {
|
|
854
|
+
const prismaProjectDir = await resolvePrismaProjectDir(projectDir);
|
|
816
855
|
try {
|
|
817
|
-
return await writePrismaDependencies(context.databaseProvider,
|
|
856
|
+
return await writePrismaDependencies(context.databaseProvider, context.packageManager, prismaProjectDir);
|
|
818
857
|
} catch (error) {
|
|
819
858
|
cancel(getCommandErrorMessage(error));
|
|
820
859
|
return;
|
|
@@ -846,22 +885,18 @@ async function installDependenciesForContext(context, projectDir) {
|
|
|
846
885
|
return false;
|
|
847
886
|
}
|
|
848
887
|
}
|
|
849
|
-
async function
|
|
888
|
+
async function finalizePrismaFilesForContext(context, projectDir, provisionResult) {
|
|
850
889
|
const initSpinner = spinner();
|
|
851
890
|
initSpinner.start("Preparing Prisma files...");
|
|
852
891
|
try {
|
|
853
|
-
const
|
|
892
|
+
const finalizeResult = await finalizePrismaFiles({
|
|
854
893
|
provider: context.databaseProvider,
|
|
855
894
|
databaseUrl: provisionResult.databaseUrl,
|
|
856
895
|
claimUrl: provisionResult.claimUrl,
|
|
857
|
-
schemaPreset: context.schemaPreset,
|
|
858
|
-
prismaFilesMode: context.prismaFilesMode,
|
|
859
896
|
projectDir
|
|
860
897
|
});
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
else initSpinner.stop("Prisma files ready.");
|
|
864
|
-
return initResult;
|
|
898
|
+
initSpinner.stop("Prisma files ready.");
|
|
899
|
+
return finalizeResult;
|
|
865
900
|
} catch (error) {
|
|
866
901
|
initSpinner.stop("Could not prepare Prisma files.");
|
|
867
902
|
cancel(getCommandErrorMessage(error));
|
|
@@ -869,6 +904,7 @@ async function initializePrismaFilesForContext(context, projectDir, provisionRes
|
|
|
869
904
|
}
|
|
870
905
|
}
|
|
871
906
|
async function generatePrismaClientForContext(context, projectDir) {
|
|
907
|
+
const prismaProjectDir = await resolvePrismaProjectDir(projectDir);
|
|
872
908
|
if (!context.shouldGenerate) return { didGenerateClient: false };
|
|
873
909
|
const generateCommand = getPrismaCliCommand(context.packageManager, ["generate"]);
|
|
874
910
|
if (context.verbose) log.step(`Running ${generateCommand}`);
|
|
@@ -877,7 +913,7 @@ async function generatePrismaClientForContext(context, projectDir) {
|
|
|
877
913
|
try {
|
|
878
914
|
const generateArgs = getPrismaCliArgs(context.packageManager, ["generate"]);
|
|
879
915
|
await execa(generateArgs.command, generateArgs.args, {
|
|
880
|
-
cwd:
|
|
916
|
+
cwd: prismaProjectDir,
|
|
881
917
|
stdio: context.verbose ? "inherit" : "pipe"
|
|
882
918
|
});
|
|
883
919
|
if (context.verbose) log.success("Prisma Client generated.");
|
|
@@ -904,16 +940,17 @@ function buildNextStepsForContext(opts) {
|
|
|
904
940
|
if (!context.shouldInstall) nextSteps.push(`- ${getInstallCommand(context.packageManager)}`);
|
|
905
941
|
if (!didGenerateClient || !context.shouldGenerate) nextSteps.push(`- ${getRunScriptCommand(context.packageManager, "db:generate")}`);
|
|
906
942
|
nextSteps.push(`- ${getRunScriptCommand(context.packageManager, "db:migrate")}`);
|
|
943
|
+
nextSteps.push(`- ${getRunScriptCommand(context.packageManager, "db:seed")}`);
|
|
907
944
|
if (options.includeDevNextStep) nextSteps.push(`- ${getRunScriptCommand(context.packageManager, "dev")}`);
|
|
908
945
|
return nextSteps;
|
|
909
946
|
}
|
|
910
|
-
async function
|
|
947
|
+
async function executePrismaSetupContext(context, options = {}) {
|
|
911
948
|
const projectDir = path.resolve(options.projectDir ?? context.projectDir);
|
|
912
949
|
const provisionResult = await provisionPrismaPostgresIfNeeded(context, projectDir);
|
|
913
950
|
if (!provisionResult) return;
|
|
914
951
|
if (!await writeDependenciesForContext(context, projectDir)) return;
|
|
915
952
|
if (!await installDependenciesForContext(context, projectDir)) return;
|
|
916
|
-
if (!await
|
|
953
|
+
if (!await finalizePrismaFilesForContext(context, projectDir, provisionResult)) return;
|
|
917
954
|
const generateResult = await generatePrismaClientForContext(context, projectDir);
|
|
918
955
|
const warningLines = buildWarningLines(provisionResult.warning, generateResult.warning);
|
|
919
956
|
const nextSteps = buildNextStepsForContext({
|
|
@@ -927,15 +964,540 @@ Next steps:
|
|
|
927
964
|
${nextSteps.join("\n")}`);
|
|
928
965
|
return { packageManager: context.packageManager };
|
|
929
966
|
}
|
|
930
|
-
|
|
967
|
+
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/tasks/setup-addons.ts
|
|
970
|
+
const DEFAULT_ADDON_SCOPE = "project";
|
|
971
|
+
const DEFAULT_SKILLS_AGENTS = [
|
|
972
|
+
"claude-code",
|
|
973
|
+
"codex",
|
|
974
|
+
"cursor"
|
|
975
|
+
];
|
|
976
|
+
const DEFAULT_MCP_AGENTS = [
|
|
977
|
+
"claude-code",
|
|
978
|
+
"codex",
|
|
979
|
+
"cursor"
|
|
980
|
+
];
|
|
981
|
+
const DEFAULT_EXTENSION_TARGETS = ["vscode", "cursor"];
|
|
982
|
+
const PRISMA_MCP_SERVER = "https://mcp.prisma.io/mcp";
|
|
983
|
+
const ADDON_OPTIONS = [
|
|
984
|
+
{
|
|
985
|
+
value: "skills",
|
|
986
|
+
label: "Skills",
|
|
987
|
+
hint: "Install curated Prisma skills to your selected coding agents"
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
value: "mcp",
|
|
991
|
+
label: "MCP",
|
|
992
|
+
hint: "Configure Prisma MCP server in agent MCP config files"
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
value: "extension",
|
|
996
|
+
label: "IDE Extension",
|
|
997
|
+
hint: "Install Prisma extension in selected IDEs (VS Code, Cursor, Windsurf)"
|
|
998
|
+
}
|
|
999
|
+
];
|
|
1000
|
+
const SKILLS_AGENT_OPTIONS = [
|
|
1001
|
+
{
|
|
1002
|
+
value: "cursor",
|
|
1003
|
+
label: "Cursor"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
value: "claude-code",
|
|
1007
|
+
label: "Claude Code"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
value: "cline",
|
|
1011
|
+
label: "Cline"
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
value: "github-copilot",
|
|
1015
|
+
label: "GitHub Copilot"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
value: "codex",
|
|
1019
|
+
label: "Codex"
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
value: "opencode",
|
|
1023
|
+
label: "OpenCode"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
value: "windsurf",
|
|
1027
|
+
label: "Windsurf"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
value: "goose",
|
|
1031
|
+
label: "Goose"
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
value: "roo",
|
|
1035
|
+
label: "Roo Code"
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
value: "kilo",
|
|
1039
|
+
label: "Kilo Code"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
value: "gemini-cli",
|
|
1043
|
+
label: "Gemini CLI"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
value: "antigravity",
|
|
1047
|
+
label: "Antigravity"
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
value: "openhands",
|
|
1051
|
+
label: "OpenHands"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
value: "trae",
|
|
1055
|
+
label: "Trae"
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
value: "amp",
|
|
1059
|
+
label: "Amp"
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
value: "pi",
|
|
1063
|
+
label: "Pi"
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
value: "qoder",
|
|
1067
|
+
label: "Qoder"
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
value: "qwen-code",
|
|
1071
|
+
label: "Qwen Code"
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
value: "kiro-cli",
|
|
1075
|
+
label: "Kiro CLI"
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
value: "droid",
|
|
1079
|
+
label: "Droid"
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
value: "command-code",
|
|
1083
|
+
label: "Command Code"
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
value: "clawdbot",
|
|
1087
|
+
label: "Clawdbot"
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
value: "zencoder",
|
|
1091
|
+
label: "Zencoder"
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
value: "neovate",
|
|
1095
|
+
label: "Neovate"
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
value: "mcpjam",
|
|
1099
|
+
label: "MCPJam"
|
|
1100
|
+
}
|
|
1101
|
+
];
|
|
1102
|
+
const MCP_AGENT_OPTIONS = [
|
|
1103
|
+
{
|
|
1104
|
+
value: "claude-code",
|
|
1105
|
+
label: "Claude Code"
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
value: "codex",
|
|
1109
|
+
label: "Codex"
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
value: "cursor",
|
|
1113
|
+
label: "Cursor"
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
value: "vscode",
|
|
1117
|
+
label: "VS Code"
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
value: "github-copilot-cli",
|
|
1121
|
+
label: "GitHub Copilot CLI"
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
value: "opencode",
|
|
1125
|
+
label: "OpenCode"
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
value: "gemini-cli",
|
|
1129
|
+
label: "Gemini CLI"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
value: "goose",
|
|
1133
|
+
label: "Goose"
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
value: "zed",
|
|
1137
|
+
label: "Zed"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
value: "antigravity",
|
|
1141
|
+
label: "Antigravity"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
value: "cline",
|
|
1145
|
+
label: "Cline VS Code Extension"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
value: "cline-cli",
|
|
1149
|
+
label: "Cline CLI"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
value: "claude-desktop",
|
|
1153
|
+
label: "Claude Desktop"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
value: "mcporter",
|
|
1157
|
+
label: "MCPorter"
|
|
1158
|
+
}
|
|
1159
|
+
];
|
|
1160
|
+
const SHARED_PRISMA_SKILLS = [
|
|
1161
|
+
"prisma-cli",
|
|
1162
|
+
"prisma-client-api",
|
|
1163
|
+
"prisma-database-setup",
|
|
1164
|
+
"prisma-upgrade-v7"
|
|
1165
|
+
];
|
|
1166
|
+
function getAvailablePrismaSkills(provider) {
|
|
1167
|
+
if (provider === "postgresql") return [...SHARED_PRISMA_SKILLS, "prisma-postgres"];
|
|
1168
|
+
return [...SHARED_PRISMA_SKILLS];
|
|
1169
|
+
}
|
|
1170
|
+
function getSkillOptions(provider) {
|
|
1171
|
+
const available = getAvailablePrismaSkills(provider);
|
|
1172
|
+
const options = {
|
|
1173
|
+
"prisma-cli": {
|
|
1174
|
+
value: "prisma-cli",
|
|
1175
|
+
label: "prisma-cli",
|
|
1176
|
+
hint: "Prisma CLI reference"
|
|
1177
|
+
},
|
|
1178
|
+
"prisma-client-api": {
|
|
1179
|
+
value: "prisma-client-api",
|
|
1180
|
+
label: "prisma-client-api",
|
|
1181
|
+
hint: "Prisma Client query patterns"
|
|
1182
|
+
},
|
|
1183
|
+
"prisma-database-setup": {
|
|
1184
|
+
value: "prisma-database-setup",
|
|
1185
|
+
label: "prisma-database-setup",
|
|
1186
|
+
hint: "Database provider setup guides"
|
|
1187
|
+
},
|
|
1188
|
+
"prisma-upgrade-v7": {
|
|
1189
|
+
value: "prisma-upgrade-v7",
|
|
1190
|
+
label: "prisma-upgrade-v7",
|
|
1191
|
+
hint: "v6 to v7 migration guide"
|
|
1192
|
+
},
|
|
1193
|
+
"prisma-postgres": {
|
|
1194
|
+
value: "prisma-postgres",
|
|
1195
|
+
label: "prisma-postgres",
|
|
1196
|
+
hint: "Prisma Postgres workflows"
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
return available.map((skill) => options[skill]);
|
|
1200
|
+
}
|
|
1201
|
+
function collectAddonsFromInput(input) {
|
|
1202
|
+
const addons = [];
|
|
1203
|
+
if (input.skills === true) addons.push("skills");
|
|
1204
|
+
if (input.mcp === true) addons.push("mcp");
|
|
1205
|
+
if (input.extension === true) addons.push("extension");
|
|
1206
|
+
return uniqueValues(addons);
|
|
1207
|
+
}
|
|
1208
|
+
const EXTENSION_TARGET_OPTIONS = [
|
|
1209
|
+
{
|
|
1210
|
+
value: "vscode",
|
|
1211
|
+
label: "VS Code",
|
|
1212
|
+
hint: "Uses the `code` CLI"
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
value: "cursor",
|
|
1216
|
+
label: "Cursor",
|
|
1217
|
+
hint: "Uses the `cursor` CLI"
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
value: "windsurf",
|
|
1221
|
+
label: "Windsurf",
|
|
1222
|
+
hint: "Uses the `windsurf` CLI"
|
|
1223
|
+
}
|
|
1224
|
+
];
|
|
1225
|
+
function uniqueValues(values) {
|
|
1226
|
+
return Array.from(new Set(values));
|
|
1227
|
+
}
|
|
1228
|
+
function getRecommendedPrismaSkills(provider, shouldUsePrismaPostgres) {
|
|
1229
|
+
const skills = [...getAvailablePrismaSkills(provider)];
|
|
1230
|
+
if (!shouldUsePrismaPostgres) return skills.filter((skill) => skill !== "prisma-postgres");
|
|
1231
|
+
return uniqueValues(skills);
|
|
1232
|
+
}
|
|
1233
|
+
async function promptForAddons() {
|
|
1234
|
+
const selectedAddons = await multiselect({
|
|
1235
|
+
message: "Select add-ons (optional)",
|
|
1236
|
+
options: ADDON_OPTIONS,
|
|
1237
|
+
required: false
|
|
1238
|
+
});
|
|
1239
|
+
if (isCancel(selectedAddons)) {
|
|
1240
|
+
cancel("Operation cancelled.");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
return uniqueValues(selectedAddons);
|
|
1244
|
+
}
|
|
1245
|
+
async function promptForAddonScope() {
|
|
1246
|
+
const selectedScope = await select({
|
|
1247
|
+
message: "Where should add-ons write config?",
|
|
1248
|
+
initialValue: DEFAULT_ADDON_SCOPE,
|
|
1249
|
+
options: [{
|
|
1250
|
+
value: "project",
|
|
1251
|
+
label: "Project",
|
|
1252
|
+
hint: "Recommended for teams (checked into the project when applicable)"
|
|
1253
|
+
}, {
|
|
1254
|
+
value: "global",
|
|
1255
|
+
label: "Global",
|
|
1256
|
+
hint: "Personal machine-level setup"
|
|
1257
|
+
}]
|
|
1258
|
+
});
|
|
1259
|
+
if (isCancel(selectedScope)) {
|
|
1260
|
+
cancel("Operation cancelled.");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
return selectedScope;
|
|
1264
|
+
}
|
|
1265
|
+
async function promptForPrismaSkills(provider, recommendedSkills) {
|
|
1266
|
+
const options = getSkillOptions(provider);
|
|
1267
|
+
const optionValues = new Set(options.map((option) => option.value));
|
|
1268
|
+
const selectedSkills = await multiselect({
|
|
1269
|
+
message: "Select Prisma skills",
|
|
1270
|
+
options,
|
|
1271
|
+
required: false,
|
|
1272
|
+
initialValues: recommendedSkills.filter((skill) => optionValues.has(skill))
|
|
1273
|
+
});
|
|
1274
|
+
if (isCancel(selectedSkills)) {
|
|
1275
|
+
cancel("Operation cancelled.");
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
return uniqueValues(selectedSkills);
|
|
1279
|
+
}
|
|
1280
|
+
async function promptForSkillsAgents() {
|
|
1281
|
+
const selectedAgents = await multiselect({
|
|
1282
|
+
message: "Select agents for skills",
|
|
1283
|
+
options: SKILLS_AGENT_OPTIONS,
|
|
1284
|
+
required: false,
|
|
1285
|
+
initialValues: [...DEFAULT_SKILLS_AGENTS]
|
|
1286
|
+
});
|
|
1287
|
+
if (isCancel(selectedAgents)) {
|
|
1288
|
+
cancel("Operation cancelled.");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
return uniqueValues(selectedAgents);
|
|
1292
|
+
}
|
|
1293
|
+
async function promptForMcpAgents() {
|
|
1294
|
+
const selectedAgents = await multiselect({
|
|
1295
|
+
message: "Select agents for MCP",
|
|
1296
|
+
options: MCP_AGENT_OPTIONS,
|
|
1297
|
+
required: false,
|
|
1298
|
+
initialValues: [...DEFAULT_MCP_AGENTS]
|
|
1299
|
+
});
|
|
1300
|
+
if (isCancel(selectedAgents)) {
|
|
1301
|
+
cancel("Operation cancelled.");
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
return uniqueValues(selectedAgents);
|
|
1305
|
+
}
|
|
1306
|
+
async function promptForExtensionTargets() {
|
|
1307
|
+
const selectedTargets = await multiselect({
|
|
1308
|
+
message: "Select IDEs for extension install",
|
|
1309
|
+
options: EXTENSION_TARGET_OPTIONS,
|
|
1310
|
+
required: false,
|
|
1311
|
+
initialValues: DEFAULT_EXTENSION_TARGETS
|
|
1312
|
+
});
|
|
1313
|
+
if (isCancel(selectedTargets)) {
|
|
1314
|
+
cancel("Operation cancelled.");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
return uniqueValues(selectedTargets);
|
|
1318
|
+
}
|
|
1319
|
+
async function collectCreateAddonSetupContext(input, options) {
|
|
1320
|
+
const hasExplicitAddonSelection = input.skills !== void 0 || input.mcp !== void 0 || input.extension !== void 0;
|
|
1321
|
+
const selectedFromInput = collectAddonsFromInput(input);
|
|
1322
|
+
const selectedAddons = selectedFromInput.length > 0 ? selectedFromInput : hasExplicitAddonSelection ? [] : options.useDefaults ? [] : await promptForAddons();
|
|
1323
|
+
if (!selectedAddons) return;
|
|
1324
|
+
const addons = uniqueValues(selectedAddons);
|
|
1325
|
+
if (addons.length === 0) return null;
|
|
1326
|
+
const scope = addons.includes("skills") || addons.includes("mcp") ? options.useDefaults ? DEFAULT_ADDON_SCOPE : await promptForAddonScope() : DEFAULT_ADDON_SCOPE;
|
|
1327
|
+
if (!scope) return;
|
|
1328
|
+
const recommendedSkills = getRecommendedPrismaSkills(options.provider, options.shouldUsePrismaPostgres);
|
|
1329
|
+
const skills = !addons.includes("skills") ? [] : options.useDefaults ? recommendedSkills : await promptForPrismaSkills(options.provider, recommendedSkills);
|
|
1330
|
+
if (!skills) return;
|
|
1331
|
+
const skillsAgents = !addons.includes("skills") ? [] : options.useDefaults ? [...DEFAULT_SKILLS_AGENTS] : await promptForSkillsAgents();
|
|
1332
|
+
if (!skillsAgents) return;
|
|
1333
|
+
const mcpAgents = !addons.includes("mcp") ? [] : options.useDefaults ? [...DEFAULT_MCP_AGENTS] : await promptForMcpAgents();
|
|
1334
|
+
if (!mcpAgents) return;
|
|
1335
|
+
const extensionTargets = !addons.includes("extension") ? [] : options.useDefaults ? [...DEFAULT_EXTENSION_TARGETS] : await promptForExtensionTargets();
|
|
1336
|
+
if (!extensionTargets) return;
|
|
1337
|
+
return {
|
|
1338
|
+
addons,
|
|
1339
|
+
scope,
|
|
1340
|
+
skills,
|
|
1341
|
+
skillsAgents: uniqueValues(skillsAgents),
|
|
1342
|
+
mcpAgents: uniqueValues(mcpAgents),
|
|
1343
|
+
extensionTargets: uniqueValues(extensionTargets)
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
async function executeExternalCommand(params) {
|
|
1347
|
+
await execa(params.command, params.args, {
|
|
1348
|
+
cwd: params.cwd,
|
|
1349
|
+
stdio: params.verbose ? "inherit" : "pipe",
|
|
1350
|
+
env: {
|
|
1351
|
+
...process.env,
|
|
1352
|
+
CI: "true"
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
async function installSkillsAddon(params) {
|
|
1357
|
+
if (params.agents.length === 0 || params.skills.length === 0) return "Skipped skills addon because no skills or agents were selected.";
|
|
1358
|
+
const scopeArgs = params.scope === "global" ? ["-g"] : [];
|
|
1359
|
+
const skillArgs = params.skills.flatMap((skill) => ["-s", skill]);
|
|
1360
|
+
const agentArgs = params.agents.flatMap((agent) => ["-a", agent]);
|
|
1361
|
+
const commandArgs = [
|
|
1362
|
+
"skills@latest",
|
|
1363
|
+
"add",
|
|
1364
|
+
"prisma/skills",
|
|
1365
|
+
...scopeArgs,
|
|
1366
|
+
...skillArgs,
|
|
1367
|
+
...agentArgs,
|
|
1368
|
+
"-y"
|
|
1369
|
+
];
|
|
1370
|
+
const execution = getPackageExecutionArgs(params.packageManager, commandArgs);
|
|
931
1371
|
try {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1372
|
+
await executeExternalCommand({
|
|
1373
|
+
command: execution.command,
|
|
1374
|
+
args: execution.args,
|
|
1375
|
+
cwd: params.projectDir,
|
|
1376
|
+
verbose: params.verbose
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
return `Skills addon failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
async function installMcpAddon(params) {
|
|
1384
|
+
if (params.agents.length === 0) return "Skipped MCP addon because no agents were selected.";
|
|
1385
|
+
const scopeArgs = params.scope === "global" ? ["-g"] : [];
|
|
1386
|
+
const agentArgs = params.agents.flatMap((agent) => ["-a", agent]);
|
|
1387
|
+
const commandArgs = [
|
|
1388
|
+
"add-mcp@latest",
|
|
1389
|
+
PRISMA_MCP_SERVER,
|
|
1390
|
+
...scopeArgs,
|
|
1391
|
+
...agentArgs,
|
|
1392
|
+
"--name",
|
|
1393
|
+
"prisma",
|
|
1394
|
+
"--gitignore",
|
|
1395
|
+
"-y"
|
|
1396
|
+
];
|
|
1397
|
+
const execution = getPackageExecutionArgs(params.packageManager, commandArgs);
|
|
1398
|
+
try {
|
|
1399
|
+
await executeExternalCommand({
|
|
1400
|
+
command: execution.command,
|
|
1401
|
+
args: execution.args,
|
|
1402
|
+
cwd: params.projectDir,
|
|
1403
|
+
verbose: params.verbose
|
|
1404
|
+
});
|
|
1405
|
+
return;
|
|
935
1406
|
} catch (error) {
|
|
936
|
-
|
|
1407
|
+
return `MCP addon failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function getExtensionInstallBinary(target) {
|
|
1411
|
+
switch (target) {
|
|
1412
|
+
case "vscode": return "code";
|
|
1413
|
+
case "cursor": return "cursor";
|
|
1414
|
+
case "windsurf": return "windsurf";
|
|
1415
|
+
default: {
|
|
1416
|
+
const exhaustiveCheck = target;
|
|
1417
|
+
throw new Error(`Unsupported extension target: ${String(exhaustiveCheck)}`);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async function installExtensionAddon(params) {
|
|
1422
|
+
if (params.targets.length === 0) return ["Skipped extension addon because no IDE targets were selected."];
|
|
1423
|
+
const warnings = [];
|
|
1424
|
+
for (const target of params.targets) {
|
|
1425
|
+
const binary = getExtensionInstallBinary(target);
|
|
1426
|
+
try {
|
|
1427
|
+
await executeExternalCommand({
|
|
1428
|
+
command: binary,
|
|
1429
|
+
args: ["--version"],
|
|
1430
|
+
cwd: params.projectDir,
|
|
1431
|
+
verbose: false
|
|
1432
|
+
});
|
|
1433
|
+
} catch {
|
|
1434
|
+
warnings.push(`Skipped ${target} extension install because the \`${binary}\` CLI is not available.`);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
await executeExternalCommand({
|
|
1439
|
+
command: binary,
|
|
1440
|
+
args: [
|
|
1441
|
+
"--install-extension",
|
|
1442
|
+
"Prisma.prisma",
|
|
1443
|
+
"--force"
|
|
1444
|
+
],
|
|
1445
|
+
cwd: params.projectDir,
|
|
1446
|
+
verbose: params.verbose
|
|
1447
|
+
});
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
warnings.push(`${target} extension install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return warnings;
|
|
1453
|
+
}
|
|
1454
|
+
async function executeCreateAddonSetupContext(params) {
|
|
1455
|
+
const { context, packageManager, projectDir, verbose } = params;
|
|
1456
|
+
const addonSpinner = spinner();
|
|
1457
|
+
addonSpinner.start("Applying selected add-ons...");
|
|
1458
|
+
const warnings = [];
|
|
1459
|
+
if (context.addons.includes("skills")) {
|
|
1460
|
+
const warning = await installSkillsAddon({
|
|
1461
|
+
packageManager,
|
|
1462
|
+
projectDir,
|
|
1463
|
+
scope: context.scope,
|
|
1464
|
+
skills: context.skills,
|
|
1465
|
+
agents: context.skillsAgents,
|
|
1466
|
+
verbose
|
|
1467
|
+
});
|
|
1468
|
+
if (warning) warnings.push(warning);
|
|
1469
|
+
}
|
|
1470
|
+
if (context.addons.includes("mcp")) {
|
|
1471
|
+
const warning = await installMcpAddon({
|
|
1472
|
+
packageManager,
|
|
1473
|
+
projectDir,
|
|
1474
|
+
scope: context.scope,
|
|
1475
|
+
agents: context.mcpAgents,
|
|
1476
|
+
verbose
|
|
1477
|
+
});
|
|
1478
|
+
if (warning) warnings.push(warning);
|
|
1479
|
+
}
|
|
1480
|
+
if (context.addons.includes("extension")) {
|
|
1481
|
+
const extensionWarnings = await installExtensionAddon({
|
|
1482
|
+
projectDir,
|
|
1483
|
+
verbose,
|
|
1484
|
+
targets: context.extensionTargets
|
|
1485
|
+
});
|
|
1486
|
+
warnings.push(...extensionWarnings);
|
|
1487
|
+
}
|
|
1488
|
+
if (warnings.length > 0) {
|
|
1489
|
+
addonSpinner.stop("Add-ons applied with warnings.");
|
|
1490
|
+
for (const warning of warnings) log.warn(warning);
|
|
937
1491
|
return;
|
|
938
1492
|
}
|
|
1493
|
+
addonSpinner.stop("Add-ons applied.");
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
//#endregion
|
|
1497
|
+
//#region src/ui/branding.ts
|
|
1498
|
+
const prismaTitle = `${styleText(["bold", "cyan"], "Create")} ${styleText(["bold", "magenta"], "Prisma")}`;
|
|
1499
|
+
function getCreatePrismaIntro() {
|
|
1500
|
+
return prismaTitle;
|
|
939
1501
|
}
|
|
940
1502
|
|
|
941
1503
|
//#endregion
|
|
@@ -972,15 +1534,38 @@ async function promptForCreateTemplate() {
|
|
|
972
1534
|
const template = await select({
|
|
973
1535
|
message: "Select template",
|
|
974
1536
|
initialValue: DEFAULT_TEMPLATE,
|
|
975
|
-
options: [
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1537
|
+
options: [
|
|
1538
|
+
{
|
|
1539
|
+
value: "hono",
|
|
1540
|
+
label: "Hono",
|
|
1541
|
+
hint: "TypeScript API starter"
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
value: "next",
|
|
1545
|
+
label: "Next.js",
|
|
1546
|
+
hint: "App Router + TypeScript starter"
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
value: "svelte",
|
|
1550
|
+
label: "SvelteKit",
|
|
1551
|
+
hint: "Official minimal SvelteKit + TypeScript starter"
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
value: "astro",
|
|
1555
|
+
label: "Astro",
|
|
1556
|
+
hint: "Official minimal Astro starter with API route example"
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
value: "nuxt",
|
|
1560
|
+
label: "Nuxt",
|
|
1561
|
+
hint: "Official minimal Nuxt starter with Nitro API route example"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
value: "turborepo",
|
|
1565
|
+
label: "Turborepo",
|
|
1566
|
+
hint: "Monorepo starter with apps + packages/db Prisma package"
|
|
1567
|
+
}
|
|
1568
|
+
]
|
|
984
1569
|
});
|
|
985
1570
|
if (isCancel(template)) {
|
|
986
1571
|
cancel("Operation cancelled.");
|
|
@@ -1023,7 +1608,6 @@ async function collectCreateContext(input) {
|
|
|
1023
1608
|
if (!projectName) return;
|
|
1024
1609
|
const template = input.template ?? (useDefaults ? DEFAULT_TEMPLATE : await promptForCreateTemplate());
|
|
1025
1610
|
if (!template) return;
|
|
1026
|
-
const schemaPreset = input.schemaPreset ?? DEFAULT_SCHEMA_PRESET;
|
|
1027
1611
|
const targetDirectory = path.resolve(process.cwd(), projectName);
|
|
1028
1612
|
const targetPathState = await inspectTargetPath(targetDirectory);
|
|
1029
1613
|
if (targetPathState.exists && !targetPathState.isDirectory) {
|
|
@@ -1034,29 +1618,26 @@ async function collectCreateContext(input) {
|
|
|
1034
1618
|
cancel(`Target directory ${formatPathForDisplay(targetDirectory)} is not empty. Use --force to continue.`);
|
|
1035
1619
|
return;
|
|
1036
1620
|
}
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
provider: input.provider,
|
|
1041
|
-
packageManager: input.packageManager,
|
|
1042
|
-
prismaPostgres: input.prismaPostgres,
|
|
1043
|
-
databaseUrl: input.databaseUrl,
|
|
1044
|
-
install: input.install,
|
|
1045
|
-
generate: input.generate,
|
|
1046
|
-
schemaPreset
|
|
1047
|
-
}, {
|
|
1048
|
-
skipIntro: true,
|
|
1049
|
-
projectDir: targetDirectory
|
|
1621
|
+
const prismaSetupContext = await collectPrismaSetupContext(input, {
|
|
1622
|
+
projectDir: targetDirectory,
|
|
1623
|
+
defaultSchemaPreset: DEFAULT_SCHEMA_PRESET
|
|
1050
1624
|
});
|
|
1051
|
-
if (!
|
|
1625
|
+
if (!prismaSetupContext) return;
|
|
1626
|
+
const addonSetupContext = await collectCreateAddonSetupContext(input, {
|
|
1627
|
+
useDefaults,
|
|
1628
|
+
provider: prismaSetupContext.databaseProvider,
|
|
1629
|
+
shouldUsePrismaPostgres: prismaSetupContext.shouldUsePrismaPostgres
|
|
1630
|
+
});
|
|
1631
|
+
if (addonSetupContext === void 0) return;
|
|
1052
1632
|
return {
|
|
1053
1633
|
targetDirectory,
|
|
1054
1634
|
targetPathState,
|
|
1055
1635
|
force,
|
|
1056
1636
|
template,
|
|
1057
|
-
schemaPreset,
|
|
1637
|
+
schemaPreset: prismaSetupContext.schemaPreset,
|
|
1058
1638
|
projectPackageName: toPackageName(path.basename(targetDirectory)),
|
|
1059
|
-
|
|
1639
|
+
prismaSetupContext,
|
|
1640
|
+
addonSetupContext: addonSetupContext ?? void 0
|
|
1060
1641
|
};
|
|
1061
1642
|
}
|
|
1062
1643
|
async function executeCreateContext(context) {
|
|
@@ -1068,7 +1649,8 @@ async function executeCreateContext(context) {
|
|
|
1068
1649
|
projectName: context.projectPackageName,
|
|
1069
1650
|
template: context.template,
|
|
1070
1651
|
schemaPreset: context.schemaPreset,
|
|
1071
|
-
|
|
1652
|
+
provider: context.prismaSetupContext.databaseProvider,
|
|
1653
|
+
packageManager: context.prismaSetupContext.packageManager
|
|
1072
1654
|
});
|
|
1073
1655
|
scaffoldSpinner.stop("Project files scaffolded.");
|
|
1074
1656
|
} catch (error) {
|
|
@@ -1078,8 +1660,13 @@ async function executeCreateContext(context) {
|
|
|
1078
1660
|
}
|
|
1079
1661
|
if (context.targetPathState.exists && !context.targetPathState.isEmptyDirectory && context.force) log.warn(`Used --force in non-empty directory ${formatPathForDisplay(context.targetDirectory)}.`);
|
|
1080
1662
|
const cdStep = `- cd ${formatPathForDisplay(context.targetDirectory)}`;
|
|
1081
|
-
|
|
1082
|
-
|
|
1663
|
+
if (context.addonSetupContext) await executeCreateAddonSetupContext({
|
|
1664
|
+
context: context.addonSetupContext,
|
|
1665
|
+
packageManager: context.prismaSetupContext.packageManager,
|
|
1666
|
+
projectDir: context.targetDirectory,
|
|
1667
|
+
verbose: context.prismaSetupContext.verbose
|
|
1668
|
+
});
|
|
1669
|
+
await executePrismaSetupContext(context.prismaSetupContext, {
|
|
1083
1670
|
prependNextSteps: [cdStep],
|
|
1084
1671
|
projectDir: context.targetDirectory,
|
|
1085
1672
|
includeDevNextStep: true
|
|
@@ -1087,4 +1674,4 @@ async function executeCreateContext(context) {
|
|
|
1087
1674
|
}
|
|
1088
1675
|
|
|
1089
1676
|
//#endregion
|
|
1090
|
-
export {
|
|
1677
|
+
export { DatabaseUrlSchema as a, DatabaseProviderSchema as i, CreateCommandInputSchema as n, PackageManagerSchema as o, CreateTemplateSchema as r, SchemaPresetSchema as s, runCreateCommand as t };
|