create-prisma-php-app 4.0.0-alpha.4 → 4.0.0-alpha.41

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 (60) hide show
  1. package/dist/.htaccess +54 -41
  2. package/dist/bootstrap.php +143 -98
  3. package/dist/index.js +1628 -142
  4. package/dist/settings/auto-swagger-docs.ts +196 -95
  5. package/dist/settings/bs-config.ts +53 -58
  6. package/dist/settings/files-list.json +1 -1
  7. package/dist/settings/project-name.ts +2 -0
  8. package/dist/settings/restart-mcp.ts +58 -0
  9. package/dist/settings/restart-websocket.ts +51 -45
  10. package/dist/settings/utils.ts +240 -0
  11. package/dist/src/Lib/AI/ChatGPTClient.php +147 -0
  12. package/dist/src/Lib/Auth/Auth.php +544 -0
  13. package/dist/src/Lib/Auth/AuthConfig.php +89 -0
  14. package/dist/src/Lib/CacheHandler.php +121 -0
  15. package/dist/src/Lib/ErrorHandler.php +322 -0
  16. package/dist/src/Lib/FileManager/UploadFile.php +383 -0
  17. package/dist/src/Lib/Headers/Boom.php +192 -0
  18. package/dist/src/Lib/IncludeTracker.php +59 -0
  19. package/dist/src/Lib/MCP/WeatherTools.php +104 -0
  20. package/dist/src/Lib/MCP/mcp-server.php +80 -0
  21. package/dist/src/Lib/MainLayout.php +230 -0
  22. package/dist/src/Lib/Middleware/AuthMiddleware.php +157 -0
  23. package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
  24. package/dist/src/Lib/PHPMailer/Mailer.php +169 -0
  25. package/dist/src/Lib/PHPX/Exceptions/ComponentValidationException.php +49 -0
  26. package/dist/src/Lib/PHPX/Fragment.php +32 -0
  27. package/dist/src/Lib/PHPX/IPHPX.php +22 -0
  28. package/dist/src/Lib/PHPX/PHPX.php +287 -0
  29. package/dist/src/Lib/PHPX/TemplateCompiler.php +641 -0
  30. package/dist/src/Lib/PHPX/TwMerge.php +346 -0
  31. package/dist/src/Lib/PHPX/TypeCoercer.php +490 -0
  32. package/dist/src/Lib/PartialRenderer.php +40 -0
  33. package/dist/src/Lib/PrismaPHPSettings.php +181 -0
  34. package/dist/src/Lib/Request.php +479 -0
  35. package/dist/src/Lib/Security/RateLimiter.php +33 -0
  36. package/dist/src/Lib/Set.php +102 -0
  37. package/dist/src/Lib/StateManager.php +127 -0
  38. package/dist/src/Lib/Validator.php +752 -0
  39. package/dist/src/{Websocket → Lib/Websocket}/ConnectionManager.php +1 -1
  40. package/dist/src/Lib/Websocket/websocket-server.php +118 -0
  41. package/dist/src/app/error.php +1 -1
  42. package/dist/src/app/index.php +22 -5
  43. package/dist/src/app/js/index.js +1 -1
  44. package/dist/src/app/layout.php +2 -2
  45. package/package.json +1 -1
  46. package/dist/settings/restart-websocket.bat +0 -28
  47. package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
  48. package/dist/websocket-server.php +0 -22
  49. package/vendor/autoload.php +0 -25
  50. package/vendor/composer/ClassLoader.php +0 -579
  51. package/vendor/composer/InstalledVersions.php +0 -359
  52. package/vendor/composer/LICENSE +0 -21
  53. package/vendor/composer/autoload_classmap.php +0 -10
  54. package/vendor/composer/autoload_namespaces.php +0 -9
  55. package/vendor/composer/autoload_psr4.php +0 -10
  56. package/vendor/composer/autoload_real.php +0 -38
  57. package/vendor/composer/autoload_static.php +0 -25
  58. package/vendor/composer/installed.json +0 -825
  59. package/vendor/composer/installed.php +0 -132
  60. package/vendor/composer/platform_check.php +0 -26
package/dist/index.js CHANGED
@@ -1,5 +1,857 @@
1
1
  #!/usr/bin/env node
