create-prisma 0.0.0 → 0.1.3
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/CHANGELOG.md +3 -0
- package/README.md +128 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +9 -0
- package/dist/create-fJECj1B0.mjs +1026 -0
- package/dist/index.d.mts +319 -0
- package/dist/index.mjs +38 -0
- package/package.json +63 -3
- package/templates/create/hono/README.md.hbs +28 -0
- package/templates/create/hono/package.json.hbs +19 -0
- package/templates/create/hono/src/index.ts.hbs +34 -0
- package/templates/create/hono/tsconfig.json +15 -0
- package/templates/create/next/README.md.hbs +27 -0
- package/templates/create/next/app/globals.css +47 -0
- package/templates/create/next/app/layout.tsx.hbs +19 -0
- package/templates/create/next/app/page.tsx.hbs +40 -0
- package/templates/create/next/eslint.config.mjs +16 -0
- package/templates/create/next/next.config.ts +7 -0
- package/templates/create/next/package.json.hbs +25 -0
- package/templates/create/next/tsconfig.json +34 -0
- package/templates/init/prisma/index.ts.hbs +44 -0
- package/templates/init/prisma/schema.prisma.hbs +18 -0
- package/templates/init/prisma.config.ts.hbs +12 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import Handlebars from "handlebars";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { execa } from "execa";
|
|
10
|
+
|
|
11
|
+
//#region src/templates/shared.ts
|
|
12
|
+
function findPackageRoot(startDir) {
|
|
13
|
+
let currentDir = startDir;
|
|
14
|
+
while (true) {
|
|
15
|
+
if (existsSync(path.join(currentDir, "package.json"))) return currentDir;
|
|
16
|
+
const parentDir = path.dirname(currentDir);
|
|
17
|
+
if (parentDir === currentDir) break;
|
|
18
|
+
currentDir = parentDir;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unable to locate package root from: ${startDir}`);
|
|
21
|
+
}
|
|
22
|
+
function resolveTemplatesDir(relativeTemplatesDir) {
|
|
23
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
24
|
+
const packageRoot = findPackageRoot(path.dirname(currentFilePath));
|
|
25
|
+
const templatePath = path.join(packageRoot, relativeTemplatesDir);
|
|
26
|
+
if (!existsSync(templatePath)) throw new Error(`Template directory not found at: ${templatePath}`);
|
|
27
|
+
return templatePath;
|
|
28
|
+
}
|
|
29
|
+
async function getTemplateFilesRecursively(dir) {
|
|
30
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
31
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
32
|
+
const entryPath = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) return getTemplateFilesRecursively(entryPath);
|
|
34
|
+
if (!entry.isFile()) return [];
|
|
35
|
+
return [entryPath];
|
|
36
|
+
}))).flat();
|
|
37
|
+
}
|
|
38
|
+
function stripHbsExtension(filePath) {
|
|
39
|
+
return filePath.endsWith(".hbs") ? filePath.slice(0, -4) : filePath;
|
|
40
|
+
}
|
|
41
|
+
function ensureTrailingNewline(content) {
|
|
42
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
43
|
+
}
|
|
44
|
+
async function renderTemplateTree(opts) {
|
|
45
|
+
const { templateRoot, outputDir, context } = opts;
|
|
46
|
+
const templateFiles = await getTemplateFilesRecursively(templateRoot);
|
|
47
|
+
for (const templateFilePath of templateFiles) {
|
|
48
|
+
const relativeTemplatePath = path.relative(templateRoot, templateFilePath);
|
|
49
|
+
const relativeOutputPath = stripHbsExtension(relativeTemplatePath);
|
|
50
|
+
const outputPath = path.join(outputDir, relativeOutputPath);
|
|
51
|
+
const templateContent = await fs.readFile(templateFilePath, "utf8");
|
|
52
|
+
const outputContent = relativeTemplatePath.endsWith(".hbs") ? Handlebars.compile(templateContent, {
|
|
53
|
+
noEscape: true,
|
|
54
|
+
strict: true
|
|
55
|
+
})(context) : templateContent;
|
|
56
|
+
await fs.outputFile(outputPath, ensureTrailingNewline(outputContent), "utf8");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/templates/render-create-template.ts
|
|
62
|
+
function getCreateTemplateDir(template) {
|
|
63
|
+
return resolveTemplatesDir(`templates/create/${template}`);
|
|
64
|
+
}
|
|
65
|
+
function createTemplateContext$1(projectName, schemaPreset) {
|
|
66
|
+
return {
|
|
67
|
+
projectName,
|
|
68
|
+
schemaPreset,
|
|
69
|
+
useBasicSchema: schemaPreset === "basic"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function scaffoldCreateTemplate(opts) {
|
|
73
|
+
const { projectDir, projectName, template, schemaPreset } = opts;
|
|
74
|
+
await renderTemplateTree({
|
|
75
|
+
templateRoot: getCreateTemplateDir(template),
|
|
76
|
+
outputDir: projectDir,
|
|
77
|
+
context: createTemplateContext$1(projectName, schemaPreset)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/types.ts
|
|
83
|
+
const databaseProviders = [
|
|
84
|
+
"postgresql",
|
|
85
|
+
"mysql",
|
|
86
|
+
"sqlite",
|
|
87
|
+
"sqlserver",
|
|
88
|
+
"cockroachdb"
|
|
89
|
+
];
|
|
90
|
+
const packageManagers = [
|
|
91
|
+
"npm",
|
|
92
|
+
"pnpm",
|
|
93
|
+
"bun"
|
|
94
|
+
];
|
|
95
|
+
const schemaPresets = ["empty", "basic"];
|
|
96
|
+
const createTemplates = ["hono", "next"];
|
|
97
|
+
const DatabaseProviderSchema = z.enum(databaseProviders);
|
|
98
|
+
const PackageManagerSchema = z.enum(packageManagers);
|
|
99
|
+
const SchemaPresetSchema = z.enum(schemaPresets);
|
|
100
|
+
const CreateTemplateSchema = z.enum(createTemplates);
|
|
101
|
+
const DatabaseUrlSchema = z.string().trim().min(1, "Please enter a valid database URL");
|
|
102
|
+
const CommonCommandOptionsSchema = z.object({
|
|
103
|
+
yes: z.boolean().optional().describe("Skip prompts and accept default choices"),
|
|
104
|
+
verbose: z.boolean().optional().describe("Show verbose command output during setup")
|
|
105
|
+
});
|
|
106
|
+
const PrismaSetupOptionsSchema = z.object({
|
|
107
|
+
provider: DatabaseProviderSchema.optional().describe("Database provider"),
|
|
108
|
+
packageManager: PackageManagerSchema.optional().describe("Package manager used for dependency installation"),
|
|
109
|
+
prismaPostgres: z.boolean().optional().describe("Provision Prisma Postgres with create-db when provider is postgresql"),
|
|
110
|
+
databaseUrl: DatabaseUrlSchema.optional().describe("DATABASE_URL value"),
|
|
111
|
+
install: z.boolean().optional().describe("Install dependencies with selected package manager"),
|
|
112
|
+
generate: z.boolean().optional().describe("Generate Prisma Client after scaffolding"),
|
|
113
|
+
schemaPreset: SchemaPresetSchema.optional().describe("Schema preset to scaffold in prisma/schema.prisma")
|
|
114
|
+
});
|
|
115
|
+
const InitCommandInputSchema = CommonCommandOptionsSchema.merge(PrismaSetupOptionsSchema);
|
|
116
|
+
const CreateScaffoldOptionsSchema = z.object({
|
|
117
|
+
name: z.string().trim().min(1, "Please enter a valid project name").optional().describe("Project name / directory"),
|
|
118
|
+
template: CreateTemplateSchema.optional().describe("Project template"),
|
|
119
|
+
force: z.boolean().optional().describe("Allow scaffolding into a non-empty target directory")
|
|
120
|
+
});
|
|
121
|
+
const CreateCommandInputSchema = CommonCommandOptionsSchema.merge(CreateScaffoldOptionsSchema).merge(PrismaSetupOptionsSchema);
|
|
122
|
+
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/utils/package-manager.ts
|
|
125
|
+
function parseUserAgent(userAgent) {
|
|
126
|
+
if (userAgent?.startsWith("pnpm")) return "pnpm";
|
|
127
|
+
if (userAgent?.startsWith("bun")) return "bun";
|
|
128
|
+
if (userAgent?.startsWith("npm")) return "npm";
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function parsePackageManagerField(packageManagerField) {
|
|
132
|
+
if (typeof packageManagerField !== "string" || packageManagerField.length === 0) return null;
|
|
133
|
+
const managerName = packageManagerField.split("@")[0];
|
|
134
|
+
const parsed = PackageManagerSchema.safeParse(managerName);
|
|
135
|
+
return parsed.success ? parsed.data : null;
|
|
136
|
+
}
|
|
137
|
+
async function detectFromPackageJson(projectDir) {
|
|
138
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
139
|
+
if (!await fs.pathExists(packageJsonPath)) return null;
|
|
140
|
+
return parsePackageManagerField((await fs.readJson(packageJsonPath)).packageManager);
|
|
141
|
+
}
|
|
142
|
+
async function detectFromLockfile(projectDir) {
|
|
143
|
+
for (const check of [
|
|
144
|
+
{
|
|
145
|
+
manager: "pnpm",
|
|
146
|
+
lockfile: "pnpm-lock.yaml"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
manager: "bun",
|
|
150
|
+
lockfile: "bun.lockb"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
manager: "bun",
|
|
154
|
+
lockfile: "bun.lock"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
manager: "npm",
|
|
158
|
+
lockfile: "package-lock.json"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
manager: "npm",
|
|
162
|
+
lockfile: "npm-shrinkwrap.json"
|
|
163
|
+
}
|
|
164
|
+
]) if (await fs.pathExists(path.join(projectDir, check.lockfile))) return check.manager;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
async function detectPackageManager(projectDir = process.cwd()) {
|
|
168
|
+
const fromPackageJson = await detectFromPackageJson(projectDir);
|
|
169
|
+
if (fromPackageJson) return fromPackageJson;
|
|
170
|
+
const fromLockfile = await detectFromLockfile(projectDir);
|
|
171
|
+
if (fromLockfile) return fromLockfile;
|
|
172
|
+
const fromUserAgent = parseUserAgent(process.env.npm_config_user_agent);
|
|
173
|
+
if (fromUserAgent) return fromUserAgent;
|
|
174
|
+
return "bun";
|
|
175
|
+
}
|
|
176
|
+
function getInstallCommand(packageManager) {
|
|
177
|
+
return `${packageManager} install`;
|
|
178
|
+
}
|
|
179
|
+
function getRunScriptCommand(packageManager, scriptName) {
|
|
180
|
+
switch (packageManager) {
|
|
181
|
+
case "bun": return `bun run ${scriptName}`;
|
|
182
|
+
case "pnpm": return `pnpm run ${scriptName}`;
|
|
183
|
+
default: return `npm run ${scriptName}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function getInstallArgs(packageManager) {
|
|
187
|
+
return {
|
|
188
|
+
command: packageManager,
|
|
189
|
+
args: ["install"]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function getPackageExecutor(packageManager) {
|
|
193
|
+
switch (packageManager) {
|
|
194
|
+
case "pnpm": return {
|
|
195
|
+
command: "pnpm",
|
|
196
|
+
args: ["dlx"]
|
|
197
|
+
};
|
|
198
|
+
case "bun": return {
|
|
199
|
+
command: "bunx",
|
|
200
|
+
args: []
|
|
201
|
+
};
|
|
202
|
+
default: return {
|
|
203
|
+
command: "npx",
|
|
204
|
+
args: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function getPackageExecutionArgs(packageManager, commandArgs) {
|
|
209
|
+
const executor = getPackageExecutor(packageManager);
|
|
210
|
+
return {
|
|
211
|
+
command: executor.command,
|
|
212
|
+
args: [...executor.args, ...commandArgs]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function getPackageExecutionCommand(packageManager, commandArgs) {
|
|
216
|
+
const execution = getPackageExecutionArgs(packageManager, commandArgs);
|
|
217
|
+
return [execution.command, ...execution.args].join(" ");
|
|
218
|
+
}
|
|
219
|
+
function getPrismaCliArgs(packageManager, prismaArgs) {
|
|
220
|
+
if (packageManager === "bun") return getPackageExecutionArgs(packageManager, [
|
|
221
|
+
"--bun",
|
|
222
|
+
"prisma",
|
|
223
|
+
...prismaArgs
|
|
224
|
+
]);
|
|
225
|
+
return getPackageExecutionArgs(packageManager, ["prisma", ...prismaArgs]);
|
|
226
|
+
}
|
|
227
|
+
function getPrismaCliCommand(packageManager, prismaArgs) {
|
|
228
|
+
const execution = getPrismaCliArgs(packageManager, prismaArgs);
|
|
229
|
+
return [execution.command, ...execution.args].join(" ");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/tasks/prisma-postgres.ts
|
|
234
|
+
const PRISMA_POSTGRES_TEMPORARY_NOTICE = "Prisma Postgres is temporary for 24 hours. Claim this database before it expires using CLAIM_URL.";
|
|
235
|
+
const CREATE_DB_COMMAND_ARGS = ["create-db@latest", "--json"];
|
|
236
|
+
function parseCreateDbJson(rawOutput) {
|
|
237
|
+
const trimmed = rawOutput.trim();
|
|
238
|
+
if (!trimmed) throw new Error("create-db returned empty output.");
|
|
239
|
+
const jsonCandidates = [trimmed];
|
|
240
|
+
const firstBrace = trimmed.indexOf("{");
|
|
241
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
242
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) jsonCandidates.push(trimmed.slice(firstBrace, lastBrace + 1));
|
|
243
|
+
for (const candidate of jsonCandidates) try {
|
|
244
|
+
return JSON.parse(candidate);
|
|
245
|
+
} catch {}
|
|
246
|
+
throw new Error(`Unable to parse create-db JSON output: ${trimmed}`);
|
|
247
|
+
}
|
|
248
|
+
function pickConnectionString(payload) {
|
|
249
|
+
if (typeof payload.connectionString === "string" && payload.connectionString.length > 0) return payload.connectionString;
|
|
250
|
+
if (typeof payload.databaseUrl === "string" && payload.databaseUrl.length > 0) return payload.databaseUrl;
|
|
251
|
+
}
|
|
252
|
+
function extractErrorMessage(payload, fallback) {
|
|
253
|
+
if (typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
254
|
+
if (typeof payload.error === "string" && payload.error.length > 0) return payload.error;
|
|
255
|
+
return fallback;
|
|
256
|
+
}
|
|
257
|
+
async function provisionPrismaPostgres(packageManager, projectDir = process.cwd()) {
|
|
258
|
+
const command = getPackageExecutionArgs(packageManager, [...CREATE_DB_COMMAND_ARGS]);
|
|
259
|
+
const commandString = getCreateDbCommand(packageManager);
|
|
260
|
+
let stdout;
|
|
261
|
+
try {
|
|
262
|
+
stdout = (await execa(command.command, command.args, {
|
|
263
|
+
cwd: projectDir,
|
|
264
|
+
stdio: "pipe"
|
|
265
|
+
})).stdout;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof Error && "stderr" in error) {
|
|
268
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
269
|
+
const message = stderr.length > 0 ? stderr : error.message;
|
|
270
|
+
throw new Error(`Failed to run ${commandString}: ${message}`);
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const payload = parseCreateDbJson(stdout);
|
|
275
|
+
if (payload.success === false) throw new Error(extractErrorMessage(payload, "create-db reported failure."));
|
|
276
|
+
const databaseUrl = pickConnectionString(payload);
|
|
277
|
+
if (!databaseUrl) throw new Error("create-db did not return a connection string.");
|
|
278
|
+
return {
|
|
279
|
+
databaseUrl,
|
|
280
|
+
claimUrl: typeof payload.claimUrl === "string" && payload.claimUrl.length > 0 ? payload.claimUrl : typeof payload.claimURL === "string" && payload.claimURL.length > 0 ? payload.claimURL : void 0
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function getCreateDbCommand(packageManager) {
|
|
284
|
+
return getPackageExecutionCommand(packageManager, [...CREATE_DB_COMMAND_ARGS]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/templates/render-init-templates.ts
|
|
289
|
+
function getInitTemplatesDir() {
|
|
290
|
+
return resolveTemplatesDir("templates/init");
|
|
291
|
+
}
|
|
292
|
+
function createTemplateContext(provider, envVar, schemaPreset) {
|
|
293
|
+
return {
|
|
294
|
+
envVar,
|
|
295
|
+
provider,
|
|
296
|
+
schemaPreset,
|
|
297
|
+
usePgAdapter: provider === "postgresql" || provider === "cockroachdb",
|
|
298
|
+
useMariaDbAdapter: provider === "mysql",
|
|
299
|
+
useSqliteAdapter: provider === "sqlite",
|
|
300
|
+
useMssqlAdapter: provider === "sqlserver",
|
|
301
|
+
useBasicSchema: schemaPreset === "basic"
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function scaffoldInitTemplates(projectDir, provider, envVar = "DATABASE_URL", schemaPreset = "empty") {
|
|
305
|
+
await renderTemplateTree({
|
|
306
|
+
templateRoot: getInitTemplatesDir(),
|
|
307
|
+
outputDir: projectDir,
|
|
308
|
+
context: createTemplateContext(provider, envVar, schemaPreset)
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
schemaPath: path.join(projectDir, "prisma/schema.prisma"),
|
|
312
|
+
configPath: path.join(projectDir, "prisma.config.ts"),
|
|
313
|
+
singletonPath: path.join(projectDir, "prisma/index.ts")
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/tasks/init-prisma.ts
|
|
319
|
+
const prismaManagedFiles = [
|
|
320
|
+
"schema.prisma",
|
|
321
|
+
"prisma/schema.prisma",
|
|
322
|
+
"prisma/index.ts",
|
|
323
|
+
"prisma.config.ts"
|
|
324
|
+
];
|
|
325
|
+
const prismaTemplateFiles = [
|
|
326
|
+
"prisma/schema.prisma",
|
|
327
|
+
"prisma/index.ts",
|
|
328
|
+
"prisma.config.ts"
|
|
329
|
+
];
|
|
330
|
+
var PrismaAlreadyInitializedError = class extends Error {
|
|
331
|
+
existingFiles;
|
|
332
|
+
constructor(existingFiles) {
|
|
333
|
+
super(`This project already appears to be Prisma-initialized: ${existingFiles.join(", ")}`);
|
|
334
|
+
this.name = "PrismaAlreadyInitializedError";
|
|
335
|
+
this.existingFiles = existingFiles;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
function findExistingPrismaFiles(projectDir = process.cwd()) {
|
|
339
|
+
return prismaManagedFiles.map((relativePath) => path.join(projectDir, relativePath)).filter((absolutePath) => fs.existsSync(absolutePath));
|
|
340
|
+
}
|
|
341
|
+
function canReusePrismaFiles(projectDir = process.cwd()) {
|
|
342
|
+
return prismaTemplateFiles.every((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
|
|
343
|
+
}
|
|
344
|
+
function getDefaultDatabaseUrl(provider) {
|
|
345
|
+
switch (provider) {
|
|
346
|
+
case "postgresql": return "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public";
|
|
347
|
+
case "cockroachdb": return "postgresql://johndoe:randompassword@localhost:26257/mydb?schema=public";
|
|
348
|
+
case "mysql": return "mysql://johndoe:randompassword@localhost:3306/mydb";
|
|
349
|
+
case "sqlite": return "file:./dev.db";
|
|
350
|
+
case "sqlserver": return "sqlserver://localhost:1433;database=mydb;user=SA;password=randompassword;";
|
|
351
|
+
default: {
|
|
352
|
+
const exhaustiveCheck = provider;
|
|
353
|
+
throw new Error(`Unsupported provider: ${String(exhaustiveCheck)}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function escapeRegExp(value) {
|
|
358
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
359
|
+
}
|
|
360
|
+
function hasEnvVar(content, envVarName) {
|
|
361
|
+
const escapedName = escapeRegExp(envVarName);
|
|
362
|
+
return new RegExp(`(^|\\n)\\s*${escapedName}\\s*=`).test(content);
|
|
363
|
+
}
|
|
364
|
+
function hasEnvComment(content, comment) {
|
|
365
|
+
const escapedComment = escapeRegExp(comment);
|
|
366
|
+
return new RegExp(`(^|\\n)\\s*#\\s*${escapedComment}\\s*(?=\\n|$)`).test(content);
|
|
367
|
+
}
|
|
368
|
+
async function ensureEnvVarInEnv(projectDir, envVarName, envVarValue, opts) {
|
|
369
|
+
const envPath = path.join(projectDir, ".env");
|
|
370
|
+
const envLine = `${envVarName}="${envVarValue}"`;
|
|
371
|
+
if (!await fs.pathExists(envPath)) {
|
|
372
|
+
await fs.writeFile(envPath, `${envLine}\n`, "utf8");
|
|
373
|
+
return {
|
|
374
|
+
envPath,
|
|
375
|
+
status: "created"
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const existingContent = await fs.readFile(envPath, "utf8");
|
|
379
|
+
if (hasEnvVar(existingContent, envVarName)) {
|
|
380
|
+
if (opts.mode === "keep-existing") return {
|
|
381
|
+
envPath,
|
|
382
|
+
status: "existing"
|
|
383
|
+
};
|
|
384
|
+
const escapedName = escapeRegExp(envVarName);
|
|
385
|
+
const lineRegex = new RegExp(`(^|\\n)\\s*${escapedName}\\s*=.*(?=\\n|$)`, "m");
|
|
386
|
+
const updatedContent = existingContent.replace(lineRegex, `$1${envLine}`);
|
|
387
|
+
if (updatedContent === existingContent) return {
|
|
388
|
+
envPath,
|
|
389
|
+
status: "existing"
|
|
390
|
+
};
|
|
391
|
+
await fs.writeFile(envPath, updatedContent, "utf8");
|
|
392
|
+
return {
|
|
393
|
+
envPath,
|
|
394
|
+
status: "updated"
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const insertion = `${existingContent.endsWith("\n") ? "" : "\n"}${opts.comment ? `\n# ${opts.comment}\n` : "\n"}${envLine}\n`;
|
|
398
|
+
await fs.appendFile(envPath, insertion, "utf8");
|
|
399
|
+
return {
|
|
400
|
+
envPath,
|
|
401
|
+
status: "appended"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async function ensureEnvComment(projectDir, comment) {
|
|
405
|
+
const envPath = path.join(projectDir, ".env");
|
|
406
|
+
const commentLine = `# ${comment}`;
|
|
407
|
+
if (!await fs.pathExists(envPath)) {
|
|
408
|
+
await fs.writeFile(envPath, `${commentLine}\n`, "utf8");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const existingContent = await fs.readFile(envPath, "utf8");
|
|
412
|
+
if (hasEnvComment(existingContent, comment)) return;
|
|
413
|
+
const separator = existingContent.endsWith("\n") ? "" : "\n";
|
|
414
|
+
await fs.appendFile(envPath, `${separator}${commentLine}\n`, "utf8");
|
|
415
|
+
}
|
|
416
|
+
function hasGitignoreEntry(content, entry) {
|
|
417
|
+
const escapedEntry = escapeRegExp(entry);
|
|
418
|
+
const escapedWithLeadingSlash = escapeRegExp(`/${entry}`);
|
|
419
|
+
return new RegExp(`(^|\\n)\\s*(?:${escapedEntry}|${escapedWithLeadingSlash})\\s*(?=\\n|$)`).test(content);
|
|
420
|
+
}
|
|
421
|
+
async function ensureGitignoreEntry(projectDir, entry) {
|
|
422
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
423
|
+
if (!await fs.pathExists(gitignorePath)) {
|
|
424
|
+
await fs.writeFile(gitignorePath, `${entry}\n`, "utf8");
|
|
425
|
+
return {
|
|
426
|
+
gitignorePath,
|
|
427
|
+
status: "created"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const existingContent = await fs.readFile(gitignorePath, "utf8");
|
|
431
|
+
if (hasGitignoreEntry(existingContent, entry)) return {
|
|
432
|
+
gitignorePath,
|
|
433
|
+
status: "existing"
|
|
434
|
+
};
|
|
435
|
+
const separator = existingContent.endsWith("\n") ? "" : "\n";
|
|
436
|
+
await fs.appendFile(gitignorePath, `${separator}${entry}\n`, "utf8");
|
|
437
|
+
return {
|
|
438
|
+
gitignorePath,
|
|
439
|
+
status: "appended"
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async function initializePrismaFiles(options) {
|
|
443
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
444
|
+
const prismaFilesMode = options.prismaFilesMode ?? "create";
|
|
445
|
+
const existingPrismaFiles = findExistingPrismaFiles(projectDir);
|
|
446
|
+
if (prismaFilesMode === "create" && existingPrismaFiles.length > 0) throw new PrismaAlreadyInitializedError(existingPrismaFiles);
|
|
447
|
+
if (prismaFilesMode === "reuse" && existingPrismaFiles.length === 0) throw new Error("Cannot reuse Prisma files because no existing Prisma files were found.");
|
|
448
|
+
if (prismaFilesMode === "reuse" && !canReusePrismaFiles(projectDir)) throw new Error("Cannot reuse Prisma files because required files are missing.");
|
|
449
|
+
const schemaPath = path.join(projectDir, "prisma/schema.prisma");
|
|
450
|
+
const configPath = path.join(projectDir, "prisma.config.ts");
|
|
451
|
+
const singletonPath = path.join(projectDir, "prisma/index.ts");
|
|
452
|
+
if (prismaFilesMode !== "reuse") await scaffoldInitTemplates(projectDir, options.provider, "DATABASE_URL", options.schemaPreset ?? "empty");
|
|
453
|
+
const envResult = await ensureEnvVarInEnv(projectDir, "DATABASE_URL", options.databaseUrl ?? getDefaultDatabaseUrl(options.provider), {
|
|
454
|
+
mode: options.databaseUrl ? "upsert" : "keep-existing",
|
|
455
|
+
comment: "Added by create-prisma init"
|
|
456
|
+
});
|
|
457
|
+
let claimEnvStatus;
|
|
458
|
+
if (options.claimUrl) {
|
|
459
|
+
claimEnvStatus = (await ensureEnvVarInEnv(projectDir, "CLAIM_URL", options.claimUrl, {
|
|
460
|
+
mode: "upsert",
|
|
461
|
+
comment: PRISMA_POSTGRES_TEMPORARY_NOTICE
|
|
462
|
+
})).status;
|
|
463
|
+
await ensureEnvComment(projectDir, PRISMA_POSTGRES_TEMPORARY_NOTICE);
|
|
464
|
+
}
|
|
465
|
+
const gitignoreResult = await ensureGitignoreEntry(projectDir, "prisma/generated");
|
|
466
|
+
return {
|
|
467
|
+
schemaPath,
|
|
468
|
+
configPath,
|
|
469
|
+
singletonPath,
|
|
470
|
+
prismaFilesMode,
|
|
471
|
+
envPath: envResult.envPath,
|
|
472
|
+
envStatus: envResult.status,
|
|
473
|
+
gitignorePath: gitignoreResult.gitignorePath,
|
|
474
|
+
gitignoreStatus: gitignoreResult.status,
|
|
475
|
+
claimEnvStatus
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/constants/dependencies.ts
|
|
481
|
+
const dependencyVersionMap = {
|
|
482
|
+
"@prisma/client": "^7.4.0",
|
|
483
|
+
"@prisma/adapter-pg": "^7.4.0",
|
|
484
|
+
"@prisma/adapter-mariadb": "^7.4.0",
|
|
485
|
+
"@prisma/adapter-better-sqlite3": "^7.4.0",
|
|
486
|
+
"@prisma/adapter-mssql": "^7.4.0",
|
|
487
|
+
dotenv: "^17.2.3",
|
|
488
|
+
prisma: "^7.4.0"
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/db/config.ts
|
|
493
|
+
function getDbPackages(provider) {
|
|
494
|
+
switch (provider) {
|
|
495
|
+
case "postgresql":
|
|
496
|
+
case "cockroachdb": return { adapterPackage: "@prisma/adapter-pg" };
|
|
497
|
+
case "mysql": return { adapterPackage: "@prisma/adapter-mariadb" };
|
|
498
|
+
case "sqlite": return { adapterPackage: "@prisma/adapter-better-sqlite3" };
|
|
499
|
+
case "sqlserver": return { adapterPackage: "@prisma/adapter-mssql" };
|
|
500
|
+
default: {
|
|
501
|
+
const exhaustiveCheck = provider;
|
|
502
|
+
throw new Error(`Unsupported database provider: ${String(exhaustiveCheck)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/tasks/install.ts
|
|
509
|
+
const prismaScriptMap = {
|
|
510
|
+
"db:generate": "prisma generate",
|
|
511
|
+
"db:push": "prisma db push",
|
|
512
|
+
"db:migrate": "prisma migrate dev"
|
|
513
|
+
};
|
|
514
|
+
function getVersion(packageName) {
|
|
515
|
+
return dependencyVersionMap[packageName];
|
|
516
|
+
}
|
|
517
|
+
function unique(items) {
|
|
518
|
+
return [...new Set(items)];
|
|
519
|
+
}
|
|
520
|
+
function sortRecord(record) {
|
|
521
|
+
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
522
|
+
}
|
|
523
|
+
async function addPackageDependency(opts) {
|
|
524
|
+
const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, scripts = {}, scriptMode = "upsert", projectDir } = opts;
|
|
525
|
+
const addedScripts = [];
|
|
526
|
+
const existingScripts = [];
|
|
527
|
+
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
528
|
+
if (!await fs.pathExists(pkgJsonPath)) throw new Error(`No package.json found in ${projectDir}. Run this command inside an existing JavaScript/TypeScript project.`);
|
|
529
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
530
|
+
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
531
|
+
if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
|
|
532
|
+
if (!pkgJson.scripts) pkgJson.scripts = {};
|
|
533
|
+
for (const pkgName of unique(dependencies)) {
|
|
534
|
+
const version = getVersion(pkgName);
|
|
535
|
+
if (version) pkgJson.dependencies[pkgName] = version;
|
|
536
|
+
else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
|
|
537
|
+
}
|
|
538
|
+
for (const pkgName of unique(devDependencies)) {
|
|
539
|
+
const version = getVersion(pkgName);
|
|
540
|
+
if (version) pkgJson.devDependencies[pkgName] = version;
|
|
541
|
+
else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
|
|
542
|
+
}
|
|
543
|
+
for (const [pkgName, version] of Object.entries(customDependencies)) pkgJson.dependencies[pkgName] = version;
|
|
544
|
+
for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
|
|
545
|
+
for (const [scriptName, command] of Object.entries(scripts)) {
|
|
546
|
+
if (scriptMode === "if-missing") {
|
|
547
|
+
if (typeof pkgJson.scripts[scriptName] !== "string" || pkgJson.scripts[scriptName].trim().length === 0) {
|
|
548
|
+
pkgJson.scripts[scriptName] = command;
|
|
549
|
+
addedScripts.push(scriptName);
|
|
550
|
+
} else existingScripts.push(scriptName);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (pkgJson.scripts[scriptName] === command) existingScripts.push(scriptName);
|
|
554
|
+
else addedScripts.push(scriptName);
|
|
555
|
+
pkgJson.scripts[scriptName] = command;
|
|
556
|
+
}
|
|
557
|
+
pkgJson.dependencies = sortRecord(pkgJson.dependencies);
|
|
558
|
+
pkgJson.devDependencies = sortRecord(pkgJson.devDependencies);
|
|
559
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
560
|
+
return {
|
|
561
|
+
addedScripts,
|
|
562
|
+
existingScripts
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
async function writePrismaDependencies(provider, projectDir = process.cwd()) {
|
|
566
|
+
const dependencies = ["@prisma/client", "dotenv"];
|
|
567
|
+
const devDependencies = ["prisma"];
|
|
568
|
+
const { adapterPackage } = getDbPackages(provider);
|
|
569
|
+
dependencies.push(adapterPackage);
|
|
570
|
+
const scriptWriteResult = await addPackageDependency({
|
|
571
|
+
dependencies,
|
|
572
|
+
devDependencies,
|
|
573
|
+
scripts: prismaScriptMap,
|
|
574
|
+
scriptMode: "if-missing",
|
|
575
|
+
projectDir
|
|
576
|
+
});
|
|
577
|
+
return {
|
|
578
|
+
dependencies,
|
|
579
|
+
devDependencies,
|
|
580
|
+
scripts: Object.keys(prismaScriptMap),
|
|
581
|
+
addedScripts: scriptWriteResult.addedScripts,
|
|
582
|
+
existingScripts: scriptWriteResult.existingScripts
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
async function installProjectDependencies(packageManager, projectDir = process.cwd(), options = {}) {
|
|
586
|
+
const verbose = options.verbose === true;
|
|
587
|
+
const installCommand = getInstallArgs(packageManager);
|
|
588
|
+
await execa(installCommand.command, installCommand.args, {
|
|
589
|
+
cwd: projectDir,
|
|
590
|
+
stdio: verbose ? "inherit" : "pipe"
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/commands/init.ts
|
|
596
|
+
const DEFAULT_DATABASE_PROVIDER = "postgresql";
|
|
597
|
+
const DEFAULT_SCHEMA_PRESET$1 = "empty";
|
|
598
|
+
const DEFAULT_PRISMA_POSTGRES = true;
|
|
599
|
+
const DEFAULT_INSTALL = true;
|
|
600
|
+
const DEFAULT_GENERATE = true;
|
|
601
|
+
async function promptForDatabaseProvider() {
|
|
602
|
+
const databaseProvider = await select({
|
|
603
|
+
message: "Select your database",
|
|
604
|
+
initialValue: DEFAULT_DATABASE_PROVIDER,
|
|
605
|
+
options: [
|
|
606
|
+
{
|
|
607
|
+
value: "postgresql",
|
|
608
|
+
label: "PostgreSQL",
|
|
609
|
+
hint: "Default"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
value: "mysql",
|
|
613
|
+
label: "MySQL"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
value: "sqlite",
|
|
617
|
+
label: "SQLite"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
value: "sqlserver",
|
|
621
|
+
label: "SQL Server"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
value: "cockroachdb",
|
|
625
|
+
label: "CockroachDB"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
});
|
|
629
|
+
if (isCancel(databaseProvider)) {
|
|
630
|
+
cancel("Cancelled.");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
return DatabaseProviderSchema.parse(databaseProvider);
|
|
634
|
+
}
|
|
635
|
+
function getPackageManagerHint(option, detected) {
|
|
636
|
+
if (option === detected) return "Detected";
|
|
637
|
+
if (option === "bun") return "Fast runtime + package manager";
|
|
638
|
+
}
|
|
639
|
+
async function promptForPackageManager(detectedPackageManager) {
|
|
640
|
+
const packageManager = await select({
|
|
641
|
+
message: "Choose package manager",
|
|
642
|
+
initialValue: detectedPackageManager,
|
|
643
|
+
options: [
|
|
644
|
+
{
|
|
645
|
+
value: "npm",
|
|
646
|
+
label: "npm",
|
|
647
|
+
hint: getPackageManagerHint("npm", detectedPackageManager)
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
value: "pnpm",
|
|
651
|
+
label: "pnpm",
|
|
652
|
+
hint: getPackageManagerHint("pnpm", detectedPackageManager)
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
value: "bun",
|
|
656
|
+
label: "bun",
|
|
657
|
+
hint: getPackageManagerHint("bun", detectedPackageManager)
|
|
658
|
+
}
|
|
659
|
+
]
|
|
660
|
+
});
|
|
661
|
+
if (isCancel(packageManager)) {
|
|
662
|
+
cancel("Cancelled.");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
return PackageManagerSchema.parse(packageManager);
|
|
666
|
+
}
|
|
667
|
+
async function promptForDependencyInstall(packageManager) {
|
|
668
|
+
const shouldInstall = await confirm({
|
|
669
|
+
message: `Install dependencies now with ${getInstallCommand(packageManager)}?`,
|
|
670
|
+
initialValue: true
|
|
671
|
+
});
|
|
672
|
+
if (isCancel(shouldInstall)) {
|
|
673
|
+
cancel("Cancelled.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
return Boolean(shouldInstall);
|
|
677
|
+
}
|
|
678
|
+
async function promptForPrismaPostgres() {
|
|
679
|
+
const shouldUsePrismaPostgres = await confirm({
|
|
680
|
+
message: "Use Prisma Postgres and auto-generate DATABASE_URL with create-db?",
|
|
681
|
+
initialValue: true
|
|
682
|
+
});
|
|
683
|
+
if (isCancel(shouldUsePrismaPostgres)) {
|
|
684
|
+
cancel("Cancelled.");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
return Boolean(shouldUsePrismaPostgres);
|
|
688
|
+
}
|
|
689
|
+
async function promptContinueWithDefaultPostgresUrl() {
|
|
690
|
+
const shouldContinue = await confirm({
|
|
691
|
+
message: "Continue with default local PostgreSQL DATABASE_URL instead?",
|
|
692
|
+
initialValue: true
|
|
693
|
+
});
|
|
694
|
+
if (isCancel(shouldContinue)) {
|
|
695
|
+
cancel("Cancelled.");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
return Boolean(shouldContinue);
|
|
699
|
+
}
|
|
700
|
+
async function promptForPrismaFilesMode(existingFiles, canReuseExistingPrismaFiles, baseDir) {
|
|
701
|
+
const mode = await select({
|
|
702
|
+
message: `Prisma already exists (${existingFiles.map((filePath) => formatCreatedPath(filePath, baseDir)).join(", ")}). How should we continue?`,
|
|
703
|
+
initialValue: canReuseExistingPrismaFiles ? "reuse" : "overwrite",
|
|
704
|
+
options: canReuseExistingPrismaFiles ? [
|
|
705
|
+
{
|
|
706
|
+
value: "reuse",
|
|
707
|
+
label: "Keep existing Prisma files",
|
|
708
|
+
hint: "Recommended"
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
value: "overwrite",
|
|
712
|
+
label: "Overwrite Prisma files"
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
value: "cancel",
|
|
716
|
+
label: "Cancel"
|
|
717
|
+
}
|
|
718
|
+
] : [{
|
|
719
|
+
value: "overwrite",
|
|
720
|
+
label: "Repair and overwrite Prisma files",
|
|
721
|
+
hint: "Recommended"
|
|
722
|
+
}, {
|
|
723
|
+
value: "cancel",
|
|
724
|
+
label: "Cancel"
|
|
725
|
+
}]
|
|
726
|
+
});
|
|
727
|
+
if (isCancel(mode) || mode === "cancel") {
|
|
728
|
+
cancel("Cancelled.");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (mode !== "reuse" && mode !== "overwrite") {
|
|
732
|
+
cancel("Cancelled.");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
return mode;
|
|
736
|
+
}
|
|
737
|
+
function formatEnvStatus(status, envPath, envVarName, baseDir) {
|
|
738
|
+
const relativeEnvPath = path.relative(baseDir, envPath) || ".env";
|
|
739
|
+
switch (status) {
|
|
740
|
+
case "created": return `Created ${relativeEnvPath} with ${envVarName}`;
|
|
741
|
+
case "appended": return `Appended ${envVarName} to ${relativeEnvPath}`;
|
|
742
|
+
case "existing": return `Kept existing ${envVarName} in ${relativeEnvPath}`;
|
|
743
|
+
case "updated": return `Updated ${envVarName} in ${relativeEnvPath}`;
|
|
744
|
+
default: return `Updated ${relativeEnvPath}`;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function formatGitignoreStatus(status, gitignorePath, baseDir) {
|
|
748
|
+
const relativePath = path.relative(baseDir, gitignorePath) || ".gitignore";
|
|
749
|
+
switch (status) {
|
|
750
|
+
case "created": return `Created ${relativePath} with prisma/generated`;
|
|
751
|
+
case "appended": return `Added prisma/generated to ${relativePath}`;
|
|
752
|
+
case "existing": return `Kept existing prisma/generated ignore in ${relativePath}`;
|
|
753
|
+
default: return `Updated ${relativePath}`;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function formatCreatedPath(filePath, baseDir) {
|
|
757
|
+
return path.relative(baseDir, filePath);
|
|
758
|
+
}
|
|
759
|
+
function formatFileAction(action) {
|
|
760
|
+
switch (action) {
|
|
761
|
+
case "create": return "Created";
|
|
762
|
+
case "overwrite": return "Wrote";
|
|
763
|
+
case "reuse": return "Kept existing";
|
|
764
|
+
default: {
|
|
765
|
+
const exhaustiveCheck = action;
|
|
766
|
+
throw new Error(`Unsupported file action: ${String(exhaustiveCheck)}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function getCommandErrorMessage(error) {
|
|
771
|
+
if (error instanceof Error && "stderr" in error) {
|
|
772
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
773
|
+
if (stderr.length > 0) return stderr;
|
|
774
|
+
}
|
|
775
|
+
return error instanceof Error ? error.message : String(error);
|
|
776
|
+
}
|
|
777
|
+
async function runInitCommand(rawInput = {}, options = {}) {
|
|
778
|
+
const projectDir = path.resolve(options.projectDir ?? process.cwd());
|
|
779
|
+
const input = InitCommandInputSchema.parse(rawInput);
|
|
780
|
+
const useDefaults = input.yes === true;
|
|
781
|
+
const verbose = input.verbose === true;
|
|
782
|
+
const shouldGenerate = input.generate ?? DEFAULT_GENERATE;
|
|
783
|
+
if (!options.skipIntro) intro("Create Prisma");
|
|
784
|
+
let prismaFilesMode = "create";
|
|
785
|
+
const existingPrismaFiles = findExistingPrismaFiles(projectDir);
|
|
786
|
+
const canReuseExistingPrismaFiles = canReusePrismaFiles(projectDir);
|
|
787
|
+
if (existingPrismaFiles.length > 0) if (useDefaults) prismaFilesMode = canReuseExistingPrismaFiles ? "reuse" : "overwrite";
|
|
788
|
+
else {
|
|
789
|
+
const selectedMode = await promptForPrismaFilesMode(existingPrismaFiles, canReuseExistingPrismaFiles, projectDir);
|
|
790
|
+
if (!selectedMode) return;
|
|
791
|
+
prismaFilesMode = selectedMode;
|
|
792
|
+
}
|
|
793
|
+
const databaseProvider = input.provider ?? (useDefaults ? DEFAULT_DATABASE_PROVIDER : await promptForDatabaseProvider());
|
|
794
|
+
if (!databaseProvider) return;
|
|
795
|
+
const schemaPreset = input.schemaPreset ?? DEFAULT_SCHEMA_PRESET$1;
|
|
796
|
+
let databaseUrl = input.databaseUrl;
|
|
797
|
+
let shouldUsePrismaPostgres = false;
|
|
798
|
+
let claimUrl;
|
|
799
|
+
let prismaPostgresWarning;
|
|
800
|
+
if (databaseProvider === "postgresql" && !databaseUrl) {
|
|
801
|
+
const prismaPostgresChoice = input.prismaPostgres ?? (useDefaults ? DEFAULT_PRISMA_POSTGRES : await promptForPrismaPostgres());
|
|
802
|
+
if (prismaPostgresChoice === void 0) return;
|
|
803
|
+
shouldUsePrismaPostgres = prismaPostgresChoice;
|
|
804
|
+
}
|
|
805
|
+
const detectedPackageManager = await detectPackageManager(projectDir);
|
|
806
|
+
const finalPackageManager = input.packageManager ?? (useDefaults ? detectedPackageManager : await promptForPackageManager(detectedPackageManager));
|
|
807
|
+
if (!finalPackageManager) return;
|
|
808
|
+
const installCommand = getInstallCommand(finalPackageManager);
|
|
809
|
+
if (shouldUsePrismaPostgres) {
|
|
810
|
+
const createDbCommand = getCreateDbCommand(finalPackageManager);
|
|
811
|
+
const prismaPostgresSpinner = spinner();
|
|
812
|
+
prismaPostgresSpinner.start(`Provisioning Prisma Postgres with ${createDbCommand}...`);
|
|
813
|
+
try {
|
|
814
|
+
const prismaPostgresResult = await provisionPrismaPostgres(finalPackageManager);
|
|
815
|
+
databaseUrl = prismaPostgresResult.databaseUrl;
|
|
816
|
+
claimUrl = prismaPostgresResult.claimUrl;
|
|
817
|
+
prismaPostgresSpinner.stop("Prisma Postgres database provisioned.");
|
|
818
|
+
} catch (error) {
|
|
819
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
820
|
+
prismaPostgresSpinner.stop("Could not provision Prisma Postgres.");
|
|
821
|
+
prismaPostgresWarning = `Prisma Postgres provisioning failed: ${errorMessage}`;
|
|
822
|
+
const shouldContinue = useDefaults ? true : await promptContinueWithDefaultPostgresUrl();
|
|
823
|
+
if (shouldContinue === void 0) return;
|
|
824
|
+
if (!shouldContinue) {
|
|
825
|
+
cancel("Cancelled.");
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const dependencyWriteResult = await writePrismaDependencies(databaseProvider, projectDir);
|
|
831
|
+
const shouldInstall = input.install ?? (useDefaults ? DEFAULT_INSTALL : await promptForDependencyInstall(finalPackageManager));
|
|
832
|
+
if (shouldInstall === void 0) return;
|
|
833
|
+
if (shouldInstall) if (verbose) {
|
|
834
|
+
log.step(`Running ${installCommand}`);
|
|
835
|
+
try {
|
|
836
|
+
await installProjectDependencies(finalPackageManager, projectDir, { verbose });
|
|
837
|
+
log.success("Dependencies installed.");
|
|
838
|
+
} catch (error) {
|
|
839
|
+
cancel(`Failed to run ${installCommand}: ${getCommandErrorMessage(error)}`);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
const installSpinner = spinner();
|
|
844
|
+
installSpinner.start(`Running ${installCommand}...`);
|
|
845
|
+
try {
|
|
846
|
+
await installProjectDependencies(finalPackageManager, projectDir, { verbose });
|
|
847
|
+
installSpinner.stop("Dependencies installed.");
|
|
848
|
+
} catch (error) {
|
|
849
|
+
installSpinner.stop("Could not install dependencies.");
|
|
850
|
+
cancel(`Failed to run ${installCommand}: ${getCommandErrorMessage(error)}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const initSpinner = spinner();
|
|
855
|
+
initSpinner.start("Preparing Prisma files...");
|
|
856
|
+
let initResult;
|
|
857
|
+
try {
|
|
858
|
+
initResult = await initializePrismaFiles({
|
|
859
|
+
provider: databaseProvider,
|
|
860
|
+
databaseUrl,
|
|
861
|
+
claimUrl,
|
|
862
|
+
schemaPreset,
|
|
863
|
+
prismaFilesMode,
|
|
864
|
+
projectDir
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
initSpinner.stop("Could not prepare Prisma files.");
|
|
868
|
+
cancel(getCommandErrorMessage(error));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (initResult.prismaFilesMode === "overwrite") initSpinner.stop("Prisma files updated.");
|
|
872
|
+
else if (initResult.prismaFilesMode === "reuse") initSpinner.stop("Using existing Prisma files.");
|
|
873
|
+
else initSpinner.stop("Prisma files ready.");
|
|
874
|
+
let generateWarning;
|
|
875
|
+
let didGenerateClient = false;
|
|
876
|
+
if (shouldGenerate) {
|
|
877
|
+
const generateCommand = getPrismaCliCommand(finalPackageManager, ["generate"]);
|
|
878
|
+
if (verbose) log.step(`Running ${generateCommand}`);
|
|
879
|
+
const generateSpinner = verbose ? void 0 : spinner();
|
|
880
|
+
generateSpinner?.start("Generating Prisma Client...");
|
|
881
|
+
try {
|
|
882
|
+
const generateArgs = getPrismaCliArgs(finalPackageManager, ["generate"]);
|
|
883
|
+
await execa(generateArgs.command, generateArgs.args, {
|
|
884
|
+
cwd: projectDir,
|
|
885
|
+
stdio: verbose ? "inherit" : "pipe"
|
|
886
|
+
});
|
|
887
|
+
didGenerateClient = true;
|
|
888
|
+
if (verbose) log.success("Prisma Client generated.");
|
|
889
|
+
else generateSpinner?.stop("Prisma Client generated.");
|
|
890
|
+
} catch (error) {
|
|
891
|
+
if (verbose) log.warn("Could not generate Prisma Client.");
|
|
892
|
+
else generateSpinner?.stop("Could not generate Prisma Client.");
|
|
893
|
+
generateWarning = `Prisma generate failed: ${getCommandErrorMessage(error)}`;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const fileActionLabel = formatFileAction(initResult.prismaFilesMode);
|
|
897
|
+
const summaryLines = [
|
|
898
|
+
`- ${fileActionLabel} ${formatCreatedPath(initResult.schemaPath, projectDir)}`,
|
|
899
|
+
`- ${fileActionLabel} ${formatCreatedPath(initResult.configPath, projectDir)}`,
|
|
900
|
+
`- ${fileActionLabel} ${formatCreatedPath(initResult.singletonPath, projectDir)}`
|
|
901
|
+
];
|
|
902
|
+
if (initResult.envStatus !== "existing") summaryLines.push(`- ${formatEnvStatus(initResult.envStatus, initResult.envPath, "DATABASE_URL", projectDir)}`);
|
|
903
|
+
if (initResult.claimEnvStatus) summaryLines.push(`- ${formatEnvStatus(initResult.claimEnvStatus, initResult.envPath, "CLAIM_URL", projectDir)}`);
|
|
904
|
+
if (initResult.gitignoreStatus !== "existing") summaryLines.push(`- ${formatGitignoreStatus(initResult.gitignoreStatus, initResult.gitignorePath, projectDir)}`);
|
|
905
|
+
if (dependencyWriteResult.addedScripts.length > 0) summaryLines.push(`- Added package.json scripts: ${dependencyWriteResult.addedScripts.join(", ")}`);
|
|
906
|
+
else if (dependencyWriteResult.scripts.length > 0) summaryLines.push(`- Kept existing package.json scripts: ${dependencyWriteResult.scripts.join(", ")}`);
|
|
907
|
+
if (!shouldInstall) summaryLines.push(`- Skipped ${installCommand}.`);
|
|
908
|
+
else summaryLines.push(`- Installed dependencies with ${installCommand}.`);
|
|
909
|
+
if (!shouldGenerate) summaryLines.push("- Skipped Prisma Client generation.");
|
|
910
|
+
else if (didGenerateClient) summaryLines.push("- Prisma Client generated.");
|
|
911
|
+
const postgresLines = [];
|
|
912
|
+
if (prismaPostgresWarning) postgresLines.push(`- ${prismaPostgresWarning}`);
|
|
913
|
+
if (generateWarning) postgresLines.push(`- ${generateWarning}`);
|
|
914
|
+
const nextSteps = [...options.prependNextSteps ?? []];
|
|
915
|
+
if (!shouldInstall) nextSteps.push(`- ${installCommand}`);
|
|
916
|
+
if (!didGenerateClient || !shouldGenerate) nextSteps.push(`- ${getRunScriptCommand(finalPackageManager, "db:generate")}`);
|
|
917
|
+
nextSteps.push(`- ${getRunScriptCommand(finalPackageManager, "db:migrate")}`);
|
|
918
|
+
outro(`Setup complete.
|
|
919
|
+
${summaryLines.join("\n")}
|
|
920
|
+
${postgresLines.length > 0 ? `\n${postgresLines.join("\n")}` : ""}
|
|
921
|
+
|
|
922
|
+
Next steps:
|
|
923
|
+
${nextSteps.join("\n")}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/commands/create.ts
|
|
928
|
+
const DEFAULT_PROJECT_NAME = "my-app";
|
|
929
|
+
const DEFAULT_TEMPLATE = "hono";
|
|
930
|
+
const DEFAULT_SCHEMA_PRESET = "basic";
|
|
931
|
+
function toPackageName(projectName) {
|
|
932
|
+
return projectName.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+/, "").replace(/-+$/, "") || "app";
|
|
933
|
+
}
|
|
934
|
+
function formatPathForDisplay(filePath) {
|
|
935
|
+
return path.relative(process.cwd(), filePath) || ".";
|
|
936
|
+
}
|
|
937
|
+
async function promptForProjectName() {
|
|
938
|
+
const projectName = await text({
|
|
939
|
+
message: "Project name",
|
|
940
|
+
placeholder: DEFAULT_PROJECT_NAME,
|
|
941
|
+
initialValue: DEFAULT_PROJECT_NAME,
|
|
942
|
+
validate: (value) => {
|
|
943
|
+
return String(value ?? "").trim().length > 0 ? void 0 : "Please enter a valid project name.";
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
if (isCancel(projectName)) {
|
|
947
|
+
cancel("Cancelled.");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
return String(projectName).trim();
|
|
951
|
+
}
|
|
952
|
+
async function promptForCreateTemplate() {
|
|
953
|
+
const template = await select({
|
|
954
|
+
message: "Select template",
|
|
955
|
+
initialValue: DEFAULT_TEMPLATE,
|
|
956
|
+
options: [{
|
|
957
|
+
value: "hono",
|
|
958
|
+
label: "Hono",
|
|
959
|
+
hint: "Bun + TypeScript API starter"
|
|
960
|
+
}, {
|
|
961
|
+
value: "next",
|
|
962
|
+
label: "Next.js",
|
|
963
|
+
hint: "App Router + TypeScript starter"
|
|
964
|
+
}]
|
|
965
|
+
});
|
|
966
|
+
if (isCancel(template)) {
|
|
967
|
+
cancel("Cancelled.");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
return CreateTemplateSchema.parse(template);
|
|
971
|
+
}
|
|
972
|
+
async function isDirectoryEmpty(directoryPath) {
|
|
973
|
+
if (!await fs.pathExists(directoryPath)) return true;
|
|
974
|
+
return (await fs.readdir(directoryPath)).length === 0;
|
|
975
|
+
}
|
|
976
|
+
async function runCreateCommand(rawInput = {}) {
|
|
977
|
+
const input = CreateCommandInputSchema.parse(rawInput);
|
|
978
|
+
const useDefaults = input.yes === true;
|
|
979
|
+
const force = input.force === true;
|
|
980
|
+
const projectName = input.name ?? (useDefaults ? DEFAULT_PROJECT_NAME : await promptForProjectName());
|
|
981
|
+
if (!projectName) return;
|
|
982
|
+
const template = input.template ?? (useDefaults ? DEFAULT_TEMPLATE : await promptForCreateTemplate());
|
|
983
|
+
if (!template) return;
|
|
984
|
+
const schemaPreset = input.schemaPreset ?? DEFAULT_SCHEMA_PRESET;
|
|
985
|
+
const targetDirectory = path.resolve(process.cwd(), projectName);
|
|
986
|
+
const targetExists = await fs.pathExists(targetDirectory);
|
|
987
|
+
const targetIsEmpty = await isDirectoryEmpty(targetDirectory);
|
|
988
|
+
if (targetExists && !targetIsEmpty && !force) {
|
|
989
|
+
cancel(`Target directory ${formatPathForDisplay(targetDirectory)} is not empty. Use --force to continue.`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const scaffoldSpinner = spinner();
|
|
993
|
+
scaffoldSpinner.start(`Scaffolding ${template} project...`);
|
|
994
|
+
try {
|
|
995
|
+
await scaffoldCreateTemplate({
|
|
996
|
+
projectDir: targetDirectory,
|
|
997
|
+
projectName: toPackageName(path.basename(targetDirectory)),
|
|
998
|
+
template,
|
|
999
|
+
schemaPreset
|
|
1000
|
+
});
|
|
1001
|
+
scaffoldSpinner.stop("Project files scaffolded.");
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
scaffoldSpinner.stop("Could not scaffold project files.");
|
|
1004
|
+
cancel(error instanceof Error ? error.message : String(error));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (targetExists && !targetIsEmpty && force) log.warn(`Used --force in non-empty directory ${formatPathForDisplay(targetDirectory)}.`);
|
|
1008
|
+
await runInitCommand({
|
|
1009
|
+
yes: input.yes,
|
|
1010
|
+
verbose: input.verbose,
|
|
1011
|
+
provider: input.provider,
|
|
1012
|
+
packageManager: input.packageManager,
|
|
1013
|
+
prismaPostgres: input.prismaPostgres,
|
|
1014
|
+
databaseUrl: input.databaseUrl,
|
|
1015
|
+
install: input.install,
|
|
1016
|
+
generate: input.generate,
|
|
1017
|
+
schemaPreset
|
|
1018
|
+
}, {
|
|
1019
|
+
skipIntro: true,
|
|
1020
|
+
prependNextSteps: [`- cd ${formatPathForDisplay(targetDirectory)}`],
|
|
1021
|
+
projectDir: targetDirectory
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
//#endregion
|
|
1026
|
+
export { DatabaseProviderSchema as a, PackageManagerSchema as c, CreateTemplateSchema as i, SchemaPresetSchema as l, runInitCommand as n, DatabaseUrlSchema as o, CreateCommandInputSchema as r, InitCommandInputSchema as s, runCreateCommand as t };
|