create-prisma-php-app 4.0.0-alpha.32 → 4.0.0-alpha.34

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.
Files changed (2) hide show
  1. package/dist/index.js +101 -861
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,857 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execSync, spawnSync } from "child_process";
3
- import fs from "fs";
4
- import { fileURLToPath } from "url";
5
- import path from "path";
6
- import chalk from "chalk";
7
- import prompts from "prompts";
8
- import https from "https";
9
- import { randomBytes } from "crypto";
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- let updateAnswer = null;
13
- const nonBackendFiles = [
14
- "favicon.ico",
15
- "\\src\\app\\index.php",
16
- "metadata.php",
17
- "not-found.php",
18
- "error.php",
19
- ];
20
- const dockerFiles = [
21
- ".dockerignore",
22
- "docker-compose.yml",
23
- "Dockerfile",
24
- "apache.conf",
25
- ];
26
- const STARTER_KITS = {
27
- basic: {
28
- id: "basic",
29
- name: "Basic PHP Application",
30
- description: "Simple PHP backend with minimal dependencies",
31
- features: {
32
- backendOnly: true,
33
- tailwindcss: false,
34
- websocket: false,
35
- prisma: false,
36
- docker: false,
37
- swaggerDocs: false,
38
- mcp: false,
39
- },
40
- requiredFiles: [
41
- "bootstrap.php",
42
- ".htaccess",
43
- "src/app/layout.php",
44
- "src/app/index.php",
45
- ],
46
- },
47
- fullstack: {
48
- id: "fullstack",
49
- name: "Full-Stack Application",
50
- description: "Complete web application with frontend and backend",
51
- features: {
52
- backendOnly: false,
53
- tailwindcss: true,
54
- websocket: false,
55
- prisma: true,
56
- docker: false,
57
- swaggerDocs: true,
58
- mcp: false,
59
- },
60
- requiredFiles: [
61
- "bootstrap.php",
62
- ".htaccess",
63
- "postcss.config.js",
64
- "src/app/layout.php",
65
- "src/app/index.php",
66
- "src/app/js/index.js",
67
- "src/app/css/tailwind.css",
68
- ],
69
- },
70
- api: {
71
- id: "api",
72
- name: "REST API",
73
- description: "Backend API with database and documentation",
74
- features: {
75
- backendOnly: true,
76
- tailwindcss: false,
77
- websocket: false,
78
- prisma: true,
79
- docker: true,
80
- swaggerDocs: true,
81
- mcp: false,
82
- },
83
- requiredFiles: [
84
- "bootstrap.php",
85
- ".htaccess",
86
- "docker-compose.yml",
87
- "Dockerfile",
88
- ],
89
- },
90
- realtime: {
91
- id: "realtime",
92
- name: "Real-time Application",
93
- description: "Application with WebSocket support and MCP",
94
- features: {
95
- backendOnly: false,
96
- tailwindcss: true,
97
- websocket: true,
98
- prisma: true,
99
- docker: false,
100
- swaggerDocs: true,
101
- mcp: true,
102
- },
103
- requiredFiles: [
104
- "bootstrap.php",
105
- ".htaccess",
106
- "postcss.config.js",
107
- "src/lib/websocket",
108
- "src/lib/mcp",
109
- ],
110
- },
111
- // Custom starter kit examples
112
- ecommerce: {
113
- id: "ecommerce",
114
- name: "E-commerce Starter",
115
- description: "Full e-commerce application with cart, payments, and admin",
116
- features: {
117
- backendOnly: false,
118
- tailwindcss: true,
119
- websocket: false,
120
- prisma: true,
121
- docker: true,
122
- swaggerDocs: true,
123
- mcp: false,
124
- },
125
- requiredFiles: [],
126
- source: {
127
- type: "git",
128
- url: "https://github.com/your-org/prisma-php-ecommerce-starter",
129
- branch: "main",
130
- },
131
- },
132
- blog: {
133
- id: "blog",
134
- name: "Blog CMS",
135
- description: "Blog content management system",
136
- features: {
137
- backendOnly: false,
138
- tailwindcss: true,
139
- websocket: false,
140
- prisma: true,
141
- docker: false,
142
- swaggerDocs: false,
143
- mcp: false,
144
- },
145
- requiredFiles: [],
146
- source: {
147
- type: "git",
148
- url: "https://github.com/your-org/prisma-php-blog-starter",
149
- },
150
- },
151
- };
152
- function bsConfigUrls(projectRootPath) {
153
- // Identify the base path dynamically up to and including 'htdocs'
154
- const htdocsIndex = projectRootPath.indexOf("\\htdocs\\");
155
- if (htdocsIndex === -1) {
156
- console.error(
157
- "Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"
158
- );
159
- return {
160
- bsTarget: "",
161
- bsPathRewrite: {},
162
- };
163
- }
164
- // Extract the path up to and including 'htdocs\\'
165
- const basePathToRemove = projectRootPath.substring(
166
- 0,
167
- htdocsIndex + "\\htdocs\\".length
168
- );
169
- // Escape backslashes for the regex pattern
170
- const escapedBasePathToRemove = basePathToRemove.replace(/\\/g, "\\\\");
171
- // Remove the base path and replace backslashes with forward slashes for URL compatibility
172
- const relativeWebPath = projectRootPath
173
- .replace(new RegExp(`^${escapedBasePathToRemove}`), "")
174
- .replace(/\\/g, "/");
175
- // Construct the Browser Sync command with the correct proxy URL, being careful not to affect the protocol part
176
- let proxyUrl = `http://localhost/${relativeWebPath}`;
177
- // Ensure the proxy URL does not end with a slash before appending '/public'
178
- proxyUrl = proxyUrl.endsWith("/") ? proxyUrl.slice(0, -1) : proxyUrl;
179
- // Clean the URL by replacing "//" with "/" but not affecting "http://"
180
- // We replace instances of "//" that are not preceded by ":"
181
- const cleanUrl = proxyUrl.replace(/(?<!:)(\/\/+)/g, "/");
182
- const cleanRelativeWebPath = relativeWebPath.replace(/\/\/+/g, "/");
183
- // Correct the relativeWebPath to ensure it does not start with a "/"
184
- const adjustedRelativeWebPath = cleanRelativeWebPath.startsWith("/")
185
- ? cleanRelativeWebPath.substring(1)
186
- : cleanRelativeWebPath;
187
- return {
188
- bsTarget: `${cleanUrl}/`,
189
- bsPathRewrite: {
190
- "^/": `/${adjustedRelativeWebPath}/`,
191
- },
192
- };
193
- }
194
- async function updatePackageJson(baseDir, answer) {
195
- const packageJsonPath = path.join(baseDir, "package.json");
196
- if (checkExcludeFiles(packageJsonPath)) return;
197
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
198
- packageJson.scripts = {
199
- ...packageJson.scripts,
200
- projectName: "tsx settings/project-name.ts",
201
- };
202
- let answersToInclude = [];
203
- if (answer.tailwindcss) {
204
- packageJson.scripts = {
205
- ...packageJson.scripts,
206
- tailwind:
207
- "postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch",
208
- "tailwind:build":
209
- "postcss src/app/css/tailwind.css -o src/app/css/styles.css",
210
- };
211
- answersToInclude.push("tailwind");
212
- }
213
- if (answer.websocket) {
214
- packageJson.scripts = {
215
- ...packageJson.scripts,
216
- websocket: "tsx settings/restart-websocket.ts",
217
- };
218
- answersToInclude.push("websocket");
219
- }
220
- if (answer.mcp) {
221
- packageJson.scripts = {
222
- ...packageJson.scripts,
223
- mcp: "tsx settings/restart-mcp.ts",
224
- };
225
- answersToInclude.push("mcp");
226
- }
227
- if (answer.docker) {
228
- packageJson.scripts = {
229
- ...packageJson.scripts,
230
- docker: "docker-compose up",
231
- };
232
- answersToInclude.push("docker");
233
- }
234
- if (answer.swaggerDocs) {
235
- const swaggerDocsExecuteScript = answer.prisma
236
- ? "tsx settings/auto-swagger-docs.ts"
237
- : "tsx settings/swagger-config.ts";
238
- packageJson.scripts = {
239
- ...packageJson.scripts,
240
- "create-swagger-docs": swaggerDocsExecuteScript,
241
- };
242
- }
243
- // Initialize with existing scripts
244
- let updatedScripts = {
245
- ...packageJson.scripts,
246
- };
247
- updatedScripts.browserSync = "tsx settings/bs-config.ts";
248
- updatedScripts["browserSync:build"] = "tsx settings/build.ts";
249
- updatedScripts.dev = `npm-run-all projectName -p browserSync ${answersToInclude.join(
250
- " "
251
- )}`;
252
- updatedScripts.build = `npm-run-all${
253
- answer.tailwindcss ? " tailwind:build" : ""
254
- } browserSync:build`;
255
- // Finally, assign the updated scripts back to packageJson
256
- packageJson.scripts = updatedScripts;
257
- packageJson.type = "module";
258
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
259
- }
260
- async function updateComposerJson(baseDir) {
261
- const composerJsonPath = path.join(baseDir, "composer.json");
262
- if (checkExcludeFiles(composerJsonPath)) return;
263
- }
264
- async function updateIndexJsForWebSocket(baseDir, answer) {
265
- if (!answer.websocket) {
266
- return;
267
- }
268
- const indexPath = path.join(baseDir, "src", "app", "js", "index.js");
269
- if (checkExcludeFiles(indexPath)) return;
270
- let indexContent = fs.readFileSync(indexPath, "utf8");
271
- // WebSocket initialization code to be appended
272
- const webSocketCode = `
273
- // WebSocket initialization
274
- var ws = new WebSocket("ws://localhost:8080");
275
- `;
276
- // Append WebSocket code if user chose to use WebSocket
277
- indexContent += webSocketCode;
278
- fs.writeFileSync(indexPath, indexContent, "utf8");
279
- console.log("WebSocket code added to index.js successfully.");
280
- }
281
- function generateAuthSecret() {
282
- // Generate 33 random bytes and encode them as a base64 string
283
- return randomBytes(33).toString("base64");
284
- }
285
- function generateHexEncodedKey(size = 16) {
286
- return randomBytes(size).toString("hex"); // Hex encoding ensures safe session keys
287
- }
288
- // Recursive copy function
289
- function copyRecursiveSync(src, dest, answer) {
290
- const exists = fs.existsSync(src);
291
- const stats = exists && fs.statSync(src);
292
- const isDirectory = exists && stats && stats.isDirectory();
293
- if (isDirectory) {
294
- const destLower = dest.toLowerCase();
295
- if (!answer.websocket && destLower.includes("src\\lib\\websocket")) return;
296
- if (!answer.mcp && destLower.includes("src\\lib\\mcp")) return;
297
- if (
298
- (answer.backendOnly && destLower.includes("src\\app\\js")) ||
299
- (answer.backendOnly && destLower.includes("src\\app\\css")) ||
300
- (answer.backendOnly && destLower.includes("src\\app\\assets"))
301
- )
302
- return;
303
- if (!answer.swaggerDocs && destLower.includes("src\\app\\swagger-docs"))
304
- return;
305
- const destModified = dest.replace(/\\/g, "/");
306
- if (updateAnswer?.excludeFilePath?.includes(destModified)) return;
307
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
308
- fs.readdirSync(src).forEach((childItemName) => {
309
- copyRecursiveSync(
310
- path.join(src, childItemName),
311
- path.join(dest, childItemName),
312
- answer
313
- );
314
- });
315
- } else {
316
- if (checkExcludeFiles(dest)) return;
317
- if (
318
- !answer.tailwindcss &&
319
- (dest.includes("tailwind.css") || dest.includes("styles.css"))
320
- )
321
- return;
322
- if (!answer.websocket && dest.includes("restart-websocket.ts")) return;
323
- if (!answer.mcp && dest.includes("restart-mcp.ts")) return;
324
- if (!answer.docker && dockerFiles.some((file) => dest.includes(file)))
325
- return;
326
- if (
327
- answer.backendOnly &&
328
- nonBackendFiles.some((file) => dest.includes(file))
329
- )
330
- return;
331
- if (!answer.backendOnly && dest.includes("route.php")) return;
332
- if (
333
- answer.backendOnly &&
334
- !answer.swaggerDocs &&
335
- dest.includes("layout.php")
336
- )
337
- return;
338
- if (!answer.swaggerDocs && dest.includes("swagger-config.ts")) return;
339
- if (answer.tailwindcss && dest.includes("index.css")) return;
340
- if (
341
- (!answer.swaggerDocs || !answer.prisma) &&
342
- (dest.includes("auto-swagger-docs.ts") ||
343
- dest.includes("prisma-schema-config.json"))
344
- )
345
- return;
346
- fs.copyFileSync(src, dest, 0);
347
- }
348
- }
349
- // Function to execute the recursive copy for entire directories
350
- async function executeCopy(baseDir, directoriesToCopy, answer) {
351
- directoriesToCopy.forEach(({ src: srcDir, dest: destDir }) => {
352
- const sourcePath = path.join(__dirname, srcDir);
353
- const destPath = path.join(baseDir, destDir);
354
- copyRecursiveSync(sourcePath, destPath, answer);
355
- });
356
- }
357
- function modifyPostcssConfig(baseDir) {
358
- const filePath = path.join(baseDir, "postcss.config.js");
359
- if (checkExcludeFiles(filePath)) return;
360
- const newContent = `export default {
361
- plugins: {
362
- "@tailwindcss/postcss": {},
363
- cssnano: {},
364
- },
365
- };`;
366
- fs.writeFileSync(filePath, newContent, { flag: "w" });
367
- console.log(chalk.green("postcss.config.js updated successfully."));
368
- }
369
- function modifyLayoutPHP(baseDir, answer) {
370
- const layoutPath = path.join(baseDir, "src", "app", "layout.php");
371
- if (checkExcludeFiles(layoutPath)) return;
372
- try {
373
- let indexContent = fs.readFileSync(layoutPath, "utf8");
374
- let stylesAndLinks = "";
375
- if (!answer.backendOnly) {
376
- if (!answer.tailwindcss) {
377
- stylesAndLinks = `\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />`;
378
- }
379
- stylesAndLinks += `\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"></script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"></script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"></script>`;
380
- }
381
- // Tailwind CSS link or CDN script
382
- let tailwindLink = "";
383
- if (!answer.backendOnly) {
384
- tailwindLink = answer.tailwindcss
385
- ? ` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${stylesAndLinks}`
386
- : stylesAndLinks;
387
- }
388
- // Insert before the closing </head> tag
389
- indexContent = indexContent.replace(
390
- "</head>",
391
- `${tailwindLink}
392
- </head>`
393
- );
394
- fs.writeFileSync(layoutPath, indexContent, { flag: "w" });
395
- console.log(
396
- chalk.green(
397
- `layout.php modified successfully for ${
398
- answer.tailwindcss ? "local Tailwind CSS" : "Tailwind CSS CDN"
399
- }.`
400
- )
401
- );
402
- } catch (error) {
403
- console.error(chalk.red("Error modifying layout.php:"), error);
404
- }
405
- }
406
- // This function updates or creates the .env file
407
- async function createOrUpdateEnvFile(baseDir, content) {
408
- const envPath = path.join(baseDir, ".env");
409
- if (checkExcludeFiles(envPath)) return;
410
- console.log("🚀 ~ content:", content);
411
- fs.writeFileSync(envPath, content, { flag: "w" });
412
- }
413
- function checkExcludeFiles(destPath) {
414
- if (!updateAnswer?.isUpdate) return false;
415
- return (
416
- updateAnswer?.excludeFilePath?.includes(destPath.replace(/\\/g, "/")) ??
417
- false
418
- );
419
- }
420
- async function createDirectoryStructure(baseDir, answer) {
421
- console.log("🚀 ~ baseDir:", baseDir);
422
- console.log("🚀 ~ answer:", answer);
423
- const filesToCopy = [
424
- { src: "/bootstrap.php", dest: "/bootstrap.php" },
425
- { src: "/.htaccess", dest: "/.htaccess" },
426
- { src: "/tsconfig.json", dest: "/tsconfig.json" },
427
- { src: "/app-gitignore", dest: "/.gitignore" },
428
- ];
429
- if (answer.tailwindcss) {
430
- filesToCopy.push({ src: "/postcss.config.js", dest: "/postcss.config.js" });
431
- }
432
- const directoriesToCopy = [
433
- {
434
- src: "/settings",
435
- dest: "/settings",
436
- },
437
- {
438
- src: "/src",
439
- dest: "/src",
440
- },
441
- ];
442
- if (answer.docker) {
443
- directoriesToCopy.push(
444
- { src: "/.dockerignore", dest: "/.dockerignore" },
445
- { src: "/docker-compose.yml", dest: "/docker-compose.yml" },
446
- { src: "/Dockerfile", dest: "/Dockerfile" },
447
- { src: "/apache.conf", dest: "/apache.conf" }
448
- );
449
- }
450
- console.log("🚀 ~ directoriesToCopy:", directoriesToCopy);
451
- filesToCopy.forEach(({ src, dest }) => {
452
- const sourcePath = path.join(__dirname, src);
453
- const destPath = path.join(baseDir, dest);
454
- if (checkExcludeFiles(destPath)) return;
455
- const code = fs.readFileSync(sourcePath, "utf8");
456
- fs.writeFileSync(destPath, code, { flag: "w" });
457
- });
458
- await executeCopy(baseDir, directoriesToCopy, answer);
459
- await updatePackageJson(baseDir, answer);
460
- await updateComposerJson(baseDir);
461
- if (!answer.backendOnly) {
462
- await updateIndexJsForWebSocket(baseDir, answer);
463
- }
464
- if (answer.tailwindcss) {
465
- modifyPostcssConfig(baseDir);
466
- }
467
- if (answer.tailwindcss || !answer.backendOnly || answer.swaggerDocs) {
468
- modifyLayoutPHP(baseDir, answer);
469
- }
470
- const authSecret = generateAuthSecret();
471
- const localStoreKey = generateHexEncodedKey();
472
- const authCookieName = generateHexEncodedKey(8);
473
- const functionCallSecret = generateHexEncodedKey(32);
474
- const prismaPHPEnvContent = `# Authentication secret key for JWT or session encryption.
475
- AUTH_SECRET="${authSecret}"
476
- # Name of the authentication cookie.
477
- AUTH_COOKIE_NAME="${authCookieName}"
478
-
479
- # PHPMailer SMTP configuration (uncomment and set as needed)
480
- # SMTP_HOST="smtp.gmail.com" # Your SMTP host
481
- # SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username
482
- # SMTP_PASSWORD="123456" # Your SMTP password
483
- # SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port
484
- # SMTP_ENCRYPTION="ssl" # ssl or tls
485
- # MAIL_FROM="john.doe@gmail.com" # Sender email address
486
- # MAIL_FROM_NAME="John Doe" # Sender name
487
-
488
- # Show errors in the browser (development only). Set to false in production.
489
- SHOW_ERRORS="true"
490
-
491
- # Application timezone (default: UTC)
492
- APP_TIMEZONE="UTC"
493
-
494
- # Application environment (development or production)
495
- APP_ENV="development"
496
-
497
- # Enable or disable application cache (default: false)
498
- CACHE_ENABLED="false"
499
- # Cache time-to-live in seconds (default: 600)
500
- CACHE_TTL="600"
501
-
502
- # Local storage key for browser storage (auto-generated if not set).
503
- # Spaces will be replaced with underscores and converted to lowercase.
504
- LOCALSTORE_KEY="${localStoreKey}"
505
-
506
- # Secret key for encrypting function calls.
507
- FUNCTION_CALL_SECRET="${functionCallSecret}"
508
-
509
- # Single or multiple origins (CSV or JSON array)
510
- CORS_ALLOWED_ORIGINS=[]
511
-
512
- # If you need cookies/Authorization across origins, keep this true
513
- CORS_ALLOW_CREDENTIALS="true"
514
-
515
- # Optional tuning
516
- CORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"
517
- CORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"
518
- CORS_EXPOSE_HEADERS=""
519
- CORS_MAX_AGE="86400"`;
520
- if (answer.prisma) {
521
- const prismaEnvContent = `# Environment variables declared in this file are automatically made available to Prisma.
522
- # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
523
-
524
- # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
525
- # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
526
-
527
- DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"`;
528
- const envContent = `${prismaEnvContent}\n\n${prismaPHPEnvContent}`;
529
- await createOrUpdateEnvFile(baseDir, envContent);
530
- } else {
531
- await createOrUpdateEnvFile(baseDir, prismaPHPEnvContent);
532
- }
533
- }
534
- async function getAnswer(predefinedAnswers = {}) {
535
- console.log("🚀 ~ predefinedAnswers:", predefinedAnswers);
536
- // If starter kit is specified, use non-interactive mode
537
- if (predefinedAnswers.starterKit) {
538
- const selectedKit = predefinedAnswers.starterKit;
539
- let starterKit = null;
540
- // Check built-in starter kits
541
- if (STARTER_KITS[selectedKit]) {
542
- starterKit = STARTER_KITS[selectedKit];
543
- }
544
- if (starterKit) {
545
- console.log(chalk.blue(`Using starter kit: ${starterKit.name}`));
546
- console.log(chalk.gray(starterKit.description));
547
- const answer = {
548
- projectName: predefinedAnswers.projectName ?? "my-app",
549
- starterKit: selectedKit,
550
- starterKitSource: predefinedAnswers.starterKitSource,
551
- backendOnly: starterKit.features.backendOnly ?? false,
552
- tailwindcss: starterKit.features.tailwindcss ?? false,
553
- websocket: starterKit.features.websocket ?? false,
554
- prisma: starterKit.features.prisma ?? false,
555
- docker: starterKit.features.docker ?? false,
556
- swaggerDocs: starterKit.features.swaggerDocs ?? false,
557
- mcp: starterKit.features.mcp ?? false,
558
- };
559
- // Allow CLI overrides
560
- const args = process.argv.slice(2);
561
- if (args.includes("--backend-only")) answer.backendOnly = true;
562
- if (args.includes("--swagger-docs")) answer.swaggerDocs = true;
563
- if (args.includes("--tailwindcss")) answer.tailwindcss = true;
564
- if (args.includes("--websocket")) answer.websocket = true;
565
- if (args.includes("--mcp")) answer.mcp = true;
566
- if (args.includes("--prisma")) answer.prisma = true;
567
- if (args.includes("--docker")) answer.docker = true;
568
- return answer; // ✅ Return immediately - no interactive prompts
569
- }
570
- // Handle custom starter kit
571
- else if (predefinedAnswers.starterKitSource) {
572
- console.log(
573
- chalk.blue(
574
- `Using custom starter kit from: ${predefinedAnswers.starterKitSource}`
575
- )
576
- );
577
- const answer = {
578
- projectName: predefinedAnswers.projectName ?? "my-app",
579
- starterKit: selectedKit,
580
- starterKitSource: predefinedAnswers.starterKitSource,
581
- // Default features - will be overridden by starter kit config
582
- backendOnly: false,
583
- tailwindcss: true,
584
- websocket: false,
585
- prisma: true,
586
- docker: false,
587
- swaggerDocs: true,
588
- mcp: false,
589
- };
590
- // Allow CLI overrides
591
- const args = process.argv.slice(2);
592
- if (args.includes("--backend-only")) answer.backendOnly = true;
593
- if (args.includes("--swagger-docs")) answer.swaggerDocs = true;
594
- if (args.includes("--tailwindcss")) answer.tailwindcss = true;
595
- if (args.includes("--websocket")) answer.websocket = true;
596
- if (args.includes("--mcp")) answer.mcp = true;
597
- if (args.includes("--prisma")) answer.prisma = true;
598
- if (args.includes("--docker")) answer.docker = true;
599
- return answer; // ✅ Return immediately - no interactive prompts
600
- }
601
- }
602
- const questionsArray = [];
603
- // Ask for project name if not provided
604
- if (!predefinedAnswers.projectName) {
605
- questionsArray.push({
606
- type: "text",
607
- name: "projectName",
608
- message: "What is your project named?",
609
- initial: "my-app",
610
- });
611
- }
612
- // IMPORTANT: skip asking backendOnly if updateAnswer.isUpdate is true
613
- if (!predefinedAnswers.backendOnly && !updateAnswer?.isUpdate) {
614
- questionsArray.push({
615
- type: "toggle",
616
- name: "backendOnly",
617
- message: `Would you like to create a ${chalk.blue(
618
- "backend-only project"
619
- )}?`,
620
- initial: false,
621
- active: "Yes",
622
- inactive: "No",
623
- });
624
- }
625
- const onCancel = () => {
626
- console.log(chalk.red("Operation cancelled by the user."));
627
- process.exit(0);
628
- };
629
- const initialResponse = await prompts(questionsArray, { onCancel });
630
- console.log("🚀 ~ initialResponse:", initialResponse);
631
- const nonBackendOnlyQuestionsArray = [];
632
- const isBackendOnly =
633
- initialResponse.backendOnly ?? predefinedAnswers.backendOnly ?? false;
634
- if (isBackendOnly) {
635
- // For backend-only project (skip Tailwind), but still ask other features
636
- if (!predefinedAnswers.swaggerDocs) {
637
- nonBackendOnlyQuestionsArray.push({
638
- type: "toggle",
639
- name: "swaggerDocs",
640
- message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
641
- initial: false,
642
- active: "Yes",
643
- inactive: "No",
644
- });
645
- }
646
- if (!predefinedAnswers.websocket) {
647
- nonBackendOnlyQuestionsArray.push({
648
- type: "toggle",
649
- name: "websocket",
650
- message: `Would you like to use ${chalk.blue("Websocket")}?`,
651
- initial: false,
652
- active: "Yes",
653
- inactive: "No",
654
- });
655
- }
656
- if (!predefinedAnswers.mcp) {
657
- nonBackendOnlyQuestionsArray.push({
658
- type: "toggle",
659
- name: "mcp",
660
- message: `Would you like to use ${chalk.blue(
661
- "MCP (Model Context Protocol)"
662
- )}?`,
663
- initial: false,
664
- active: "Yes",
665
- inactive: "No",
666
- });
667
- }
668
- if (!predefinedAnswers.prisma) {
669
- nonBackendOnlyQuestionsArray.push({
670
- type: "toggle",
671
- name: "prisma",
672
- message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
673
- initial: false,
674
- active: "Yes",
675
- inactive: "No",
676
- });
677
- }
678
- if (!predefinedAnswers.docker) {
679
- nonBackendOnlyQuestionsArray.push({
680
- type: "toggle",
681
- name: "docker",
682
- message: `Would you like to use ${chalk.blue("Docker")}?`,
683
- initial: false,
684
- active: "Yes",
685
- inactive: "No",
686
- });
687
- }
688
- } else {
689
- // For full-stack project, include Tailwind
690
- if (!predefinedAnswers.swaggerDocs) {
691
- nonBackendOnlyQuestionsArray.push({
692
- type: "toggle",
693
- name: "swaggerDocs",
694
- message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
695
- initial: false,
696
- active: "Yes",
697
- inactive: "No",
698
- });
699
- }
700
- if (!predefinedAnswers.tailwindcss) {
701
- nonBackendOnlyQuestionsArray.push({
702
- type: "toggle",
703
- name: "tailwindcss",
704
- message: `Would you like to use ${chalk.blue("Tailwind CSS")}?`,
705
- initial: false,
706
- active: "Yes",
707
- inactive: "No",
708
- });
709
- }
710
- if (!predefinedAnswers.websocket) {
711
- nonBackendOnlyQuestionsArray.push({
712
- type: "toggle",
713
- name: "websocket",
714
- message: `Would you like to use ${chalk.blue("Websocket")}?`,
715
- initial: false,
716
- active: "Yes",
717
- inactive: "No",
718
- });
719
- }
720
- if (!predefinedAnswers.mcp) {
721
- nonBackendOnlyQuestionsArray.push({
722
- type: "toggle",
723
- name: "mcp",
724
- message: `Would you like to use ${chalk.blue(
725
- "MCP (Model Context Protocol)"
726
- )}?`,
727
- initial: false,
728
- active: "Yes",
729
- inactive: "No",
730
- });
731
- }
732
- if (!predefinedAnswers.prisma) {
733
- nonBackendOnlyQuestionsArray.push({
734
- type: "toggle",
735
- name: "prisma",
736
- message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
737
- initial: false,
738
- active: "Yes",
739
- inactive: "No",
740
- });
741
- }
742
- if (!predefinedAnswers.docker) {
743
- nonBackendOnlyQuestionsArray.push({
744
- type: "toggle",
745
- name: "docker",
746
- message: `Would you like to use ${chalk.blue("Docker")}?`,
747
- initial: false,
748
- active: "Yes",
749
- inactive: "No",
750
- });
751
- }
752
- }
753
- const nonBackendOnlyResponse = await prompts(nonBackendOnlyQuestionsArray, {
754
- onCancel,
755
- });
756
- console.log("🚀 ~ nonBackendOnlyResponse:", nonBackendOnlyResponse);
757
- return {
758
- projectName: initialResponse.projectName
759
- ? String(initialResponse.projectName).trim().replace(/ /g, "-")
760
- : predefinedAnswers.projectName ?? "my-app",
761
- backendOnly:
762
- initialResponse.backendOnly ?? predefinedAnswers.backendOnly ?? false,
763
- swaggerDocs:
764
- nonBackendOnlyResponse.swaggerDocs ??
765
- predefinedAnswers.swaggerDocs ??
766
- false,
767
- tailwindcss:
768
- nonBackendOnlyResponse.tailwindcss ??
769
- predefinedAnswers.tailwindcss ??
770
- false,
771
- websocket:
772
- nonBackendOnlyResponse.websocket ?? predefinedAnswers.websocket ?? false,
773
- mcp: nonBackendOnlyResponse.mcp ?? predefinedAnswers.mcp ?? false,
774
- prisma: nonBackendOnlyResponse.prisma ?? predefinedAnswers.prisma ?? false,
775
- docker: nonBackendOnlyResponse.docker ?? predefinedAnswers.docker ?? false,
776
- };
777
- }
778
- async function uninstallNpmDependencies(baseDir, dependencies, isDev = false) {
779
- console.log("Uninstalling dependencies:");
780
- dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
781
- // Prepare the npm uninstall command with the appropriate flag for dev dependencies
782
- const npmUninstallCommand = `npm uninstall ${
783
- isDev ? "--save-dev" : "--save"
784
- } ${dependencies.join(" ")}`;
785
- // Execute the npm uninstall command
786
- execSync(npmUninstallCommand, {
787
- stdio: "inherit",
788
- cwd: baseDir,
789
- });
790
- }
791
- async function uninstallComposerDependencies(baseDir, dependencies) {
792
- console.log("Uninstalling Composer dependencies:");
793
- dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
794
- // Prepare the composer remove command
795
- const composerRemoveCommand = `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${dependencies.join(
796
- " "
797
- )}`;
798
- // Execute the composer remove command
799
- execSync(composerRemoveCommand, {
800
- stdio: "inherit",
801
- cwd: baseDir,
802
- });
803
- }
804
- function fetchPackageVersion(packageName) {
805
- return new Promise((resolve, reject) => {
806
- https
807
- .get(`https://registry.npmjs.org/${packageName}`, (res) => {
808
- let data = "";
809
- res.on("data", (chunk) => (data += chunk));
810
- res.on("end", () => {
811
- try {
812
- const parsed = JSON.parse(data);
813
- resolve(parsed["dist-tags"].latest);
814
- } catch (error) {
815
- reject(new Error("Failed to parse JSON response"));
816
- }
817
- });
818
- })
819
- .on("error", (err) => reject(err));
820
- });
821
- }
822
- const readJsonFile = (filePath) => {
823
- const jsonData = fs.readFileSync(filePath, "utf8");
824
- return JSON.parse(jsonData);
825
- };
826
- function compareVersions(installedVersion, currentVersion) {
827
- const installedVersionArray = installedVersion.split(".").map(Number);
828
- const currentVersionArray = currentVersion.split(".").map(Number);
829
- for (let i = 0; i < installedVersionArray.length; i++) {
830
- if (installedVersionArray[i] > currentVersionArray[i]) {
831
- return 1;
832
- } else if (installedVersionArray[i] < currentVersionArray[i]) {
833
- return -1;
834
- }
835
- }
836
- return 0;
837
- }
838
- function getInstalledPackageVersion(packageName) {
839
- try {
840
- const output = execSync(`npm list -g ${packageName} --depth=0`).toString();
841
- const versionMatch = output.match(
842
- new RegExp(`${packageName}@(\\d+\\.\\d+\\.\\d+)`)
843
- );
844
- if (versionMatch) {
845
- return versionMatch[1];
846
- } else {
847
- console.error(`Package ${packageName} is not installed`);
848
- return null;
849
- }
850
- } catch (error) {
851
- console.error(error instanceof Error ? error.message : String(error));
852
- return null;
853
- }
854
- }
2
+ import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],dockerFiles=[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!1,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","src/app/layout.php","src/app/index.php"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/app/layout.php","src/app/index.php","src/app/js/index.js","src/app/css/tailwind.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","docker-compose.yml","Dockerfile"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,websocket:!0,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!0},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/lib/websocket","src/lib/mcp"]},ecommerce:{id:"ecommerce",name:"E-commerce Starter",description:"Full e-commerce application with cart, payments, and admin",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-ecommerce-starter",branch:"main"}},blog:{id:"blog",name:"Blog CMS",description:"Blog content management system",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-blog-starter"}}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),c=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let n=`http://localhost/${c}`;n=n.endsWith("/")?n.slice(0,-1):n;const i=n.replace(/(?<!:)(\/\/+)/g,"/"),o=c.replace(/\/\/+/g,"/");return{bsTarget:`${i}/`,bsPathRewrite:{"^/":`/${o.startsWith("/")?o.substring(1):o}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const c=JSON.parse(fs.readFileSync(t,"utf8"));c.scripts={...c.scripts,projectName:"tsx settings/project-name.ts"};let n=[];if(s.tailwindcss&&(c.scripts={...c.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch","tailwind:build":"postcss src/app/css/tailwind.css -o src/app/css/styles.css"},n.push("tailwind")),s.websocket&&(c.scripts={...c.scripts,websocket:"tsx settings/restart-websocket.ts"},n.push("websocket")),s.mcp&&(c.scripts={...c.scripts,mcp:"tsx settings/restart-mcp.ts"},n.push("mcp")),s.docker&&(c.scripts={...c.scripts,docker:"docker-compose up"},n.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";c.scripts={...c.scripts,"create-swagger-docs":e}}let i={...c.scripts};i.browserSync="tsx settings/bs-config.ts",i["browserSync:build"]="tsx settings/build.ts",i.dev=`npm-run-all projectName -p browserSync ${n.join(" ")}`,i.build=`npm-run-all${s.tailwindcss?" tailwind:build":""} browserSync:build`,c.scripts=i,c.type="module",fs.writeFileSync(t,JSON.stringify(c,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let c=fs.readFileSync(t,"utf8");c+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,c,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const c=fs.existsSync(e),n=c&&fs.statSync(e);if(c&&n&&n.isDirectory()){const c=s.toLowerCase();if(!t.websocket&&c.includes("src\\lib\\websocket"))return;if(!t.mcp&&c.includes("src\\lib\\mcp"))return;if(t.backendOnly&&c.includes("src\\app\\js")||t.backendOnly&&c.includes("src\\app\\css")||t.backendOnly&&c.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&c.includes("src\\app\\swagger-docs"))return;const n=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(n))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((c=>{copyRecursiveSync(path.join(e,c),path.join(s,c),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&s.includes("restart-websocket.ts"))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(!t.docker&&dockerFiles.some((e=>s.includes(e))))return;if(t.backendOnly&&nonBackendFiles.some((e=>s.includes(e))))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({src:s,dest:c})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,c),t)}))}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),c="";s.backendOnly||(s.tailwindcss||(c='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),c+='\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let n="";s.backendOnly||(n=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${c}`:c),e=e.replace("</head>",`${n}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){return!!updateAnswer&&(updateAnswer?.excludeFilePath?.includes(e.replace(/\\/g,"/"))??!1)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const c=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&c.push({src:"/.dockerignore",dest:"/.dockerignore"},{src:"/docker-compose.yml",dest:"/docker-compose.yml"},{src:"/Dockerfile",dest:"/Dockerfile"},{src:"/apache.conf",dest:"/apache.conf"}),t.forEach((({src:s,dest:t})=>{const c=path.join(__dirname,s),n=path.join(e,t);if(checkExcludeFiles(n))return;const i=fs.readFileSync(c,"utf8");fs.writeFileSync(n,i,{flag:"w"})})),await executeCopy(e,c,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const n=generateAuthSecret(),i=generateHexEncodedKey(),o=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${n}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# PHPMailer SMTP configuration (uncomment and set as needed)\n# SMTP_HOST="smtp.gmail.com" # Your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username\n# SMTP_PASSWORD="123456" # Your SMTP password\n# SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port\n# SMTP_ENCRYPTION="ssl" # ssl or tls\n# MAIL_FROM="john.doe@gmail.com" # Sender email address\n# MAIL_FROM_NAME="John Doe" # Sender name\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Local storage key for browser storage (auto-generated if not set).\n# Spaces will be replaced with underscores and converted to lowercase.\nLOCALSTORE_KEY="${i}"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins (CSV or JSON array)\nCORS_ALLOWED_ORIGINS=[]\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${o}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,o)}async function getAnswer(e={}){if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const c={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,websocket:t.features.websocket??!1,prisma:t.features.prisma??!1,docker:t.features.docker??!1,swaggerDocs:t.features.swaggerDocs??!1,mcp:t.features.mcp??!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(c.backendOnly=!0),n.includes("--swagger-docs")&&(c.swaggerDocs=!0),n.includes("--tailwindcss")&&(c.tailwindcss=!0),n.includes("--websocket")&&(c.websocket=!0),n.includes("--mcp")&&(c.mcp=!0),n.includes("--prisma")&&(c.prisma=!0),n.includes("--docker")&&(c.docker=!0),c}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},c=process.argv.slice(2);return c.includes("--backend-only")&&(t.backendOnly=!0),c.includes("--swagger-docs")&&(t.swaggerDocs=!0),c.includes("--tailwindcss")&&(t.tailwindcss=!0),c.includes("--websocket")&&(t.websocket=!0),c.includes("--mcp")&&(t.mcp=!0),c.includes("--prisma")&&(t.prisma=!0),c.includes("--docker")&&(t.docker=!0),t}}const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||s.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{process.exit(0)},c=await prompts(s,{onCancel:t}),n=[];c.backendOnly??e.backendOnly??!1?(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||n.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(n,{onCancel:t});return{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:c.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,mcp:i.mcp??e.mcp??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallNpmDependencies(e,s,t=!1){s.forEach((e=>{}));const c=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(c,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){s.forEach((e=>{}));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let c="";e.on("data",(e=>c+=e)),e.on("end",(()=>{try{const e=JSON.parse(c);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),c=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>c[e])return 1;if(t[e]<c[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:null}catch(e){return null}}
855
3
  /**
856
4
  * Install dependencies in the specified directory.
857
5
  * @param {string} baseDir - The base directory where to install the dependencies.
@@ -1061,8 +209,8 @@ async function downloadStarterKit(starterKit, tempDir) {
1061
209
  }
1062
210
  async function mergeStarterKitFiles(starterKitPath, projectPath, answer) {
1063
211
  console.log(chalk.blue("Merging starter kit files..."));
1064
- // Copy all files from starter kit, but don't overwrite base files
1065
- copyRecursiveSync(starterKitPath, projectPath, answer);
212
+ // Use the new copy function that respects exclusions
213
+ copyRecursiveSyncWithExclusions(starterKitPath, projectPath, answer, true);
1066
214
  // Look for starter kit specific configuration
1067
215
  const starterKitConfig = path.join(starterKitPath, "starter-kit.json");
1068
216
  if (fs.existsSync(starterKitConfig)) {
@@ -1183,6 +331,82 @@ function showStarterKits() {
1183
331
  );
1184
332
  console.log();
1185
333
  }
334
+ // Starter kit specific copy function that respects exclusions
335
+ function copyRecursiveSyncWithExclusions(
336
+ src,
337
+ dest,
338
+ answer,
339
+ respectExclusions = true
340
+ ) {
341
+ const exists = fs.existsSync(src);
342
+ const stats = exists && fs.statSync(src);
343
+ const isDirectory = exists && stats && stats.isDirectory();
344
+ if (isDirectory) {
345
+ const destLower = dest.toLowerCase();
346
+ // Apply feature-based exclusions
347
+ if (!answer.websocket && destLower.includes("websocket")) return;
348
+ if (!answer.mcp && destLower.includes("mcp")) return;
349
+ if (!answer.swaggerDocs && destLower.includes("swagger-docs")) return;
350
+ if (
351
+ answer.backendOnly &&
352
+ (destLower.includes("js") ||
353
+ destLower.includes("css") ||
354
+ destLower.includes("assets"))
355
+ )
356
+ return;
357
+ // Apply user-defined exclusions
358
+ const destModified = dest.replace(/\\/g, "/");
359
+ if (
360
+ respectExclusions &&
361
+ updateAnswer?.excludeFilePath?.includes(destModified)
362
+ ) {
363
+ console.log(chalk.yellow(`Skipping excluded directory: ${destModified}`));
364
+ return;
365
+ }
366
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
367
+ fs.readdirSync(src).forEach((childItemName) => {
368
+ copyRecursiveSyncWithExclusions(
369
+ path.join(src, childItemName),
370
+ path.join(dest, childItemName),
371
+ answer,
372
+ respectExclusions
373
+ );
374
+ });
375
+ } else {
376
+ // Apply user-defined exclusions for files
377
+ if (respectExclusions && checkExcludeFiles(dest)) {
378
+ console.log(
379
+ chalk.yellow(`Skipping excluded file: ${dest.replace(/\\/g, "/")}`)
380
+ );
381
+ return;
382
+ }
383
+ // Apply feature-based exclusions
384
+ if (
385
+ !answer.tailwindcss &&
386
+ (dest.includes("tailwind.css") || dest.includes("styles.css"))
387
+ )
388
+ return;
389
+ if (!answer.websocket && dest.includes("restart-websocket.ts")) return;
390
+ if (!answer.mcp && dest.includes("restart-mcp.ts")) return;
391
+ if (!answer.docker && dockerFiles.some((file) => dest.includes(file)))
392
+ return;
393
+ if (
394
+ answer.backendOnly &&
395
+ nonBackendFiles.some((file) => dest.includes(file))
396
+ )
397
+ return;
398
+ if (!answer.backendOnly && dest.includes("route.php")) return;
399
+ if (
400
+ answer.backendOnly &&
401
+ !answer.swaggerDocs &&
402
+ dest.includes("layout.php")
403
+ )
404
+ return;
405
+ if (!answer.swaggerDocs && dest.includes("swagger-config.ts")) return;
406
+ if (answer.tailwindcss && dest.includes("index.css")) return;
407
+ fs.copyFileSync(src, dest, 0);
408
+ }
409
+ }
1186
410
  async function main() {
1187
411
  try {
1188
412
  const args = process.argv.slice(2);
@@ -1204,16 +428,31 @@ async function main() {
1204
428
  if (projectName) {
1205
429
  const currentDir = process.cwd();
1206
430
  const configPath = path.join(currentDir, "prisma-php.json");
431
+ const projectNamePath = path.join(currentDir, projectName);
432
+ const projectNameConfigPath = path.join(
433
+ projectNamePath,
434
+ "prisma-php.json"
435
+ );
436
+ // Check if there's an existing config in current directory or project directory
437
+ let existingConfigPath = null;
1207
438
  if (fs.existsSync(configPath)) {
1208
- // It's an update - read existing settings
1209
- const localSettings = readJsonFile(configPath);
439
+ existingConfigPath = configPath;
440
+ } else if (fs.existsSync(projectNameConfigPath)) {
441
+ existingConfigPath = projectNameConfigPath;
442
+ }
443
+ // If we found an existing config and we're using a starter kit, load exclusions
444
+ if (existingConfigPath && (starterKitFromArgs || starterKitSource)) {
445
+ const localSettings = readJsonFile(existingConfigPath);
1210
446
  let excludeFiles = [];
1211
447
  localSettings.excludeFiles?.map((file) => {
1212
- const filePath = path.join(currentDir, file);
448
+ const filePath = path.join(
449
+ existingConfigPath === configPath ? currentDir : projectNamePath,
450
+ file
451
+ );
1213
452
  if (fs.existsSync(filePath))
1214
453
  excludeFiles.push(filePath.replace(/\\/g, "/"));
1215
454
  });
1216
- // Set updateAnswer with OLD settings initially (for checkExcludeFiles function)
455
+ // Set updateAnswer to respect exclusions during starter kit setup
1217
456
  updateAnswer = {
1218
457
  projectName,
1219
458
  backendOnly: localSettings.backendOnly,
@@ -1223,10 +462,11 @@ async function main() {
1223
462
  mcp: localSettings.mcp,
1224
463
  prisma: localSettings.prisma,
1225
464
  docker: localSettings.docker,
1226
- isUpdate: true,
465
+ isUpdate: false, // Not a true update, but we need exclusions to work
1227
466
  excludeFiles: localSettings.excludeFiles ?? [],
1228
467
  excludeFilePath: excludeFiles ?? [],
1229
- filePath: currentDir,
468
+ filePath:
469
+ existingConfigPath === configPath ? currentDir : projectNamePath,
1230
470
  };
1231
471
  // For updates, use existing settings but allow CLI overrides
1232
472
  const predefinedAnswers = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-prisma-php-app",
3
- "version": "4.0.0-alpha.32",
3
+ "version": "4.0.0-alpha.34",
4
4
  "description": "Prisma-PHP: A Revolutionary Library Bridging PHP with Prisma ORM",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",