2
- import{execSync}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"];function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let c=`http://localhost/${n}`;c=c.endsWith("/")?c.slice(0,-1):c;const o=c.replace(/(?<!:)(\/\/+)/g,"/"),i=n.replace(/\/\/+/g,"/");return{bsTarget:`${o}/`,bsPathRewrite:{"^/":`/${i.startsWith("/")?i.substring(1):i}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let c=[];if(s.tailwindcss&&(n.scripts={...n.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"},c.push("tailwind")),s.websocket&&(n.scripts={...n.scripts,websocket:"tsx settings/restart-websocket.ts"},c.push("websocket")),s.docker&&(n.scripts={...n.scripts,docker:"docker-compose up"},c.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";n.scripts={...n.scripts,"create-swagger-docs":e}}let o={...n.scripts};o.browserSync="tsx settings/bs-config.ts",o["browserSync:build"]="tsx settings/build.ts",o.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`,o.build=`npm-run-all${s.tailwindcss?" tailwind:build":""} browserSync:build`,n.scripts=o,n.type="module",fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e){const s=path.join(e,"composer.json");if(checkExcludeFiles(s))return;let t;if(fs.existsSync(s)){{const e=fs.readFileSync(s,"utf8");t=JSON.parse(e)}t.autoload={"psr-4":{"":"src/"}},t.version="1.0.0",fs.writeFileSync(s,JSON.stringify(t,null,2))}}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,n,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),c=n&&fs.statSync(e);if(n&&c&&c.isDirectory()){const n=s.toLowerCase();if(!t.websocket&&n.includes("src\\websocket"))return;if(t.backendOnly&&n.includes("src\\app\\js")||t.backendOnly&&n.includes("src\\app\\css")||t.backendOnly&&n.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&n.includes("src\\app\\swagger-docs"))return;const c=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),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")||s.includes("restart-websocket.bat")||s.includes("websocket-server.php")))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:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),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"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),n+='\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 c="";s.backendOnly||(c=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${n}`:n),e=e.replace("</head>",`${c}\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?.isUpdate&&(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"}),s.websocket&&t.push({src:"/websocket-server.php",dest:"/websocket-server.php"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&n.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 n=path.join(__dirname,s),c=path.join(e,t);if(checkExcludeFiles(c))return;const o=fs.readFileSync(n,"utf8");fs.writeFileSync(c,o,{flag:"w"})})),await executeCopy(e,n,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 c=generateAuthSecret(),o=generateHexEncodedKey(),i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${c}"\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="${o}"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"`;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${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={}){const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||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)},n=await prompts(s,{onCancel:t}),c=[];n.backendOnly||e.backendOnly?(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!0,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!0,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const o=await prompts(c,{onCancel:t});return{projectName:n.projectName?String(n.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:n.backendOnly??e.backendOnly??!1,swaggerDocs:o.swaggerDocs??e.swaggerDocs??!1,tailwindcss:o.tailwindcss??e.tailwindcss??!1,websocket:o.websocket??e.websocket??!1,prisma:o.prisma??e.prisma??!1,docker:o.docker??e.docker??!1}}async function uninstallNpmDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{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 n="";e.on("data",(e=>n+=e)),e.on("end",(()=>{try{const e=JSON.parse(n);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),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[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}}
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) 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
+ }
3
855
  /**
4
856
  * Install dependencies in the specified directory.
5
857
  * @param {string} baseDir - The base directory where to install the dependencies.
@@ -7,7 +859,11 @@ import{execSync}from"child_process";import fs from"fs";import{fileURLToPath}from
7
859
  * @param {boolean} [isDev=false] - Whether to install the dependencies as devDependencies.
8
860
  */
9
861
  async function installNpmDependencies(baseDir, dependencies, isDev = false) {
10
- console.log("Initializing new Node.js project...");
862
+ if (!fs.existsSync(path.join(baseDir, "package.json"))) {
863
+ console.log("Initializing new Node.js project...");
864
+ } else {
865
+ console.log("Updating existing Node.js project...");
866
+ }
11
867
  // Initialize a package.json if it doesn't exist
12
868
  if (!fs.existsSync(path.join(baseDir, "package.json"))) {
13
869
  execSync("npm init -y", {
@@ -32,27 +888,99 @@ async function installNpmDependencies(baseDir, dependencies, isDev = false) {
32
888
  cwd: baseDir,
33
889
  });
34
890
  }
35
- async function installComposerDependencies(baseDir, dependencies) {
36
- console.log("Initializing new Composer project...");
37
- // Initialize a composer.json if it doesn't exist
38
- if (!fs.existsSync(path.join(baseDir, "composer.json"))) {
891
+ function getComposerCmd() {
892
+ try {
893
+ execSync("composer --version", { stdio: "ignore" });
894
+ return { cmd: "composer", baseArgs: [] };
895
+ } catch {
896
+ return {
897
+ cmd: "C:\\xampp\\php\\php.exe",
898
+ baseArgs: ["C:\\ProgramData\\ComposerSetup\\bin\\composer.phar"],
899
+ };
900
+ }
901
+ }
902
+ export async function installComposerDependencies(baseDir, dependencies) {
903
+ const { cmd, baseArgs } = getComposerCmd();
904
+ const composerJsonPath = path.join(baseDir, "composer.json");
905
+ const existsAlready = fs.existsSync(composerJsonPath);
906
+ console.log(
907
+ chalk.green(
908
+ `Composer project initialization: ${
909
+ existsAlready ? "Updating existing project…" : "Setting up new project…"
910
+ }`
911
+ )
912
+ );
913
+ /* ------------------------------------------------------------------ */
914
+ /* 1. Try composer init (quietly fall back if it fails) */
915
+ /* ------------------------------------------------------------------ */
916
+ if (!existsAlready) {
917
+ const initArgs = [
918
+ ...baseArgs,
919
+ "init",
920
+ "--no-interaction",
921
+ "--name",
922
+ "tsnc/prisma-php-app",
923
+ "--require",
924
+ "php:^8.2",
925
+ "--type",
926
+ "project",
927
+ "--version",
928
+ "1.0.0",
929
+ ];
930
+ const res = spawnSync(cmd, initArgs, { cwd: baseDir });
931
+ if (res.status !== 0) {
932
+ // Silent fallback: no logs, just write a minimal composer.json
933
+ fs.writeFileSync(
934
+ composerJsonPath,
935
+ JSON.stringify(
936
+ {
937
+ name: "tsnc/prisma-php-app",
938
+ type: "project",
939
+ version: "1.0.0",
940
+ require: { php: "^8.2" },
941
+ autoload: { "psr-4": { "": "src/" } },
942
+ },
943
+ null,
944
+ 2
945
+ )
946
+ );
947
+ }
948
+ }
949
+ /* 2. Ensure PSR-4 autoload entry ---------------------------------- */
950
+ const json = JSON.parse(fs.readFileSync(composerJsonPath, "utf8"));
951
+ json.autoload ??= {};
952
+ json.autoload["psr-4"] ??= {};
953
+ json.autoload["psr-4"][""] ??= "src/";
954
+ fs.writeFileSync(composerJsonPath, JSON.stringify(json, null, 2));
955
+ /* 3. Install dependencies ----------------------------------------- */
956
+ if (dependencies.length) {
957
+ console.log("Installing Composer dependencies:");
958
+ dependencies.forEach((d) => console.log(`- ${chalk.blue(d)}`));
39
959
  execSync(
40
- `composer init -n --name="tsnc/prisma-php-app" --require="php:^8.2"`,
41
- {
42
- stdio: "inherit",
43
- cwd: baseDir,
44
- }
960
+ `${cmd} ${[
961
+ ...baseArgs,
962
+ "require",
963
+ "--no-interaction",
964
+ ...dependencies,
965
+ ].join(" ")}`,
966
+ { stdio: "inherit", cwd: baseDir }
45
967
  );
46
968
  }
47
- // Log the dependencies being installed
48
- console.log("Installing Composer dependencies:");
49
- dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
50
- // Prepare the composer require command
51
- const composerRequireCommand = `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar require ${dependencies.join(
52
- " "
53
- )}`;
54
- // Execute the composer require command
55
- execSync(composerRequireCommand, {
969
+ /* 4. Refresh lock when updating ----------------------------------- */
970
+ if (existsAlready) {
971
+ execSync(
972
+ `${cmd} ${[
973
+ ...baseArgs,
974
+ "update",
975
+ "--lock",
976
+ "--no-install",
977
+ "--no-interaction",
978
+ ].join(" ")}`,
979
+ { stdio: "inherit", cwd: baseDir }
980
+ );
981
+ }
982
+ /* 5. Regenerate autoloader ---------------------------------------- */
983
+ execSync(`${cmd} ${[...baseArgs, "dump-autoload", "--quiet"].join(" ")}`, {
56
984
  stdio: "inherit",
57
985
  cwd: baseDir,
58
986
  });
@@ -60,21 +988,20 @@ async function installComposerDependencies(baseDir, dependencies) {
60
988
  const npmPinnedVersions = {
61
989
  "@tailwindcss/postcss": "^4.1.11",
62
990
  "@types/browser-sync": "^2.29.0",
63
- "@types/node": "^24.0.7",
991
+ "@types/node": "^24.2.1",
64
992
  "@types/prompts": "^2.4.9",
65
993
  "browser-sync": "^3.0.4",
66
- chalk: "^5.4.1",
67
- "chokidar-cli": "^3.0.0",
68
- cssnano: "^7.0.7",
994
+ chalk: "^5.5.0",
995
+ cssnano: "^7.1.0",
69
996
  "http-proxy-middleware": "^3.0.5",
70
997
  "npm-run-all": "^4.1.5",
71
- "php-parser": "^3.2.4",
998
+ "php-parser": "^3.2.5",
72
999
  postcss: "^8.5.6",
73
1000
  "postcss-cli": "^11.0.1",
74
1001
  prompts: "^2.4.2",
75
1002
  tailwindcss: "^4.1.11",
76
1003
  tsx: "^4.20.3",
77
- typescript: "^5.8.3",
1004
+ typescript: "^5.9.2",
78
1005
  };
79
1006
  function npmPkg(name) {
80
1007
  return npmPinnedVersions[name] ? `${name}@${npmPinnedVersions[name]}` : name;
@@ -88,64 +1015,451 @@ const composerPinnedVersions = {
88
1015
  "symfony/uid": "^7.2.0",
89
1016
  "brick/math": "^0.13.1",
90
1017
  "cboden/ratchet": "^0.4.4",
91
- "tsnc/prisma-php": "^1.0",
1018
+ "tsnc/prisma-php": "^1.0.0",
1019
+ "php-mcp/server": "3.3.0",
92
1020
  };
93
1021
  function composerPkg(name) {
94
1022
  return composerPinnedVersions[name]
95
1023
  ? `${name}:${composerPinnedVersions[name]}`
96
1024
  : name;
97
1025
  }
1026
+ async function downloadStarterKit(starterKit, tempDir) {
1027
+ if (!starterKit.source) {
1028
+ throw new Error("No source defined for starter kit");
1029
+ }
1030
+ const { type, url, branch = "main", subfolder } = starterKit.source;
1031
+ switch (type) {
1032
+ case "git":
1033
+ console.log(chalk.blue(`Cloning ${starterKit.name} from ${url}...`));
1034
+ const cloneCommand = branch
1035
+ ? `git clone -b ${branch} --depth 1 ${url} ${tempDir}`
1036
+ : `git clone --depth 1 ${url} ${tempDir}`;
1037
+ execSync(cloneCommand, { stdio: "inherit" });
1038
+ // Remove .git directory
1039
+ const gitDir = path.join(tempDir, ".git");
1040
+ if (fs.existsSync(gitDir)) {
1041
+ fs.rmSync(gitDir, { recursive: true, force: true });
1042
+ }
1043
+ // Return the subfolder if specified
1044
+ return subfolder ? path.join(tempDir, subfolder) : tempDir;
1045
+ case "npm":
1046
+ console.log(chalk.blue(`Downloading ${starterKit.name} from npm...`));
1047
+ execSync(`npm pack ${url}`, { cwd: tempDir, stdio: "inherit" });
1048
+ // Extract the tarball
1049
+ const tarball = fs.readdirSync(tempDir).find((f) => f.endsWith(".tgz"));
1050
+ if (tarball) {
1051
+ execSync(`tar -xzf ${tarball}`, { cwd: tempDir });
1052
+ fs.unlinkSync(path.join(tempDir, tarball));
1053
+ return path.join(tempDir, "package");
1054
+ }
1055
+ throw new Error("Failed to extract npm package");
1056
+ case "url":
1057
+ throw new Error("URL download not implemented yet");
1058
+ default:
1059
+ throw new Error(`Unsupported source type: ${type}`);
1060
+ }
1061
+ }
1062
+ async function mergeStarterKitFiles(starterKitPath, projectPath, answer) {
1063
+ console.log(chalk.blue("Merging starter kit files..."));
1064
+ // Use the new copy function that respects exclusions
1065
+ copyRecursiveSyncWithExclusions(starterKitPath, projectPath, answer, true);
1066
+ // Look for starter kit specific configuration
1067
+ const starterKitConfig = path.join(starterKitPath, "starter-kit.json");
1068
+ if (fs.existsSync(starterKitConfig)) {
1069
+ const config = JSON.parse(fs.readFileSync(starterKitConfig, "utf8"));
1070
+ // Handle post-install scripts
1071
+ if (config.postInstall) {
1072
+ console.log(chalk.blue("Running post-install scripts..."));
1073
+ for (const script of config.postInstall) {
1074
+ console.log(chalk.gray(`Running: ${script}`));
1075
+ execSync(script, { cwd: projectPath, stdio: "inherit" });
1076
+ }
1077
+ }
1078
+ // Handle additional dependencies
1079
+ if (config.additionalNpmDependencies) {
1080
+ await installNpmDependencies(
1081
+ projectPath,
1082
+ config.additionalNpmDependencies.map(npmPkg),
1083
+ true
1084
+ );
1085
+ }
1086
+ if (config.additionalComposerDependencies) {
1087
+ await installComposerDependencies(
1088
+ projectPath,
1089
+ config.additionalComposerDependencies.map(composerPkg)
1090
+ );
1091
+ }
1092
+ }
1093
+ }
1094
+ async function setupStarterKit(baseDir, answer) {
1095
+ if (!answer.starterKit) return;
1096
+ let starterKit = null;
1097
+ // Check if it's a built-in starter kit
1098
+ if (STARTER_KITS[answer.starterKit]) {
1099
+ starterKit = STARTER_KITS[answer.starterKit];
1100
+ }
1101
+ // Handle custom starter kit URL
1102
+ else if (answer.starterKitSource) {
1103
+ starterKit = {
1104
+ id: answer.starterKit,
1105
+ name: `Custom Starter Kit (${answer.starterKit})`,
1106
+ description: "Custom starter kit from external source",
1107
+ features: {}, // Will be determined from the downloaded kit
1108
+ requiredFiles: [],
1109
+ source: {
1110
+ type: "git", // Assume git for now, could be enhanced
1111
+ url: answer.starterKitSource,
1112
+ },
1113
+ };
1114
+ }
1115
+ if (!starterKit) {
1116
+ console.warn(
1117
+ chalk.yellow(`Starter kit '${answer.starterKit}' not found. Skipping...`)
1118
+ );
1119
+ return;
1120
+ }
1121
+ console.log(chalk.green(`Setting up ${starterKit.name}...`));
1122
+ // If it's a custom starter kit with source, download it
1123
+ if (starterKit.source) {
1124
+ const tempDir = path.join(baseDir, ".temp-starter-kit");
1125
+ try {
1126
+ // Create temp directory
1127
+ if (fs.existsSync(tempDir)) {
1128
+ fs.rmSync(tempDir, { recursive: true, force: true });
1129
+ }
1130
+ fs.mkdirSync(tempDir, { recursive: true });
1131
+ // Download the starter kit
1132
+ const kitPath = await downloadStarterKit(starterKit, tempDir);
1133
+ // Merge files from starter kit
1134
+ await mergeStarterKitFiles(kitPath, baseDir, answer);
1135
+ // Check if starter kit has its own configuration
1136
+ const kitConfigPath = path.join(kitPath, "prisma-php-starter.json");
1137
+ if (fs.existsSync(kitConfigPath)) {
1138
+ const kitConfig = JSON.parse(fs.readFileSync(kitConfigPath, "utf8"));
1139
+ // Override features with starter kit configuration
1140
+ Object.assign(answer, kitConfig.features || {});
1141
+ console.log(chalk.green(`Applied starter kit configuration`));
1142
+ }
1143
+ // Clean up temp directory
1144
+ fs.rmSync(tempDir, { recursive: true, force: true });
1145
+ } catch (error) {
1146
+ console.error(chalk.red(`Failed to setup starter kit: ${error}`));
1147
+ // Clean up temp directory on error
1148
+ if (fs.existsSync(tempDir)) {
1149
+ fs.rmSync(tempDir, { recursive: true, force: true });
1150
+ }
1151
+ throw error;
1152
+ }
1153
+ }
1154
+ // Run custom setup if defined
1155
+ if (starterKit.customSetup) {
1156
+ await starterKit.customSetup(baseDir, answer);
1157
+ }
1158
+ console.log(chalk.green(`✓ ${starterKit.name} setup complete!`));
1159
+ }
1160
+ function showStarterKits() {
1161
+ console.log(chalk.blue("\n🚀 Available Starter Kits:\n"));
1162
+ Object.values(STARTER_KITS).forEach((kit) => {
1163
+ const isCustom = kit.source ? " (Custom)" : " (Built-in)";
1164
+ console.log(chalk.green(` ${kit.id}${chalk.gray(isCustom)}`));
1165
+ console.log(` ${kit.name}`);
1166
+ console.log(chalk.gray(` ${kit.description}`));
1167
+ if (kit.source) {
1168
+ console.log(chalk.cyan(` Source: ${kit.source.url}`));
1169
+ }
1170
+ const features = Object.entries(kit.features)
1171
+ .filter(([, value]) => value === true)
1172
+ .map(([key]) => key)
1173
+ .join(", ");
1174
+ if (features) {
1175
+ console.log(chalk.magenta(` Features: ${features}`));
1176
+ }
1177
+ console.log();
1178
+ });
1179
+ console.log(chalk.yellow("Usage:"));
1180
+ console.log(` npx create-prisma-php-app my-project --starter-kit=basic`);
1181
+ console.log(
1182
+ ` npx create-prisma-php-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo`
1183
+ );
1184
+ console.log();
1185
+ }
1186
+ // Starter kit specific copy function that respects exclusions
1187
+ function copyRecursiveSyncWithExclusions(
1188
+ src,
1189
+ dest,
1190
+ answer,
1191
+ respectExclusions = true
1192
+ ) {
1193
+ const exists = fs.existsSync(src);
1194
+ const stats = exists && fs.statSync(src);
1195
+ const isDirectory = exists && stats && stats.isDirectory();
1196
+ if (isDirectory) {
1197
+ const destLower = dest.toLowerCase();
1198
+ // Apply feature-based exclusions
1199
+ if (!answer.websocket && destLower.includes("websocket")) return;
1200
+ if (!answer.mcp && destLower.includes("mcp")) return;
1201
+ if (!answer.swaggerDocs && destLower.includes("swagger-docs")) return;
1202
+ if (
1203
+ answer.backendOnly &&
1204
+ (destLower.includes("js") ||
1205
+ destLower.includes("css") ||
1206
+ destLower.includes("assets"))
1207
+ )
1208
+ return;
1209
+ // Apply user-defined exclusions
1210
+ const destModified = dest.replace(/\\/g, "/");
1211
+ if (
1212
+ respectExclusions &&
1213
+ updateAnswer?.excludeFilePath?.includes(destModified)
1214
+ ) {
1215
+ console.log(chalk.yellow(`Skipping excluded directory: ${destModified}`));
1216
+ return;
1217
+ }
1218
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
1219
+ fs.readdirSync(src).forEach((childItemName) => {
1220
+ copyRecursiveSyncWithExclusions(
1221
+ path.join(src, childItemName),
1222
+ path.join(dest, childItemName),
1223
+ answer,
1224
+ respectExclusions
1225
+ );
1226
+ });
1227
+ } else {
1228
+ const fileName = path.basename(dest);
1229
+ // Always exclude critical config files from starter kit overwrites
1230
+ if (fileName === "prisma-php.json") {
1231
+ console.log(
1232
+ chalk.yellow(`Protecting config file: ${dest.replace(/\\/g, "/")}`)
1233
+ );
1234
+ return;
1235
+ }
1236
+ // Apply user-defined exclusions for files
1237
+ if (respectExclusions && checkExcludeFiles(dest)) {
1238
+ console.log(
1239
+ chalk.yellow(`Skipping excluded file: ${dest.replace(/\\/g, "/")}`)
1240
+ );
1241
+ return;
1242
+ }
1243
+ // Apply feature-based exclusions
1244
+ if (
1245
+ !answer.tailwindcss &&
1246
+ (dest.includes("tailwind.css") || dest.includes("styles.css"))
1247
+ )
1248
+ return;
1249
+ if (!answer.websocket && dest.includes("restart-websocket.ts")) return;
1250
+ if (!answer.mcp && dest.includes("restart-mcp.ts")) return;
1251
+ if (!answer.docker && dockerFiles.some((file) => dest.includes(file)))
1252
+ return;
1253
+ if (
1254
+ answer.backendOnly &&
1255
+ nonBackendFiles.some((file) => dest.includes(file))
1256
+ )
1257
+ return;
1258
+ if (!answer.backendOnly && dest.includes("route.php")) return;
1259
+ if (
1260
+ answer.backendOnly &&
1261
+ !answer.swaggerDocs &&
1262
+ dest.includes("layout.php")
1263
+ )
1264
+ return;
1265
+ if (!answer.swaggerDocs && dest.includes("swagger-config.ts")) return;
1266
+ if (answer.tailwindcss && dest.includes("index.css")) return;
1267
+ fs.copyFileSync(src, dest, 0);
1268
+ }
1269
+ }
1270
+ function mergeConfigurationFiles(
1271
+ projectPath,
1272
+ answer,
1273
+ latestVersion,
1274
+ existingConfig
1275
+ ) {
1276
+ const projectPathModified = projectPath.replace(/\\/g, "\\");
1277
+ const bsConfig = bsConfigUrls(projectPathModified);
1278
+ // If we have existing config, merge with it
1279
+ if (existingConfig) {
1280
+ console.log(chalk.blue("Merging with existing configuration..."));
1281
+ console.log(
1282
+ chalk.gray(
1283
+ `Preserving excludeFiles: ${JSON.stringify(
1284
+ existingConfig.excludeFiles || []
1285
+ )}`
1286
+ )
1287
+ );
1288
+ const mergedConfig = {
1289
+ ...existingConfig, // Start with existing config to preserve all existing fields
1290
+ // Only update specific fields that should be updated for starter kits
1291
+ projectName: answer.projectName,
1292
+ projectRootPath: projectPathModified,
1293
+ bsTarget: bsConfig.bsTarget,
1294
+ bsPathRewrite: bsConfig.bsPathRewrite,
1295
+ version: latestVersion,
1296
+ // Update feature flags based on starter kit
1297
+ backendOnly: answer.backendOnly,
1298
+ swaggerDocs: answer.swaggerDocs,
1299
+ tailwindcss: answer.tailwindcss,
1300
+ websocket: answer.websocket,
1301
+ mcp: answer.mcp,
1302
+ prisma: answer.prisma,
1303
+ docker: answer.docker,
1304
+ // CRITICAL: Always preserve existing excludeFiles
1305
+ excludeFiles: existingConfig.excludeFiles || [],
1306
+ };
1307
+ fs.writeFileSync(
1308
+ path.join(projectPath, "prisma-php.json"),
1309
+ JSON.stringify(mergedConfig, null, 2),
1310
+ { flag: "w" }
1311
+ );
1312
+ console.log(chalk.green("✓ Configuration merged successfully!"));
1313
+ console.log(
1314
+ chalk.green(
1315
+ `✓ Preserved ${
1316
+ (existingConfig.excludeFiles || []).length
1317
+ } excluded files`
1318
+ )
1319
+ );
1320
+ } else {
1321
+ console.log(chalk.blue("Creating new configuration..."));
1322
+ // New project - create fresh config
1323
+ const prismaPhpConfig = {
1324
+ projectName: answer.projectName,
1325
+ projectRootPath: projectPathModified,
1326
+ phpEnvironment: "XAMPP",
1327
+ phpRootPathExe: "C:\\xampp\\php\\php.exe",
1328
+ bsTarget: bsConfig.bsTarget,
1329
+ bsPathRewrite: bsConfig.bsPathRewrite,
1330
+ backendOnly: answer.backendOnly,
1331
+ swaggerDocs: answer.swaggerDocs,
1332
+ tailwindcss: answer.tailwindcss,
1333
+ websocket: answer.websocket,
1334
+ mcp: answer.mcp,
1335
+ prisma: answer.prisma,
1336
+ docker: answer.docker,
1337
+ version: latestVersion,
1338
+ excludeFiles: [],
1339
+ };
1340
+ fs.writeFileSync(
1341
+ path.join(projectPath, "prisma-php.json"),
1342
+ JSON.stringify(prismaPhpConfig, null, 2),
1343
+ { flag: "w" }
1344
+ );
1345
+ console.log(chalk.green("✓ New configuration created"));
1346
+ }
1347
+ }
98
1348
  async function main() {
99
1349
  try {
100
1350
  const args = process.argv.slice(2);
101
1351
  let projectName = args[0];
1352
+ // Parse starter kit arguments
1353
+ const starterKitArg = args.find((arg) => arg.startsWith("--starter-kit="));
1354
+ const starterKitFromArgs = starterKitArg?.split("=")[1];
1355
+ // Parse custom starter kit source
1356
+ const starterKitSourceArg = args.find((arg) =>
1357
+ arg.startsWith("--starter-kit-source=")
1358
+ );
1359
+ const starterKitSource = starterKitSourceArg?.split("=")[1];
1360
+ // Show help
1361
+ if (args.includes("--list-starter-kits")) {
1362
+ showStarterKits();
1363
+ return;
1364
+ }
102
1365
  let answer = null;
103
1366
  if (projectName) {
104
- let useBackendOnly = args.includes("--backend-only");
105
- let useSwaggerDocs = args.includes("--swagger-docs");
106
- let useTailwind = args.includes("--tailwindcss");
107
- let useWebsocket = args.includes("--websocket");
108
- let usePrisma = args.includes("--prisma");
109
- let useDocker = args.includes("--docker");
110
- const predefinedAnswers = {
111
- projectName,
112
- backendOnly: useBackendOnly,
113
- swaggerDocs: useSwaggerDocs,
114
- tailwindcss: useTailwind,
115
- websocket: useWebsocket,
116
- prisma: usePrisma,
117
- docker: useDocker,
118
- };
119
- answer = await getAnswer(predefinedAnswers);
120
- if (answer === null) {
121
- console.log(chalk.red("Installation cancelled."));
122
- return;
123
- }
124
1367
  const currentDir = process.cwd();
125
1368
  const configPath = path.join(currentDir, "prisma-php.json");
1369
+ const projectNamePath = path.join(currentDir, projectName);
1370
+ const projectNameConfigPath = path.join(
1371
+ projectNamePath,
1372
+ "prisma-php.json"
1373
+ );
1374
+ // Check if there's an existing config in current directory or project directory
1375
+ let existingConfigPath = null;
126
1376
  if (fs.existsSync(configPath)) {
127
- const localSettings = readJsonFile(configPath);
1377
+ existingConfigPath = configPath;
1378
+ } else if (fs.existsSync(projectNameConfigPath)) {
1379
+ existingConfigPath = projectNameConfigPath;
1380
+ }
1381
+ // If we found an existing config and we're using a starter kit, load exclusions
1382
+ if (existingConfigPath && (starterKitFromArgs || starterKitSource)) {
1383
+ const localSettings = readJsonFile(existingConfigPath);
128
1384
  let excludeFiles = [];
129
1385
  localSettings.excludeFiles?.map((file) => {
130
- const filePath = path.join(currentDir, file);
1386
+ const filePath = path.join(
1387
+ existingConfigPath === configPath ? currentDir : projectNamePath,
1388
+ file
1389
+ );
131
1390
  if (fs.existsSync(filePath))
132
1391
  excludeFiles.push(filePath.replace(/\\/g, "/"));
133
1392
  });
1393
+ // Set updateAnswer to respect exclusions during starter kit setup
134
1394
  updateAnswer = {
135
1395
  projectName,
136
- backendOnly: answer?.backendOnly ?? false,
137
- swaggerDocs: answer?.swaggerDocs ?? false,
138
- tailwindcss: answer?.tailwindcss ?? false,
139
- websocket: answer?.websocket ?? false,
140
- prisma: answer?.prisma ?? false,
141
- docker: answer?.docker ?? false,
142
- isUpdate: true,
1396
+ backendOnly: localSettings.backendOnly,
1397
+ swaggerDocs: localSettings.swaggerDocs,
1398
+ tailwindcss: localSettings.tailwindcss,
1399
+ websocket: localSettings.websocket,
1400
+ mcp: localSettings.mcp,
1401
+ prisma: localSettings.prisma,
1402
+ docker: localSettings.docker,
1403
+ isUpdate: false, // Not a true update, but we need exclusions to work
143
1404
  excludeFiles: localSettings.excludeFiles ?? [],
144
1405
  excludeFilePath: excludeFiles ?? [],
145
- filePath: currentDir,
1406
+ filePath:
1407
+ existingConfigPath === configPath ? currentDir : projectNamePath,
1408
+ };
1409
+ // For updates, use existing settings but allow CLI overrides
1410
+ const predefinedAnswers = {
1411
+ projectName,
1412
+ backendOnly:
1413
+ args.includes("--backend-only") || localSettings.backendOnly,
1414
+ swaggerDocs:
1415
+ args.includes("--swagger-docs") || localSettings.swaggerDocs,
1416
+ tailwindcss:
1417
+ args.includes("--tailwindcss") || localSettings.tailwindcss,
1418
+ websocket: args.includes("--websocket") || localSettings.websocket,
1419
+ prisma: args.includes("--prisma") || localSettings.prisma,
1420
+ docker: args.includes("--docker") || localSettings.docker,
1421
+ mcp: args.includes("--mcp") || localSettings.mcp,
1422
+ };
1423
+ answer = await getAnswer(predefinedAnswers);
1424
+ // IMPORTANT: Update updateAnswer with the NEW answer after getting user input
1425
+ if (answer !== null) {
1426
+ updateAnswer = {
1427
+ projectName,
1428
+ backendOnly: answer.backendOnly,
1429
+ swaggerDocs: answer.swaggerDocs,
1430
+ tailwindcss: answer.tailwindcss,
1431
+ websocket: answer.websocket,
1432
+ mcp: answer.mcp,
1433
+ prisma: answer.prisma,
1434
+ docker: answer.docker,
1435
+ isUpdate: true,
1436
+ excludeFiles: localSettings.excludeFiles ?? [],
1437
+ excludeFilePath: excludeFiles ?? [],
1438
+ filePath: currentDir,
1439
+ };
1440
+ }
1441
+ } else {
1442
+ // New project
1443
+ const predefinedAnswers = {
1444
+ projectName,
1445
+ starterKit: starterKitFromArgs,
1446
+ starterKitSource: starterKitSource,
1447
+ backendOnly: args.includes("--backend-only"),
1448
+ swaggerDocs: args.includes("--swagger-docs"),
1449
+ tailwindcss: args.includes("--tailwindcss"),
1450
+ websocket: args.includes("--websocket"),
1451
+ mcp: args.includes("--mcp"),
1452
+ prisma: args.includes("--prisma"),
1453
+ docker: args.includes("--docker"),
146
1454
  };
1455
+ answer = await getAnswer(predefinedAnswers);
1456
+ }
1457
+ if (answer === null) {
1458
+ console.log(chalk.red("Installation cancelled."));
1459
+ return;
147
1460
  }
148
1461
  } else {
1462
+ // Interactive mode
149
1463
  answer = await getAnswer();
150
1464
  }
151
1465
  if (answer === null) {
@@ -176,12 +1490,44 @@ async function main() {
176
1490
  execSync("npm install -g create-prisma-php-app", { stdio: "inherit" });
177
1491
  }
178
1492
  // Create the project directory
179
- if (!projectName) fs.mkdirSync(answer.projectName);
180
1493
  const currentDir = process.cwd();
181
- let projectPath = projectName
182
- ? currentDir
183
- : path.join(currentDir, answer.projectName);
184
- if (!projectName) process.chdir(answer.projectName);
1494
+ let projectPath;
1495
+ if (projectName) {
1496
+ // Check if we're in an existing project (has prisma-php.json) or creating a new one
1497
+ const configPath = path.join(currentDir, "prisma-php.json");
1498
+ const projectNamePath = path.join(currentDir, projectName);
1499
+ const projectNameConfigPath = path.join(
1500
+ projectNamePath,
1501
+ "prisma-php.json"
1502
+ );
1503
+ if (fs.existsSync(configPath)) {
1504
+ // We're updating an existing project in current directory
1505
+ projectPath = currentDir;
1506
+ } else if (
1507
+ fs.existsSync(projectNamePath) &&
1508
+ fs.existsSync(projectNameConfigPath)
1509
+ ) {
1510
+ // We're updating an existing project in the named directory
1511
+ projectPath = projectNamePath;
1512
+ process.chdir(projectNamePath);
1513
+ } else {
1514
+ // We're creating a new project with the given name
1515
+ if (!fs.existsSync(projectNamePath)) {
1516
+ fs.mkdirSync(projectNamePath, { recursive: true });
1517
+ }
1518
+ projectPath = projectNamePath;
1519
+ process.chdir(projectNamePath);
1520
+ }
1521
+ } else {
1522
+ // Interactive mode - create directory with answer.projectName
1523
+ fs.mkdirSync(answer.projectName, { recursive: true });
1524
+ projectPath = path.join(currentDir, answer.projectName);
1525
+ process.chdir(answer.projectName);
1526
+ }
1527
+ // Add starter kit setup before npm/composer installation
1528
+ if (answer.starterKit) {
1529
+ await setupStarterKit(projectPath, answer);
1530
+ }
185
1531
  let npmDependencies = [
186
1532
  npmPkg("typescript"),
187
1533
  npmPkg("@types/node"),
@@ -201,7 +1547,7 @@ async function main() {
201
1547
  composerPkg("ezyang/htmlpurifier"),
202
1548
  composerPkg("symfony/uid"),
203
1549
  composerPkg("brick/math"),
204
- composerPkg("tsnc/prisma-php"),
1550
+ // composerPkg("tsnc/prisma-php"),
205
1551
  ];
206
1552
  if (answer.swaggerDocs) {
207
1553
  npmDependencies.push(
@@ -222,9 +1568,11 @@ async function main() {
222
1568
  );
223
1569
  }
224
1570
  if (answer.websocket) {
225
- npmDependencies.push(npmPkg("chokidar-cli"));
226
1571
  composerDependencies.push("cboden/ratchet");
227
1572
  }
1573
+ if (answer.mcp) {
1574
+ composerDependencies.push("php-mcp/server");
1575
+ }
228
1576
  if (answer.prisma) {
229
1577
  execSync("npm install -g prisma-client-php", { stdio: "inherit" });
230
1578
  }
@@ -266,24 +1614,57 @@ async function main() {
266
1614
  if (updateAnswer?.isUpdate) {
267
1615
  const updateUninstallNpmDependencies = [];
268
1616
  const updateUninstallComposerDependencies = [];
1617
+ // Helper function to check if a composer package is installed
1618
+ const isComposerPackageInstalled = (packageName) => {
1619
+ try {
1620
+ const composerJsonPath = path.join(projectPath, "composer.json");
1621
+ if (fs.existsSync(composerJsonPath)) {
1622
+ const composerJson = JSON.parse(
1623
+ fs.readFileSync(composerJsonPath, "utf8")
1624
+ );
1625
+ return !!(
1626
+ composerJson.require && composerJson.require[packageName]
1627
+ );
1628
+ }
1629
+ return false;
1630
+ } catch {
1631
+ return false;
1632
+ }
1633
+ };
1634
+ // Helper function to check if an npm package is installed
1635
+ const isNpmPackageInstalled = (packageName) => {
1636
+ try {
1637
+ const packageJsonPath = path.join(projectPath, "package.json");
1638
+ if (fs.existsSync(packageJsonPath)) {
1639
+ const packageJson = JSON.parse(
1640
+ fs.readFileSync(packageJsonPath, "utf8")
1641
+ );
1642
+ return !!(
1643
+ (packageJson.dependencies &&
1644
+ packageJson.dependencies[packageName]) ||
1645
+ (packageJson.devDependencies &&
1646
+ packageJson.devDependencies[packageName])
1647
+ );
1648
+ }
1649
+ return false;
1650
+ } catch {
1651
+ return false;
1652
+ }
1653
+ };
269
1654
  if (updateAnswer.backendOnly) {
270
1655
  nonBackendFiles.forEach((file) => {
271
1656
  const filePath = path.join(projectPath, "src", "app", file);
272
1657
  if (fs.existsSync(filePath)) {
273
- fs.unlinkSync(filePath); // Delete each file if it exists
1658
+ fs.unlinkSync(filePath);
274
1659
  console.log(`${file} was deleted successfully.`);
275
- } else {
276
- console.log(`${file} does not exist.`);
277
1660
  }
278
1661
  });
279
1662
  const backendOnlyFolders = ["js", "css"];
280
1663
  backendOnlyFolders.forEach((folder) => {
281
1664
  const folderPath = path.join(projectPath, "src", "app", folder);
282
1665
  if (fs.existsSync(folderPath)) {
283
- fs.rmSync(folderPath, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
1666
+ fs.rmSync(folderPath, { recursive: true, force: true });
284
1667
  console.log(`${folder} was deleted successfully.`);
285
- } else {
286
- console.log(`${folder} does not exist.`);
287
1668
  }
288
1669
  });
289
1670
  }
@@ -295,81 +1676,108 @@ async function main() {
295
1676
  "swagger-docs"
296
1677
  );
297
1678
  if (fs.existsSync(swaggerDocsFolder)) {
298
- fs.rmSync(swaggerDocsFolder, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
1679
+ fs.rmSync(swaggerDocsFolder, { recursive: true, force: true });
299
1680
  console.log(`swagger-docs was deleted successfully.`);
300
1681
  }
301
1682
  const swaggerFiles = ["swagger-config.ts"];
302
1683
  swaggerFiles.forEach((file) => {
303
1684
  const filePath = path.join(projectPath, "settings", file);
304
1685
  if (fs.existsSync(filePath)) {
305
- fs.unlinkSync(filePath); // Delete each file if it exists
1686
+ fs.unlinkSync(filePath);
306
1687
  console.log(`${file} was deleted successfully.`);
307
- } else {
308
- console.log(`${file} does not exist.`);
309
1688
  }
310
1689
  });
311
- updateUninstallNpmDependencies.push(
312
- "swagger-jsdoc",
313
- "@types/swagger-jsdoc",
314
- "prompts",
315
- "@types/prompts"
316
- );
1690
+ // Only add to uninstall list if packages are actually installed
1691
+ if (isNpmPackageInstalled("swagger-jsdoc")) {
1692
+ updateUninstallNpmDependencies.push("swagger-jsdoc");
1693
+ }
1694
+ if (isNpmPackageInstalled("@types/swagger-jsdoc")) {
1695
+ updateUninstallNpmDependencies.push("@types/swagger-jsdoc");
1696
+ }
1697
+ if (isNpmPackageInstalled("prompts")) {
1698
+ updateUninstallNpmDependencies.push("prompts");
1699
+ }
1700
+ if (isNpmPackageInstalled("@types/prompts")) {
1701
+ updateUninstallNpmDependencies.push("@types/prompts");
1702
+ }
317
1703
  }
318
1704
  if (!updateAnswer.tailwindcss) {
319
1705
  const tailwindFiles = ["postcss.config.js"];
320
1706
  tailwindFiles.forEach((file) => {
321
1707
  const filePath = path.join(projectPath, file);
322
1708
  if (fs.existsSync(filePath)) {
323
- fs.unlinkSync(filePath); // Delete each file if it exists
1709
+ fs.unlinkSync(filePath);
324
1710
  console.log(`${file} was deleted successfully.`);
325
- } else {
326
- console.log(`${file} does not exist.`);
327
1711
  }
328
1712
  });
329
- updateUninstallNpmDependencies.push(
1713
+ // Only add to uninstall list if packages are actually installed
1714
+ const tailwindPackages = [
330
1715
  "tailwindcss",
331
1716
  "postcss",
332
1717
  "postcss-cli",
333
1718
  "@tailwindcss/postcss",
334
- "cssnano"
335
- );
1719
+ "cssnano",
1720
+ ];
1721
+ tailwindPackages.forEach((pkg) => {
1722
+ if (isNpmPackageInstalled(pkg)) {
1723
+ updateUninstallNpmDependencies.push(pkg);
1724
+ }
1725
+ });
336
1726
  }
337
1727
  if (!updateAnswer.websocket) {
338
- const websocketFiles = [
339
- "restart-websocket.ts",
340
- "restart-websocket.bat",
341
- ];
1728
+ const websocketFiles = ["restart-websocket.ts"];
342
1729
  websocketFiles.forEach((file) => {
343
1730
  const filePath = path.join(projectPath, "settings", file);
344
1731
  if (fs.existsSync(filePath)) {
345
- fs.unlinkSync(filePath); // Delete each file if it exists
1732
+ fs.unlinkSync(filePath);
346
1733
  console.log(`${file} was deleted successfully.`);
347
- } else {
348
- console.log(`${file} does not exist.`);
349
1734
  }
350
1735
  });
351
- const websocketFolder = path.join(projectPath, "src", "Websocket");
1736
+ const websocketFolder = path.join(
1737
+ projectPath,
1738
+ "src",
1739
+ "Lib",
1740
+ "Websocket"
1741
+ );
352
1742
  if (fs.existsSync(websocketFolder)) {
353
- fs.rmSync(websocketFolder, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
1743
+ fs.rmSync(websocketFolder, { recursive: true, force: true });
354
1744
  console.log(`Websocket folder was deleted successfully.`);
355
1745
  }
356
- const websocketServerFile = path.join(
357
- projectPath,
358
- "websocket-server.php"
359
- );
360
- if (fs.existsSync(websocketServerFile)) {
361
- fs.unlinkSync(websocketServerFile); // Delete the file if it exists
362
- console.log(`websocket-server.php was deleted successfully.`);
1746
+ // composer package for websocket only
1747
+ if (isComposerPackageInstalled("cboden/ratchet")) {
1748
+ updateUninstallComposerDependencies.push("cboden/ratchet");
1749
+ }
1750
+ }
1751
+ if (!updateAnswer.mcp) {
1752
+ const mcpFiles = ["restart-mcp.ts"];
1753
+ mcpFiles.forEach((file) => {
1754
+ const filePath = path.join(projectPath, "settings", file);
1755
+ if (fs.existsSync(filePath)) {
1756
+ fs.unlinkSync(filePath);
1757
+ console.log(`${file} was deleted successfully.`);
1758
+ }
1759
+ });
1760
+ const mcpFolder = path.join(projectPath, "src", "Lib", "MCP");
1761
+ if (fs.existsSync(mcpFolder)) {
1762
+ fs.rmSync(mcpFolder, { recursive: true, force: true });
1763
+ console.log(`MCP folder was deleted successfully.`);
1764
+ }
1765
+ // composer package for MCP only
1766
+ if (isComposerPackageInstalled("php-mcp/server")) {
1767
+ updateUninstallComposerDependencies.push("php-mcp/server");
363
1768
  }
364
- updateUninstallNpmDependencies.push("chokidar-cli");
365
- updateUninstallComposerDependencies.push("cboden/ratchet");
366
1769
  }
367
1770
  if (!updateAnswer.prisma) {
368
- updateUninstallNpmDependencies.push(
1771
+ const prismaPackages = [
369
1772
  "prisma",
370
1773
  "@prisma/client",
371
- "@prisma/internals"
372
- );
1774
+ "@prisma/internals",
1775
+ ];
1776
+ prismaPackages.forEach((pkg) => {
1777
+ if (isNpmPackageInstalled(pkg)) {
1778
+ updateUninstallNpmDependencies.push(pkg);
1779
+ }
1780
+ });
373
1781
  }
374
1782
  if (!updateAnswer.docker) {
375
1783
  const dockerFiles = [
@@ -381,49 +1789,127 @@ async function main() {
381
1789
  dockerFiles.forEach((file) => {
382
1790
  const filePath = path.join(projectPath, file);
383
1791
  if (fs.existsSync(filePath)) {
384
- fs.unlinkSync(filePath); // Delete each file if it exists
1792
+ fs.unlinkSync(filePath);
385
1793
  console.log(`${file} was deleted successfully.`);
386
- } else {
387
- console.log(`${file} does not exist.`);
388
1794
  }
389
1795
  });
390
1796
  }
391
- if (updateUninstallNpmDependencies.length > 0) {
392
- await uninstallNpmDependencies(
393
- projectPath,
394
- updateUninstallNpmDependencies,
395
- true
396
- );
1797
+ // Only uninstall if there are packages to uninstall
1798
+ const uniq = (arr) => Array.from(new Set(arr));
1799
+ const npmToUninstall = uniq(updateUninstallNpmDependencies);
1800
+ const composerToUninstall = uniq(updateUninstallComposerDependencies);
1801
+ if (npmToUninstall.length > 0) {
1802
+ console.log(`Uninstalling npm packages: ${npmToUninstall.join(", ")}`);
1803
+ await uninstallNpmDependencies(projectPath, npmToUninstall, true);
397
1804
  }
398
- if (updateUninstallComposerDependencies.length > 0) {
399
- await uninstallComposerDependencies(
400
- projectPath,
401
- updateUninstallComposerDependencies
1805
+ if (composerToUninstall.length > 0) {
1806
+ console.log(
1807
+ `Uninstalling composer packages: ${composerToUninstall.join(", ")}`
402
1808
  );
1809
+ await uninstallComposerDependencies(projectPath, composerToUninstall);
403
1810
  }
404
1811
  }
405
- const projectPathModified = projectPath.replace(/\\/g, "\\");
406
- const bsConfig = bsConfigUrls(projectPathModified);
407
- const prismaPhpConfig = {
408
- projectName: answer.projectName,
409
- projectRootPath: projectPathModified,
410
- phpEnvironment: "XAMPP",
411
- phpRootPathExe: "C:\\xampp\\php\\php.exe",
412
- bsTarget: bsConfig.bsTarget,
413
- bsPathRewrite: bsConfig.bsPathRewrite,
414
- backendOnly: answer.backendOnly,
415
- swaggerDocs: answer.swaggerDocs,
416
- tailwindcss: answer.tailwindcss,
417
- websocket: answer.websocket,
418
- prisma: answer.prisma,
419
- docker: answer.docker,
420
- version: latestVersionOfCreatePrismaPhpApp,
421
- excludeFiles: updateAnswer?.excludeFiles ?? [],
1812
+ // Check for existing config to merge with
1813
+ let existingConfig = null;
1814
+ const finalProjectConfigPath = path.join(projectPath, "prisma-php.json");
1815
+ // Function to find prisma-php.json files with excludeFiles
1816
+ const findConfigWithExclusions = (searchDir) => {
1817
+ const entries = fs.readdirSync(searchDir, { withFileTypes: true });
1818
+ for (const entry of entries) {
1819
+ if (entry.isDirectory()) {
1820
+ const subConfigPath = path.join(
1821
+ searchDir,
1822
+ entry.name,
1823
+ "prisma-php.json"
1824
+ );
1825
+ if (fs.existsSync(subConfigPath)) {
1826
+ try {
1827
+ const config = JSON.parse(fs.readFileSync(subConfigPath, "utf8"));
1828
+ if (config.excludeFiles && config.excludeFiles.length > 0) {
1829
+ console.log(
1830
+ chalk.blue(
1831
+ `Found configuration with exclusions in: ${entry.name}/`
1832
+ )
1833
+ );
1834
+ console.log(
1835
+ chalk.gray(
1836
+ `Found excludeFiles: ${JSON.stringify(config.excludeFiles)}`
1837
+ )
1838
+ );
1839
+ return config;
1840
+ }
1841
+ } catch (error) {
1842
+ console.warn(
1843
+ chalk.yellow(`Could not read config in ${entry.name}/`)
1844
+ );
1845
+ }
1846
+ }
1847
+ }
1848
+ }
1849
+ return null;
422
1850
  };
423
- fs.writeFileSync(
424
- path.join(projectPath, "prisma-php.json"),
425
- JSON.stringify(prismaPhpConfig, null, 2),
426
- { flag: "w" }
1851
+ // First, look in current directory for prisma-php.json
1852
+ const currentDirConfigPath = path.join(currentDir, "prisma-php.json");
1853
+ if (
1854
+ fs.existsSync(currentDirConfigPath) &&
1855
+ currentDirConfigPath !== finalProjectConfigPath
1856
+ ) {
1857
+ try {
1858
+ const rawConfig = fs.readFileSync(currentDirConfigPath, "utf8");
1859
+ existingConfig = JSON.parse(rawConfig);
1860
+ console.log(
1861
+ chalk.blue("Found existing configuration in current directory")
1862
+ );
1863
+ console.log(
1864
+ chalk.gray(
1865
+ `Current dir excludeFiles: ${JSON.stringify(
1866
+ existingConfig.excludeFiles || []
1867
+ )}`
1868
+ )
1869
+ );
1870
+ } catch (error) {
1871
+ console.warn(chalk.yellow("Could not read current directory config"));
1872
+ }
1873
+ }
1874
+ // If no config in current directory, search subdirectories for configs with excludeFiles
1875
+ if (!existingConfig) {
1876
+ console.log(
1877
+ chalk.blue("Searching for existing configurations with exclusions...")
1878
+ );
1879
+ existingConfig = findConfigWithExclusions(currentDir);
1880
+ }
1881
+ // If still no config, check the target project directory
1882
+ if (!existingConfig && fs.existsSync(finalProjectConfigPath)) {
1883
+ try {
1884
+ const rawConfig = fs.readFileSync(finalProjectConfigPath, "utf8");
1885
+ existingConfig = JSON.parse(rawConfig);
1886
+ console.log(
1887
+ chalk.blue("Found existing configuration in project directory")
1888
+ );
1889
+ console.log(
1890
+ chalk.gray(
1891
+ `Project excludeFiles: ${JSON.stringify(
1892
+ existingConfig.excludeFiles || []
1893
+ )}`
1894
+ )
1895
+ );
1896
+ } catch (error) {
1897
+ console.warn(chalk.yellow("Could not read project config"));
1898
+ existingConfig = null;
1899
+ }
1900
+ }
1901
+ // If still no existing config found
1902
+ if (!existingConfig) {
1903
+ console.log(
1904
+ chalk.blue("No existing configuration found, creating new one...")
1905
+ );
1906
+ }
1907
+ // Merge or create configuration
1908
+ mergeConfigurationFiles(
1909
+ projectPath,
1910
+ answer,
1911
+ latestVersionOfCreatePrismaPhpApp,
1912
+ existingConfig
427
1913
  );
428
1914
  if (updateAnswer?.isUpdate) {
429
1915
  execSync(
@@ -445,7 +1931,7 @@ async function main() {
445
1931
  `${chalk.green(
446
1932
  "Success!"
447
1933
  )} Prisma PHP project successfully created in ${chalk.green(
448
- `${currentDir.replace(/\\/g, "/")}/${answer.projectName}`
1934
+ projectPath.replace(/\\/g, "/")
449
1935
  )}!`
450
1936
  );
451
1937
  console.log("\n=========================");