create-prisma-php-app 4.0.0-alpha.3 → 4.0.0-alpha.31

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 +461 -111
  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,5 @@
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";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?.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"});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)return{projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1}}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}}
3
3
  /**
4
4
  * Install dependencies in the specified directory.
5
5
  * @param {string} baseDir - The base directory where to install the dependencies.
@@ -7,7 +7,11 @@ import{execSync}from"child_process";import fs from"fs";import{fileURLToPath}from
7
7
  * @param {boolean} [isDev=false] - Whether to install the dependencies as devDependencies.
8
8
  */
9
9
  async function installNpmDependencies(baseDir, dependencies, isDev = false) {
10
- console.log("Initializing new Node.js project...");
10
+ if (!fs.existsSync(path.join(baseDir, "package.json"))) {
11
+ console.log("Initializing new Node.js project...");
12
+ } else {
13
+ console.log("Updating existing Node.js project...");
14
+ }
11
15
  // Initialize a package.json if it doesn't exist
12
16
  if (!fs.existsSync(path.join(baseDir, "package.json"))) {
13
17
  execSync("npm init -y", {
@@ -32,27 +36,99 @@ async function installNpmDependencies(baseDir, dependencies, isDev = false) {
32
36
  cwd: baseDir,
33
37
  });
34
38
  }
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"))) {
39
+ function getComposerCmd() {
40
+ try {
41
+ execSync("composer --version", { stdio: "ignore" });
42
+ return { cmd: "composer", baseArgs: [] };
43
+ } catch {
44
+ return {
45
+ cmd: "C:\\xampp\\php\\php.exe",
46
+ baseArgs: ["C:\\ProgramData\\ComposerSetup\\bin\\composer.phar"],
47
+ };
48
+ }
49
+ }
50
+ export async function installComposerDependencies(baseDir, dependencies) {
51
+ const { cmd, baseArgs } = getComposerCmd();
52
+ const composerJsonPath = path.join(baseDir, "composer.json");
53
+ const existsAlready = fs.existsSync(composerJsonPath);
54
+ console.log(
55
+ chalk.green(
56
+ `Composer project initialization: ${
57
+ existsAlready ? "Updating existing project…" : "Setting up new project…"
58
+ }`
59
+ )
60
+ );
61
+ /* ------------------------------------------------------------------ */
62
+ /* 1. Try composer init (quietly fall back if it fails) */
63
+ /* ------------------------------------------------------------------ */
64
+ if (!existsAlready) {
65
+ const initArgs = [
66
+ ...baseArgs,
67
+ "init",
68
+ "--no-interaction",
69
+ "--name",
70
+ "tsnc/prisma-php-app",
71
+ "--require",
72
+ "php:^8.2",
73
+ "--type",
74
+ "project",
75
+ "--version",
76
+ "1.0.0",
77
+ ];
78
+ const res = spawnSync(cmd, initArgs, { cwd: baseDir });
79
+ if (res.status !== 0) {
80
+ // Silent fallback: no logs, just write a minimal composer.json
81
+ fs.writeFileSync(
82
+ composerJsonPath,
83
+ JSON.stringify(
84
+ {
85
+ name: "tsnc/prisma-php-app",
86
+ type: "project",
87
+ version: "1.0.0",
88
+ require: { php: "^8.2" },
89
+ autoload: { "psr-4": { "": "src/" } },
90
+ },
91
+ null,
92
+ 2
93
+ )
94
+ );
95
+ }
96
+ }
97
+ /* 2. Ensure PSR-4 autoload entry ---------------------------------- */
98
+ const json = JSON.parse(fs.readFileSync(composerJsonPath, "utf8"));
99
+ json.autoload ??= {};
100
+ json.autoload["psr-4"] ??= {};
101
+ json.autoload["psr-4"][""] ??= "src/";
102
+ fs.writeFileSync(composerJsonPath, JSON.stringify(json, null, 2));
103
+ /* 3. Install dependencies ----------------------------------------- */
104
+ if (dependencies.length) {
105
+ console.log("Installing Composer dependencies:");
106
+ dependencies.forEach((d) => console.log(`- ${chalk.blue(d)}`));
39
107
  execSync(
40
- `composer init -n --name="tsnc/prisma-php-app" --require="php:^8.2"`,
41
- {
42
- stdio: "inherit",
43
- cwd: baseDir,
44
- }
108
+ `${cmd} ${[
109
+ ...baseArgs,
110
+ "require",
111
+ "--no-interaction",
112
+ ...dependencies,
113
+ ].join(" ")}`,
114
+ { stdio: "inherit", cwd: baseDir }
45
115
  );
46
116
  }
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, {
117
+ /* 4. Refresh lock when updating ----------------------------------- */
118
+ if (existsAlready) {
119
+ execSync(
120
+ `${cmd} ${[
121
+ ...baseArgs,
122
+ "update",
123
+ "--lock",
124
+ "--no-install",
125
+ "--no-interaction",
126
+ ].join(" ")}`,
127
+ { stdio: "inherit", cwd: baseDir }
128
+ );
129
+ }
130
+ /* 5. Regenerate autoloader ---------------------------------------- */
131
+ execSync(`${cmd} ${[...baseArgs, "dump-autoload", "--quiet"].join(" ")}`, {
56
132
  stdio: "inherit",
57
133
  cwd: baseDir,
58
134
  });
@@ -60,21 +136,20 @@ async function installComposerDependencies(baseDir, dependencies) {
60
136
  const npmPinnedVersions = {
61
137
  "@tailwindcss/postcss": "^4.1.11",
62
138
  "@types/browser-sync": "^2.29.0",
63
- "@types/node": "^24.0.7",
139
+ "@types/node": "^24.2.1",
64
140
  "@types/prompts": "^2.4.9",
65
141
  "browser-sync": "^3.0.4",
66
- chalk: "^5.4.1",
67
- "chokidar-cli": "^3.0.0",
68
- cssnano: "^7.0.7",
142
+ chalk: "^5.5.0",
143
+ cssnano: "^7.1.0",
69
144
  "http-proxy-middleware": "^3.0.5",
70
145
  "npm-run-all": "^4.1.5",
71
- "php-parser": "^3.2.4",
146
+ "php-parser": "^3.2.5",
72
147
  postcss: "^8.5.6",
73
148
  "postcss-cli": "^11.0.1",
74
149
  prompts: "^2.4.2",
75
150
  tailwindcss: "^4.1.11",
76
151
  tsx: "^4.20.3",
77
- typescript: "^5.8.3",
152
+ typescript: "^5.9.2",
78
153
  };
79
154
  function npmPkg(name) {
80
155
  return npmPinnedVersions[name] ? `${name}@${npmPinnedVersions[name]}` : name;
@@ -88,42 +163,197 @@ const composerPinnedVersions = {
88
163
  "symfony/uid": "^7.2.0",
89
164
  "brick/math": "^0.13.1",
90
165
  "cboden/ratchet": "^0.4.4",
91
- "tsnc/prisma-php": "^1.0.2",
166
+ "tsnc/prisma-php": "^1.0.0",
167
+ "php-mcp/server": "3.3.0",
92
168
  };
93
169
  function composerPkg(name) {
94
170
  return composerPinnedVersions[name]
95
171
  ? `${name}:${composerPinnedVersions[name]}`
96
172
  : name;
97
173
  }
174
+ async function downloadStarterKit(starterKit, tempDir) {
175
+ if (!starterKit.source) {
176
+ throw new Error("No source defined for starter kit");
177
+ }
178
+ const { type, url, branch = "main", subfolder } = starterKit.source;
179
+ switch (type) {
180
+ case "git":
181
+ console.log(chalk.blue(`Cloning ${starterKit.name} from ${url}...`));
182
+ const cloneCommand = branch
183
+ ? `git clone -b ${branch} --depth 1 ${url} ${tempDir}`
184
+ : `git clone --depth 1 ${url} ${tempDir}`;
185
+ execSync(cloneCommand, { stdio: "inherit" });
186
+ // Remove .git directory
187
+ const gitDir = path.join(tempDir, ".git");
188
+ if (fs.existsSync(gitDir)) {
189
+ fs.rmSync(gitDir, { recursive: true, force: true });
190
+ }
191
+ // Return the subfolder if specified
192
+ return subfolder ? path.join(tempDir, subfolder) : tempDir;
193
+ case "npm":
194
+ console.log(chalk.blue(`Downloading ${starterKit.name} from npm...`));
195
+ execSync(`npm pack ${url}`, { cwd: tempDir, stdio: "inherit" });
196
+ // Extract the tarball
197
+ const tarball = fs.readdirSync(tempDir).find((f) => f.endsWith(".tgz"));
198
+ if (tarball) {
199
+ execSync(`tar -xzf ${tarball}`, { cwd: tempDir });
200
+ fs.unlinkSync(path.join(tempDir, tarball));
201
+ return path.join(tempDir, "package");
202
+ }
203
+ throw new Error("Failed to extract npm package");
204
+ case "url":
205
+ throw new Error("URL download not implemented yet");
206
+ default:
207
+ throw new Error(`Unsupported source type: ${type}`);
208
+ }
209
+ }
210
+ async function mergeStarterKitFiles(starterKitPath, projectPath, answer) {
211
+ console.log(chalk.blue("Merging starter kit files..."));
212
+ // Copy all files from starter kit, but don't overwrite base files
213
+ copyRecursiveSync(starterKitPath, projectPath, answer);
214
+ // Look for starter kit specific configuration
215
+ const starterKitConfig = path.join(starterKitPath, "starter-kit.json");
216
+ if (fs.existsSync(starterKitConfig)) {
217
+ const config = JSON.parse(fs.readFileSync(starterKitConfig, "utf8"));
218
+ // Handle post-install scripts
219
+ if (config.postInstall) {
220
+ console.log(chalk.blue("Running post-install scripts..."));
221
+ for (const script of config.postInstall) {
222
+ console.log(chalk.gray(`Running: ${script}`));
223
+ execSync(script, { cwd: projectPath, stdio: "inherit" });
224
+ }
225
+ }
226
+ // Handle additional dependencies
227
+ if (config.additionalNpmDependencies) {
228
+ await installNpmDependencies(
229
+ projectPath,
230
+ config.additionalNpmDependencies.map(npmPkg),
231
+ true
232
+ );
233
+ }
234
+ if (config.additionalComposerDependencies) {
235
+ await installComposerDependencies(
236
+ projectPath,
237
+ config.additionalComposerDependencies.map(composerPkg)
238
+ );
239
+ }
240
+ }
241
+ }
242
+ async function setupStarterKit(baseDir, answer) {
243
+ if (!answer.starterKit) return;
244
+ let starterKit = null;
245
+ // Check if it's a built-in starter kit
246
+ if (STARTER_KITS[answer.starterKit]) {
247
+ starterKit = STARTER_KITS[answer.starterKit];
248
+ }
249
+ // Handle custom starter kit URL
250
+ else if (answer.starterKitSource) {
251
+ starterKit = {
252
+ id: answer.starterKit,
253
+ name: `Custom Starter Kit (${answer.starterKit})`,
254
+ description: "Custom starter kit from external source",
255
+ features: {}, // Will be determined from the downloaded kit
256
+ requiredFiles: [],
257
+ source: {
258
+ type: "git", // Assume git for now, could be enhanced
259
+ url: answer.starterKitSource,
260
+ },
261
+ };
262
+ }
263
+ if (!starterKit) {
264
+ console.warn(
265
+ chalk.yellow(`Starter kit '${answer.starterKit}' not found. Skipping...`)
266
+ );
267
+ return;
268
+ }
269
+ console.log(chalk.green(`Setting up ${starterKit.name}...`));
270
+ // If it's a custom starter kit with source, download it
271
+ if (starterKit.source) {
272
+ const tempDir = path.join(baseDir, ".temp-starter-kit");
273
+ try {
274
+ // Create temp directory
275
+ if (fs.existsSync(tempDir)) {
276
+ fs.rmSync(tempDir, { recursive: true, force: true });
277
+ }
278
+ fs.mkdirSync(tempDir, { recursive: true });
279
+ // Download the starter kit
280
+ const kitPath = await downloadStarterKit(starterKit, tempDir);
281
+ // Merge files from starter kit
282
+ await mergeStarterKitFiles(kitPath, baseDir, answer);
283
+ // Check if starter kit has its own configuration
284
+ const kitConfigPath = path.join(kitPath, "prisma-php-starter.json");
285
+ if (fs.existsSync(kitConfigPath)) {
286
+ const kitConfig = JSON.parse(fs.readFileSync(kitConfigPath, "utf8"));
287
+ // Override features with starter kit configuration
288
+ Object.assign(answer, kitConfig.features || {});
289
+ console.log(chalk.green(`Applied starter kit configuration`));
290
+ }
291
+ // Clean up temp directory
292
+ fs.rmSync(tempDir, { recursive: true, force: true });
293
+ } catch (error) {
294
+ console.error(chalk.red(`Failed to setup starter kit: ${error}`));
295
+ // Clean up temp directory on error
296
+ if (fs.existsSync(tempDir)) {
297
+ fs.rmSync(tempDir, { recursive: true, force: true });
298
+ }
299
+ throw error;
300
+ }
301
+ }
302
+ // Run custom setup if defined
303
+ if (starterKit.customSetup) {
304
+ await starterKit.customSetup(baseDir, answer);
305
+ }
306
+ console.log(chalk.green(`✓ ${starterKit.name} setup complete!`));
307
+ }
308
+ function showStarterKits() {
309
+ console.log(chalk.blue("\n🚀 Available Starter Kits:\n"));
310
+ Object.values(STARTER_KITS).forEach((kit) => {
311
+ const isCustom = kit.source ? " (Custom)" : " (Built-in)";
312
+ console.log(chalk.green(` ${kit.id}${chalk.gray(isCustom)}`));
313
+ console.log(` ${kit.name}`);
314
+ console.log(chalk.gray(` ${kit.description}`));
315
+ if (kit.source) {
316
+ console.log(chalk.cyan(` Source: ${kit.source.url}`));
317
+ }
318
+ const features = Object.entries(kit.features)
319
+ .filter(([, value]) => value === true)
320
+ .map(([key]) => key)
321
+ .join(", ");
322
+ if (features) {
323
+ console.log(chalk.magenta(` Features: ${features}`));
324
+ }
325
+ console.log();
326
+ });
327
+ console.log(chalk.yellow("Usage:"));
328
+ console.log(` npx create-prisma-php-app my-project --starter-kit=basic`);
329
+ console.log(
330
+ ` npx create-prisma-php-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo`
331
+ );
332
+ console.log();
333
+ }
98
334
  async function main() {
99
335
  try {
100
336
  const args = process.argv.slice(2);
101
337
  let projectName = args[0];
338
+ // Parse starter kit arguments
339
+ const starterKitArg = args.find((arg) => arg.startsWith("--starter-kit="));
340
+ const starterKitFromArgs = starterKitArg?.split("=")[1];
341
+ // Parse custom starter kit source
342
+ const starterKitSourceArg = args.find((arg) =>
343
+ arg.startsWith("--starter-kit-source=")
344
+ );
345
+ const starterKitSource = starterKitSourceArg?.split("=")[1];
346
+ // Show help
347
+ if (args.includes("--list-starter-kits")) {
348
+ showStarterKits();
349
+ return;
350
+ }
102
351
  let answer = null;
103
352
  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
353
  const currentDir = process.cwd();
125
354
  const configPath = path.join(currentDir, "prisma-php.json");
126
355
  if (fs.existsSync(configPath)) {
356
+ // It's an update - read existing settings
127
357
  const localSettings = readJsonFile(configPath);
128
358
  let excludeFiles = [];
129
359
  localSettings.excludeFiles?.map((file) => {
@@ -131,21 +361,75 @@ async function main() {
131
361
  if (fs.existsSync(filePath))
132
362
  excludeFiles.push(filePath.replace(/\\/g, "/"));
133
363
  });
364
+ // Set updateAnswer with OLD settings initially (for checkExcludeFiles function)
134
365
  updateAnswer = {
135
366
  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,
367
+ backendOnly: localSettings.backendOnly,
368
+ swaggerDocs: localSettings.swaggerDocs,
369
+ tailwindcss: localSettings.tailwindcss,
370
+ websocket: localSettings.websocket,
371
+ mcp: localSettings.mcp,
372
+ prisma: localSettings.prisma,
373
+ docker: localSettings.docker,
142
374
  isUpdate: true,
143
375
  excludeFiles: localSettings.excludeFiles ?? [],
144
376
  excludeFilePath: excludeFiles ?? [],
145
377
  filePath: currentDir,
146
378
  };
379
+ // For updates, use existing settings but allow CLI overrides
380
+ const predefinedAnswers = {
381
+ projectName,
382
+ backendOnly:
383
+ args.includes("--backend-only") || localSettings.backendOnly,
384
+ swaggerDocs:
385
+ args.includes("--swagger-docs") || localSettings.swaggerDocs,
386
+ tailwindcss:
387
+ args.includes("--tailwindcss") || localSettings.tailwindcss,
388
+ websocket: args.includes("--websocket") || localSettings.websocket,
389
+ prisma: args.includes("--prisma") || localSettings.prisma,
390
+ docker: args.includes("--docker") || localSettings.docker,
391
+ mcp: args.includes("--mcp") || localSettings.mcp,
392
+ };
393
+ answer = await getAnswer(predefinedAnswers);
394
+ // IMPORTANT: Update updateAnswer with the NEW answer after getting user input
395
+ if (answer !== null) {
396
+ updateAnswer = {
397
+ projectName,
398
+ backendOnly: answer.backendOnly,
399
+ swaggerDocs: answer.swaggerDocs,
400
+ tailwindcss: answer.tailwindcss,
401
+ websocket: answer.websocket,
402
+ mcp: answer.mcp,
403
+ prisma: answer.prisma,
404
+ docker: answer.docker,
405
+ isUpdate: true,
406
+ excludeFiles: localSettings.excludeFiles ?? [],
407
+ excludeFilePath: excludeFiles ?? [],
408
+ filePath: currentDir,
409
+ };
410
+ }
411
+ } else {
412
+ // New project
413
+ const predefinedAnswers = {
414
+ projectName,
415
+ starterKit: starterKitFromArgs,
416
+ starterKitSource: starterKitSource,
417
+ backendOnly: args.includes("--backend-only"),
418
+ swaggerDocs: args.includes("--swagger-docs"),
419
+ tailwindcss: args.includes("--tailwindcss"),
420
+ websocket: args.includes("--websocket"),
421
+ mcp: args.includes("--mcp"),
422
+ prisma: args.includes("--prisma"),
423
+ docker: args.includes("--docker"),
424
+ };
425
+ answer = await getAnswer(predefinedAnswers);
426
+ }
427
+ if (answer === null) {
428
+ console.log(chalk.red("Installation cancelled."));
429
+ return;
147
430
  }
148
431
  } else {
432
+ // Interactive mode
149
433
  answer = await getAnswer();
150
434
  }
151
435
  if (answer === null) {
@@ -201,7 +485,7 @@ async function main() {
201
485
  composerPkg("ezyang/htmlpurifier"),
202
486
  composerPkg("symfony/uid"),
203
487
  composerPkg("brick/math"),
204
- composerPkg("tsnc/prisma-php"),
488
+ // composerPkg("tsnc/prisma-php"),
205
489
  ];
206
490
  if (answer.swaggerDocs) {
207
491
  npmDependencies.push(
@@ -222,12 +506,18 @@ async function main() {
222
506
  );
223
507
  }
224
508
  if (answer.websocket) {
225
- npmDependencies.push(npmPkg("chokidar-cli"));
226
509
  composerDependencies.push("cboden/ratchet");
227
510
  }
511
+ if (answer.mcp) {
512
+ composerDependencies.push("php-mcp/server");
513
+ }
228
514
  if (answer.prisma) {
229
515
  execSync("npm install -g prisma-client-php", { stdio: "inherit" });
230
516
  }
517
+ // Add starter kit setup before npm/composer installation
518
+ if (answer.starterKit) {
519
+ await setupStarterKit(projectPath, answer);
520
+ }
231
521
  await installNpmDependencies(projectPath, npmDependencies, true);
232
522
  await installComposerDependencies(projectPath, composerDependencies);
233
523
  if (!projectName) {
@@ -266,24 +556,57 @@ async function main() {
266
556
  if (updateAnswer?.isUpdate) {
267
557
  const updateUninstallNpmDependencies = [];
268
558
  const updateUninstallComposerDependencies = [];
559
+ // Helper function to check if a composer package is installed
560
+ const isComposerPackageInstalled = (packageName) => {
561
+ try {
562
+ const composerJsonPath = path.join(projectPath, "composer.json");
563
+ if (fs.existsSync(composerJsonPath)) {
564
+ const composerJson = JSON.parse(
565
+ fs.readFileSync(composerJsonPath, "utf8")
566
+ );
567
+ return !!(
568
+ composerJson.require && composerJson.require[packageName]
569
+ );
570
+ }
571
+ return false;
572
+ } catch {
573
+ return false;
574
+ }
575
+ };
576
+ // Helper function to check if an npm package is installed
577
+ const isNpmPackageInstalled = (packageName) => {
578
+ try {
579
+ const packageJsonPath = path.join(projectPath, "package.json");
580
+ if (fs.existsSync(packageJsonPath)) {
581
+ const packageJson = JSON.parse(
582
+ fs.readFileSync(packageJsonPath, "utf8")
583
+ );
584
+ return !!(
585
+ (packageJson.dependencies &&
586
+ packageJson.dependencies[packageName]) ||
587
+ (packageJson.devDependencies &&
588
+ packageJson.devDependencies[packageName])
589
+ );
590
+ }
591
+ return false;
592
+ } catch {
593
+ return false;
594
+ }
595
+ };
269
596
  if (updateAnswer.backendOnly) {
270
597
  nonBackendFiles.forEach((file) => {
271
598
  const filePath = path.join(projectPath, "src", "app", file);
272
599
  if (fs.existsSync(filePath)) {
273
- fs.unlinkSync(filePath); // Delete each file if it exists
600
+ fs.unlinkSync(filePath);
274
601
  console.log(`${file} was deleted successfully.`);
275
- } else {
276
- console.log(`${file} does not exist.`);
277
602
  }
278
603
  });
279
604
  const backendOnlyFolders = ["js", "css"];
280
605
  backendOnlyFolders.forEach((folder) => {
281
606
  const folderPath = path.join(projectPath, "src", "app", folder);
282
607
  if (fs.existsSync(folderPath)) {
283
- fs.rmSync(folderPath, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
608
+ fs.rmSync(folderPath, { recursive: true, force: true });
284
609
  console.log(`${folder} was deleted successfully.`);
285
- } else {
286
- console.log(`${folder} does not exist.`);
287
610
  }
288
611
  });
289
612
  }
@@ -295,81 +618,108 @@ async function main() {
295
618
  "swagger-docs"
296
619
  );
297
620
  if (fs.existsSync(swaggerDocsFolder)) {
298
- fs.rmSync(swaggerDocsFolder, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
621
+ fs.rmSync(swaggerDocsFolder, { recursive: true, force: true });
299
622
  console.log(`swagger-docs was deleted successfully.`);
300
623
  }
301
624
  const swaggerFiles = ["swagger-config.ts"];
302
625
  swaggerFiles.forEach((file) => {
303
626
  const filePath = path.join(projectPath, "settings", file);
304
627
  if (fs.existsSync(filePath)) {
305
- fs.unlinkSync(filePath); // Delete each file if it exists
628
+ fs.unlinkSync(filePath);
306
629
  console.log(`${file} was deleted successfully.`);
307
- } else {
308
- console.log(`${file} does not exist.`);
309
630
  }
310
631
  });
311
- updateUninstallNpmDependencies.push(
312
- "swagger-jsdoc",
313
- "@types/swagger-jsdoc",
314
- "prompts",
315
- "@types/prompts"
316
- );
632
+ // Only add to uninstall list if packages are actually installed
633
+ if (isNpmPackageInstalled("swagger-jsdoc")) {
634
+ updateUninstallNpmDependencies.push("swagger-jsdoc");
635
+ }
636
+ if (isNpmPackageInstalled("@types/swagger-jsdoc")) {
637
+ updateUninstallNpmDependencies.push("@types/swagger-jsdoc");
638
+ }
639
+ if (isNpmPackageInstalled("prompts")) {
640
+ updateUninstallNpmDependencies.push("prompts");
641
+ }
642
+ if (isNpmPackageInstalled("@types/prompts")) {
643
+ updateUninstallNpmDependencies.push("@types/prompts");
644
+ }
317
645
  }
318
646
  if (!updateAnswer.tailwindcss) {
319
647
  const tailwindFiles = ["postcss.config.js"];
320
648
  tailwindFiles.forEach((file) => {
321
649
  const filePath = path.join(projectPath, file);
322
650
  if (fs.existsSync(filePath)) {
323
- fs.unlinkSync(filePath); // Delete each file if it exists
651
+ fs.unlinkSync(filePath);
324
652
  console.log(`${file} was deleted successfully.`);
325
- } else {
326
- console.log(`${file} does not exist.`);
327
653
  }
328
654
  });
329
- updateUninstallNpmDependencies.push(
655
+ // Only add to uninstall list if packages are actually installed
656
+ const tailwindPackages = [
330
657
  "tailwindcss",
331
658
  "postcss",
332
659
  "postcss-cli",
333
660
  "@tailwindcss/postcss",
334
- "cssnano"
335
- );
661
+ "cssnano",
662
+ ];
663
+ tailwindPackages.forEach((pkg) => {
664
+ if (isNpmPackageInstalled(pkg)) {
665
+ updateUninstallNpmDependencies.push(pkg);
666
+ }
667
+ });
336
668
  }
337
669
  if (!updateAnswer.websocket) {
338
- const websocketFiles = [
339
- "restart-websocket.ts",
340
- "restart-websocket.bat",
341
- ];
670
+ const websocketFiles = ["restart-websocket.ts"];
342
671
  websocketFiles.forEach((file) => {
343
672
  const filePath = path.join(projectPath, "settings", file);
344
673
  if (fs.existsSync(filePath)) {
345
- fs.unlinkSync(filePath); // Delete each file if it exists
674
+ fs.unlinkSync(filePath);
346
675
  console.log(`${file} was deleted successfully.`);
347
- } else {
348
- console.log(`${file} does not exist.`);
349
676
  }
350
677
  });
351
- const websocketFolder = path.join(projectPath, "src", "Websocket");
678
+ const websocketFolder = path.join(
679
+ projectPath,
680
+ "src",
681
+ "Lib",
682
+ "Websocket"
683
+ );
352
684
  if (fs.existsSync(websocketFolder)) {
353
- fs.rmSync(websocketFolder, { recursive: true, force: true }); // Use fs.rmSync instead of fs.rmdirSync
685
+ fs.rmSync(websocketFolder, { recursive: true, force: true });
354
686
  console.log(`Websocket folder was deleted successfully.`);
355
687
  }
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.`);
688
+ // composer package for websocket only
689
+ if (isComposerPackageInstalled("cboden/ratchet")) {
690
+ updateUninstallComposerDependencies.push("cboden/ratchet");
691
+ }
692
+ }
693
+ if (!updateAnswer.mcp) {
694
+ const mcpFiles = ["restart-mcp.ts"];
695
+ mcpFiles.forEach((file) => {
696
+ const filePath = path.join(projectPath, "settings", file);
697
+ if (fs.existsSync(filePath)) {
698
+ fs.unlinkSync(filePath);
699
+ console.log(`${file} was deleted successfully.`);
700
+ }
701
+ });
702
+ const mcpFolder = path.join(projectPath, "src", "Lib", "MCP");
703
+ if (fs.existsSync(mcpFolder)) {
704
+ fs.rmSync(mcpFolder, { recursive: true, force: true });
705
+ console.log(`MCP folder was deleted successfully.`);
706
+ }
707
+ // composer package for MCP only
708
+ if (isComposerPackageInstalled("php-mcp/server")) {
709
+ updateUninstallComposerDependencies.push("php-mcp/server");
363
710
  }
364
- updateUninstallNpmDependencies.push("chokidar-cli");
365
- updateUninstallComposerDependencies.push("cboden/ratchet");
366
711
  }
367
712
  if (!updateAnswer.prisma) {
368
- updateUninstallNpmDependencies.push(
713
+ const prismaPackages = [
369
714
  "prisma",
370
715
  "@prisma/client",
371
- "@prisma/internals"
372
- );
716
+ "@prisma/internals",
717
+ ];
718
+ prismaPackages.forEach((pkg) => {
719
+ if (isNpmPackageInstalled(pkg)) {
720
+ updateUninstallNpmDependencies.push(pkg);
721
+ }
722
+ });
373
723
  }
374
724
  if (!updateAnswer.docker) {
375
725
  const dockerFiles = [
@@ -381,25 +731,24 @@ async function main() {
381
731
  dockerFiles.forEach((file) => {
382
732
  const filePath = path.join(projectPath, file);
383
733
  if (fs.existsSync(filePath)) {
384
- fs.unlinkSync(filePath); // Delete each file if it exists
734
+ fs.unlinkSync(filePath);
385
735
  console.log(`${file} was deleted successfully.`);
386
- } else {
387
- console.log(`${file} does not exist.`);
388
736
  }
389
737
  });
390
738
  }
391
- if (updateUninstallNpmDependencies.length > 0) {
392
- await uninstallNpmDependencies(
393
- projectPath,
394
- updateUninstallNpmDependencies,
395
- true
396
- );
739
+ // Only uninstall if there are packages to uninstall
740
+ const uniq = (arr) => Array.from(new Set(arr));
741
+ const npmToUninstall = uniq(updateUninstallNpmDependencies);
742
+ const composerToUninstall = uniq(updateUninstallComposerDependencies);
743
+ if (npmToUninstall.length > 0) {
744
+ console.log(`Uninstalling npm packages: ${npmToUninstall.join(", ")}`);
745
+ await uninstallNpmDependencies(projectPath, npmToUninstall, true);
397
746
  }
398
- if (updateUninstallComposerDependencies.length > 0) {
399
- await uninstallComposerDependencies(
400
- projectPath,
401
- updateUninstallComposerDependencies
747
+ if (composerToUninstall.length > 0) {
748
+ console.log(
749
+ `Uninstalling composer packages: ${composerToUninstall.join(", ")}`
402
750
  );
751
+ await uninstallComposerDependencies(projectPath, composerToUninstall);
403
752
  }
404
753
  }
405
754
  const projectPathModified = projectPath.replace(/\\/g, "\\");
@@ -415,6 +764,7 @@ async function main() {
415
764
  swaggerDocs: answer.swaggerDocs,
416
765
  tailwindcss: answer.tailwindcss,
417
766
  websocket: answer.websocket,
767
+ mcp: answer.mcp,
418
768
  prisma: answer.prisma,
419
769
  docker: answer.docker,
420
770
  version: latestVersionOfCreatePrismaPhpApp,