create-prisma-php-app 1.28.8 → 2.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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";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"],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 i=c.replace(/(?<!:)(\/\/+)/g,"/"),o=n.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 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"},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 i={...n.scripts};i.browserSync="tsx settings/bs-config.ts",i.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`,n.scripts=i,n.type="module",s.prisma&&(n.prisma={seed:"tsx prisma/seed.ts"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e,s){const t=path.join(e,"composer.json");if(checkExcludeFiles(t))return;let n;if(fs.existsSync(t)){{const e=fs.readFileSync(t,"utf8");n=JSON.parse(e)}s.websocket&&(n.require={...n.require,"cboden/ratchet":"^0.4.4"}),s.prisma&&(n.require={...n.require,"ramsey/uuid":"5.x-dev","calicastle/cuid":"^2.0"}),fs.writeFileSync(t,JSON.stringify(n,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 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\\lib\\websocket"))return;if(!t.prisma&&n.includes("src\\lib\\prisma"))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")))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-sdk.ts")||s.includes("prisma-schema-config.json")||s.includes("prisma-schema.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 createOrUpdateTailwindConfig(e){const s=path.join(e,"tailwind.config.js");if(checkExcludeFiles(s))return;let t=fs.readFileSync(s,"utf8");const n=["./src/**/*.{html,js,php}"].map((e=>` "${e}"`)).join(",\n");t=t.replace(/content: \[\],/g,`content: [\n${n}\n],`),fs.writeFileSync(s,t,{flag:"w"})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,"export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\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/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);const i=c.length>0?"\n":"";e=e.replace("</head>",`${c}${i} \x3c!-- Dynamic Head --\x3e\n <?= MainLayout::outputHeadScripts() ?>\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:"/../composer.json",dest:"/composer.json"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"},{src:"/tailwind.config.js",dest:"/tailwind.config.js"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.prisma&&n.push({src:"/prisma",dest:"/prisma"}),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 i=fs.readFileSync(n,"utf8");fs.writeFileSync(c,i,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e,s),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&(createOrUpdateTailwindConfig(e),modifyPostcssConfig(e)),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const c='# Prisma PHP Auth Secret Key For development only - Change this in production\nAUTH_SECRET=uxsjXVPHN038DEYls2Kw0QUgBcXKUyrjv416nIFWPY4= \n \n# PHPMailer\n# SMTP_HOST=smtp.gmail.com or your SMTP host\n# SMTP_USERNAME=john.doe@gmail.com or your SMTP username\n# SMTP_PASSWORD=123456\n# SMTP_PORT=587 for TLS, 465 for SSL or your SMTP port\n# SMTP_ENCRYPTION=ssl or tls\n# MAIL_FROM=john.doe@gmail.com\n# MAIL_FROM_NAME="John Doe"\n\n# SHOW ERRORS - Set to true to show errors in the browser for development only - Change this in production to false\nSHOW_ERRORS=true\n\n# APP TIMEZONE - Set your application timezone - Default is "UTC"\nAPP_TIMEZONE="UTC"\n\n# APP ENV - Set your application environment - Default is "development" - Change this in production to "production"\nAPP_ENV=development';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${c}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,c)}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:!0,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"}));const i=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:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{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}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"],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 i=c.replace(/(?<!:)(\/\/+)/g,"/"),o=n.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 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"},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 i={...n.scripts};i.browserSync="tsx settings/bs-config.ts",i.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`,n.scripts=i,n.type="module",s.prisma&&(n.prisma={seed:"tsx prisma/seed.ts"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e,s){const t=path.join(e,"composer.json");if(checkExcludeFiles(t))return;let n;if(fs.existsSync(t)){{const e=fs.readFileSync(t,"utf8");n=JSON.parse(e)}s.websocket&&(n.require={...n.require,"cboden/ratchet":"^0.4.4"}),s.prisma&&(n.require={...n.require,"calicastle/cuid":"^2.0.0"}),fs.writeFileSync(t,JSON.stringify(n,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 generateLocalStoreKey(){return randomBytes(16).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\\lib\\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")))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 },\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/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:"/../composer.json",dest:"/composer.json"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});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 i=fs.readFileSync(n,"utf8");fs.writeFileSync(c,i,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e,s),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const c=`# Prisma PHP Auth Secret Key\nAUTH_SECRET="${generateAuthSecret()}"\n\n# PHPMailer\n# SMTP_HOST="smtp.gmail.com" or your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" or your SMTP username\n# SMTP_PASSWORD="123456"\n# SMTP_PORT="587" for TLS, 465 for SSL or your SMTP port\n# SMTP_ENCRYPTION="ssl" or tls\n# MAIL_FROM="john.doe@gmail.com"\n# MAIL_FROM_NAME="John Doe"\n\n# SHOW ERRORS - Set to true to show errors in the browser for development only - Change this in production to false\nSHOW_ERRORS="true"\n\n# APP TIMEZONE - Set your application timezone - Default is "UTC"\nAPP_TIMEZONE="UTC"\n\n# APP ENV - Set your application environment - Default is "development" - Change this in production to "production"\nAPP_ENV="development"\n\n# APP CACHE ENABLED - Set to true to enable caching - Default is false\nCACHE_ENABLED="false"\n# APP CACHE TTL - Set the cache time to live in seconds - Default is 600 seconds (10 minutes)\nCACHE_TTL="600"\n\n# LOCAL STORAGE KEY - Define a custom key for local storage.\n# If not set, it defaults to "pphp_local_store_59e13".\n# Spaces in the value will be replaced with underscores, and the key will be converted to lowercase automatically.\nLOCALSTORE_KEY="${generateLocalStoreKey()}"`;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${c}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,c)}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 i=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:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{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}}
3
3
  /**
4
4
  * Install dependencies in the specified directory.
5
5
  * @param {string} baseDir - The base directory where to install the dependencies.
@@ -31,6 +31,31 @@ async function installDependencies(baseDir, dependencies, isDev = false) {
31
31
  cwd: baseDir,
32
32
  });
33
33
  }
34
+ const pinnedVersions = {
35
+ "@prisma/client": "^6.4.1",
36
+ "@prisma/internals": "^6.4.1",
37
+ "@tailwindcss/postcss": "^4.0.8",
38
+ "@types/browser-sync": "^2.29.0",
39
+ "@types/node": "^22.13.5",
40
+ "@types/prompts": "^2.4.9",
41
+ autoprefixer: "^10.4.20",
42
+ "browser-sync": "^3.0.3",
43
+ chalk: "^5.4.1",
44
+ "chokidar-cli": "^3.0.0",
45
+ "http-proxy-middleware": "^3.0.3",
46
+ "npm-run-all": "^4.1.5",
47
+ "php-parser": "^3.2.2",
48
+ postcss: "^8.5.3",
49
+ "postcss-cli": "^11.0.0",
50
+ prisma: "^6.4.1",
51
+ prompts: "^2.4.2",
52
+ tailwindcss: "^4.0.8",
53
+ tsx: "^4.19.3",
54
+ typescript: "^5.7.3",
55
+ };
56
+ function pkg(name) {
57
+ return pinnedVersions[name] ? `${name}@${pinnedVersions[name]}` : name;
58
+ }
34
59
  async function main() {
35
60
  try {
36
61
  const args = process.argv.slice(2);
@@ -59,26 +84,28 @@ async function main() {
59
84
  }
60
85
  const currentDir = process.cwd();
61
86
  const configPath = path.join(currentDir, "prisma-php.json");
62
- const localSettings = readJsonFile(configPath);
63
- let excludeFiles = [];
64
- localSettings.excludeFiles?.map((file) => {
65
- const filePath = path.join(currentDir, file);
66
- if (fs.existsSync(filePath))
67
- excludeFiles.push(filePath.replace(/\\/g, "/"));
68
- });
69
- updateAnswer = {
70
- projectName,
71
- backendOnly: answer?.backendOnly ?? false,
72
- swaggerDocs: answer?.swaggerDocs ?? false,
73
- tailwindcss: answer?.tailwindcss ?? false,
74
- websocket: answer?.websocket ?? false,
75
- prisma: answer?.prisma ?? false,
76
- docker: answer?.docker ?? false,
77
- isUpdate: true,
78
- excludeFiles: localSettings.excludeFiles ?? [],
79
- excludeFilePath: excludeFiles ?? [],
80
- filePath: currentDir,
81
- };
87
+ if (fs.existsSync(configPath)) {
88
+ const localSettings = readJsonFile(configPath);
89
+ let excludeFiles = [];
90
+ localSettings.excludeFiles?.map((file) => {
91
+ const filePath = path.join(currentDir, file);
92
+ if (fs.existsSync(filePath))
93
+ excludeFiles.push(filePath.replace(/\\/g, "/"));
94
+ });
95
+ updateAnswer = {
96
+ projectName,
97
+ backendOnly: answer?.backendOnly ?? false,
98
+ swaggerDocs: answer?.swaggerDocs ?? false,
99
+ tailwindcss: answer?.tailwindcss ?? false,
100
+ websocket: answer?.websocket ?? false,
101
+ prisma: answer?.prisma ?? false,
102
+ docker: answer?.docker ?? false,
103
+ isUpdate: true,
104
+ excludeFiles: localSettings.excludeFiles ?? [],
105
+ excludeFilePath: excludeFiles ?? [],
106
+ filePath: currentDir,
107
+ };
108
+ }
82
109
  } else {
83
110
  answer = await getAnswer();
84
111
  }
@@ -99,10 +126,10 @@ async function main() {
99
126
  latestVersionOfCreatePrismaPhpApp
100
127
  ) === -1
101
128
  ) {
102
- execSync(`npm uninstall -g create-prisma-php-app`, {
129
+ execSync("npm uninstall -g create-prisma-php-app", {
103
130
  stdio: "inherit",
104
131
  });
105
- execSync(`npm install -g create-prisma-php-app`, {
132
+ execSync("npm install -g create-prisma-php-app", {
106
133
  stdio: "inherit",
107
134
  });
108
135
  }
@@ -117,50 +144,44 @@ async function main() {
117
144
  : path.join(currentDir, answer.projectName);
118
145
  if (!projectName) process.chdir(answer.projectName);
119
146
  const dependencies = [
120
- "typescript",
121
- "@types/node",
122
- "tsx",
123
- "ts-node",
124
- "http-proxy-middleware",
125
- "chalk",
126
- "npm-run-all",
127
- "browser-sync",
128
- "@types/browser-sync",
129
- "php-parser",
147
+ pkg("typescript"),
148
+ pkg("@types/node"),
149
+ pkg("tsx"),
150
+ pkg("http-proxy-middleware"),
151
+ pkg("chalk"),
152
+ pkg("npm-run-all"),
153
+ pkg("browser-sync"),
154
+ pkg("@types/browser-sync"),
155
+ pkg("php-parser"),
130
156
  ];
131
157
  if (answer.swaggerDocs) {
132
- dependencies.push("swagger-jsdoc", "@types/swagger-jsdoc");
158
+ dependencies.push(pkg("swagger-jsdoc"), pkg("@types/swagger-jsdoc"));
133
159
  }
134
160
  if (answer.swaggerDocs && answer.prisma) {
135
- dependencies.push("prompts", "@types/prompts", "@prisma/internals");
161
+ dependencies.push(pkg("prompts"), pkg("@types/prompts"));
136
162
  }
137
163
  if (answer.tailwindcss) {
138
164
  dependencies.push(
139
- "tailwindcss@^3.4.17",
140
- "autoprefixer",
141
- "postcss",
142
- "postcss-cli",
143
- "cssnano",
144
- "@jridgewell/gen-mapping"
165
+ pkg("tailwindcss"),
166
+ pkg("postcss"),
167
+ pkg("postcss-cli"),
168
+ pkg("@tailwindcss/postcss")
145
169
  );
146
170
  }
147
171
  if (answer.websocket) {
148
- dependencies.push("chokidar-cli");
172
+ dependencies.push(pkg("chokidar-cli"));
149
173
  }
150
174
  if (answer.prisma) {
151
- dependencies.push("prisma", "@prisma/client");
175
+ execSync("npm install -g prisma-client-php", { stdio: "inherit" });
152
176
  }
153
177
  await installDependencies(projectPath, dependencies, true);
154
178
  if (!projectName) {
155
- execSync(`npx tsc --init`, { stdio: "inherit" });
179
+ execSync("npx tsc --init", { stdio: "inherit" });
156
180
  }
157
- if (answer.tailwindcss)
158
- execSync(`npx tailwindcss init -p`, { stdio: "inherit" });
181
+ await createDirectoryStructure(projectPath, answer);
159
182
  if (answer.prisma) {
160
- if (!fs.existsSync(path.join(projectPath, "prisma")))
161
- execSync(`npx prisma init`, { stdio: "inherit" });
183
+ execSync("npx ppo init --prisma-php", { stdio: "inherit" });
162
184
  }
163
- await createDirectoryStructure(projectPath, answer);
164
185
  if (answer.swaggerDocs) {
165
186
  const swaggerDocsPath = path.join(
166
187
  projectPath,
@@ -235,12 +256,11 @@ async function main() {
235
256
  "swagger-jsdoc",
236
257
  "@types/swagger-jsdoc",
237
258
  "prompts",
238
- "@types/prompts",
239
- "@prisma/internals"
259
+ "@types/prompts"
240
260
  );
241
261
  }
242
262
  if (!updateAnswer.tailwindcss) {
243
- const tailwindFiles = ["postcss.config.js", "tailwind.config.js"];
263
+ const tailwindFiles = ["postcss.config.js"];
244
264
  tailwindFiles.forEach((file) => {
245
265
  const filePath = path.join(projectPath, file);
246
266
  if (fs.existsSync(filePath)) {
@@ -252,11 +272,9 @@ async function main() {
252
272
  });
253
273
  updateUninstallDependencies.push(
254
274
  "tailwindcss",
255
- "autoprefixer",
256
275
  "postcss",
257
276
  "postcss-cli",
258
- "cssnano",
259
- "@jridgewell/gen-mapping"
277
+ "@tailwindcss/postcss"
260
278
  );
261
279
  }
262
280
  if (!updateAnswer.websocket) {
@@ -286,7 +304,11 @@ async function main() {
286
304
  updateUninstallDependencies.push("chokidar-cli");
287
305
  }
288
306
  if (!updateAnswer.prisma) {
289
- updateUninstallDependencies.push("prisma", "@prisma/client");
307
+ updateUninstallDependencies.push(
308
+ "prisma",
309
+ "@prisma/client",
310
+ "@prisma/internals"
311
+ );
290
312
  }
291
313
  if (!updateAnswer.docker) {
292
314
  const dockerFiles = [
@@ -315,13 +337,11 @@ async function main() {
315
337
  }
316
338
  const projectPathModified = projectPath.replace(/\\/g, "\\");
317
339
  const bsConfig = bsConfigUrls(projectPathModified);
318
- const phpGenerateClassPath = answer.prisma ? "src/Lib/Prisma/Classes" : "";
319
340
  const prismaPhpConfig = {
320
341
  projectName: answer.projectName,
321
342
  projectRootPath: projectPathModified,
322
343
  phpEnvironment: "XAMPP",
323
344
  phpRootPathExe: "C:\\xampp\\php\\php.exe",
324
- phpGenerateClassPath,
325
345
  bsTarget: bsConfig.bsTarget,
326
346
  bsPathRewrite: bsConfig.bsPathRewrite,
327
347
  backendOnly: answer.backendOnly,
@@ -1,7 +1,5 @@
1
- export default {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- cssnano: {},
6
- },
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
7
5
  };
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env node
2
- import chalk from"chalk";import{spawn}from"child_process";import fs from"fs";import path from"path";import prompts from"prompts";import{fileURLToPath}from"url";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),args=process.argv.slice(2),readJsonFile=e=>{const t=fs.readFileSync(e,"utf8");return JSON.parse(t)},executeCommand=(e,t=[],o={})=>new Promise(((r,a)=>{const s=spawn(e,t,{stdio:"inherit",shell:!0,...o});s.on("error",(e=>{a(e)})),s.on("close",(e=>{0===e?r():a(new Error(`Process exited with code ${e}`))}))}));async function getAnswer(){const e=[{type:"toggle",name:"shouldProceed",message:`This command will update the ${chalk.blue("create-prisma-php-app")} package and overwrite all default files. ${chalk.blue("Do you want to proceed")}?`,initial:!1,active:"Yes",inactive:"No"}],t=await prompts(e,{onCancel:()=>{process.exit(0)}});return 0===Object.keys(t).length?null:t}const commandsToExecute={generateClass:"npx php generate class",update:"npx php update project"};
1
+ #!/usr/bin/env node
2
+ import chalk from"chalk";import{spawn}from"child_process";import fs from"fs";import path from"path";import prompts from"prompts";const args=process.argv.slice(2),readJsonFile=e=>{const o=fs.readFileSync(e,"utf8");return JSON.parse(o)},executeCommand=(e,o=[],t={})=>new Promise(((r,s)=>{const a=spawn(e,o,{stdio:"inherit",shell:!0,...t});a.on("error",(e=>{s(e)})),a.on("close",(e=>{0===e?r():s(new Error(`Process exited with code ${e}`))}))}));async function getAnswer(){const e=[{type:"toggle",name:"shouldProceed",message:`This command will update the ${chalk.blue("create-prisma-php-app")} package and overwrite all default files. ${chalk.blue("Do you want to proceed")}?`,initial:!1,active:"Yes",inactive:"No"}],o=await prompts(e,{onCancel:()=>{process.exit(0)}});return 0===Object.keys(o).length?null:o}const commandsToExecute={update:"npx php update project"};
3
3
  const main = async () => {
4
4
  if (args.length === 0) {
5
5
  console.log("No command provided.");
@@ -55,46 +55,6 @@ const main = async () => {
55
55
  }
56
56
  }
57
57
  }
58
- if (formattedCommand === commandsToExecute.generateClass) {
59
- try {
60
- const currentDir = process.cwd();
61
- const configPath = path.join(currentDir, "prisma-php.json");
62
- if (!fs.existsSync(configPath)) {
63
- console.error(
64
- chalk.red(
65
- "The configuration file 'prisma-php.json' was not found in the current directory."
66
- )
67
- );
68
- return;
69
- }
70
- const localSettings = readJsonFile(configPath);
71
- if (!localSettings.prisma) {
72
- console.error(
73
- chalk.red(
74
- "Install the 'Prisma PHP ORM' package by running the command 'npx php update project'."
75
- )
76
- );
77
- return;
78
- }
79
- const prismaClientPath = path.join(
80
- __dirname,
81
- "prisma-client-php",
82
- "index.js"
83
- );
84
- if (!fs.existsSync(prismaClientPath)) {
85
- console.error(
86
- chalk.red(
87
- "The 'prisma-client-php' package was not found in the current directory."
88
- )
89
- );
90
- return;
91
- }
92
- console.log("Executing command...\n");
93
- await executeCommand("node", [prismaClientPath]);
94
- } catch (error) {
95
- console.error("Error in script execution:", error);
96
- }
97
- }
98
58
  };
99
59
  main().catch((error) => {
100
60
  console.error("Unhandled error in main function:", error);
@@ -102,15 +102,39 @@ const deleteFilesIfExist = async (filePaths: string[]): Promise<void> => {
102
102
  console.error(`Error deleting ${filePath}:`, error);
103
103
  }
104
104
  }
105
+
106
+ // If the file is 'request-data.json', recreate it as an empty file
107
+ if (filePath.endsWith("request-data.json")) {
108
+ try {
109
+ await fsPromises.writeFile(filePath, ""); // Create an empty file
110
+ // console.log(`Created empty ${filePath}`);
111
+ } catch (error) {
112
+ console.error(`Error creating empty ${filePath}:`, error);
113
+ }
114
+ }
105
115
  }
106
116
  };
107
117
 
108
- const filePaths = [
118
+ async function deleteDirectoriesIfExist(dirPaths: string[]): Promise<void> {
119
+ for (const dirPath of dirPaths) {
120
+ try {
121
+ await fsPromises.rm(dirPath, { recursive: true, force: true });
122
+ console.log(`Deleted directory: ${dirPath}`);
123
+ } catch (error) {
124
+ console.error(`Error deleting directory (${dirPath}):`, error);
125
+ }
126
+ }
127
+ }
128
+
129
+ const filesToDelete = [
109
130
  join(__dirname, "request-data.json"),
110
131
  join(__dirname, "class-log.json"),
111
132
  join(__dirname, "class-imports.json"),
112
133
  ];
113
134
 
114
- await deleteFilesIfExist(filePaths);
135
+ const dirsToDelete = [join(__dirname, "..", "caches")];
136
+
137
+ await deleteFilesIfExist(filesToDelete);
138
+ await deleteDirectoriesIfExist(dirsToDelete);
115
139
  await updateAllClassLogs();
116
140
  await updateComponentImports();
@@ -7,10 +7,8 @@ namespace Lib\AI;
7
7
  use GuzzleHttp\Client;
8
8
  use GuzzleHttp\Exception\RequestException;
9
9
  use Lib\Validator;
10
+ use RuntimeException;
10
11
 
11
- /**
12
- * ChatGPTClient handles communication with the OpenAI API for generating chat responses.
13
- */
14
12
  class ChatGPTClient
15
13
  {
16
14
  private Client $client;
@@ -18,9 +16,6 @@ class ChatGPTClient
18
16
  private string $apiKey = '';
19
17
  private array $cache = [];
20
18
 
21
- /**
22
- * Constructor initializes the Guzzle HTTP client and sets up API configuration.
23
- */
24
19
  public function __construct(Client $client = null)
25
20
  {
26
21
  // Initialize the Guzzle HTTP client, allowing for dependency injection
@@ -42,9 +37,11 @@ class ChatGPTClient
42
37
  protected function determineModel(array $conversationHistory): string
43
38
  {
44
39
  $messageCount = count($conversationHistory);
45
- $totalTokens = array_reduce($conversationHistory, function ($carry, $item) {
46
- return $carry + str_word_count($item['content'] ?? '');
47
- }, 0);
40
+ $totalTokens = array_reduce(
41
+ $conversationHistory,
42
+ fn($carry, $item) => $carry + str_word_count($item['content'] ?? ''),
43
+ 0
44
+ );
48
45
 
49
46
  // If the conversation is long or complex, use a model with more tokens
50
47
  if ($totalTokens > 4000 || $messageCount > 10) {
@@ -75,13 +72,14 @@ class ChatGPTClient
75
72
  }
76
73
 
77
74
  /**
78
- * Sends a message to the OpenAI API and returns the AI's response.
75
+ * Sends a message to the OpenAI API and returns the AI's response as HTML.
76
+ *
77
+ * @param array $conversationHistory The conversation history array containing previous messages.
78
+ * @param string $userMessage The new user message to add to the conversation.
79
+ * @return string The AI-generated HTML response.
79
80
  *
80
- * @param array $conversationHistory The conversation history array containing previous messages.
81
- * @param string $userMessage The new user message to add to the conversation.
82
- * @return string The AI-generated response.
83
81
  * @throws \InvalidArgumentException If a message in the conversation history is not valid.
84
- * @throws \RuntimeException If the API request fails.
82
+ * @throws RuntimeException If the API request fails or returns an unexpected format.
85
83
  */
86
84
  public function sendMessage(array $conversationHistory, string $userMessage): string
87
85
  {
@@ -92,13 +90,20 @@ class ChatGPTClient
92
90
  // Optional: Convert emojis or special patterns in the message
93
91
  $userMessage = Validator::emojis($userMessage);
94
92
 
95
- // Format the conversation history
93
+ // Prepare the conversation, including a system-level instruction to return valid HTML
94
+ $systemInstruction = [
95
+ 'role' => 'system',
96
+ 'content' => 'You are ChatGPT. Please provide your response in valid HTML format.'
97
+ ];
98
+
99
+ // Format existing history, then prepend the system message
96
100
  $formattedHistory = $this->formatConversationHistory($conversationHistory);
101
+ array_unshift($formattedHistory, $systemInstruction);
97
102
 
98
- // Add the new user message
103
+ // Append the new user message
99
104
  $formattedHistory[] = ['role' => 'user', 'content' => $userMessage];
100
105
 
101
- // Check cache first
106
+ // Check cache
102
107
  $cacheKey = md5(serialize($formattedHistory));
103
108
  if (isset($this->cache[$cacheKey])) {
104
109
  return $this->cache[$cacheKey];
@@ -112,107 +117,31 @@ class ChatGPTClient
112
117
  $response = $this->client->request('POST', $this->apiUrl, [
113
118
  'headers' => [
114
119
  'Authorization' => 'Bearer ' . $this->apiKey,
115
- 'Content-Type' => 'application/json',
120
+ 'Content-Type' => 'application/json',
116
121
  ],
117
122
  'json' => [
118
- 'model' => $model,
119
- 'messages' => $formattedHistory,
120
- 'max_tokens' => 500,
123
+ 'model' => $model,
124
+ 'messages' => $formattedHistory,
125
+ 'max_tokens' => 500,
121
126
  ],
122
127
  ]);
123
128
 
124
- // Get the body of the response
125
- $responseBody = $response->getBody();
126
- $responseContent = json_decode($responseBody, true);
129
+ $responseBody = $response->getBody();
130
+ $responseContent = json_decode((string) $responseBody, true);
127
131
 
128
132
  // Check if response is in expected format
129
133
  if (isset($responseContent['choices'][0]['message']['content'])) {
130
134
  $aiMessage = $responseContent['choices'][0]['message']['content'];
135
+
131
136
  // Cache the result
132
137
  $this->cache[$cacheKey] = $aiMessage;
138
+
133
139
  return $aiMessage;
134
140
  }
135
141
 
136
- throw new \RuntimeException('Unexpected API response format.');
142
+ throw new RuntimeException('Unexpected API response format.');
137
143
  } catch (RequestException $e) {
138
- // Log error here if you have a logger
139
- throw new \RuntimeException("API request failed: " . $e->getMessage(), 0, $e);
140
- }
141
- }
142
-
143
- /**
144
- * Converts a GPT response to user-friendly HTML.
145
- *
146
- * @param string $gptResponse The raw response from GPT.
147
- * @return string The formatted HTML.
148
- */
149
- public function formatGPTResponseToHTML(string $gptResponse): string
150
- {
151
- try {
152
- // Decode all HTML entities including numeric ones
153
- $gptResponse = html_entity_decode($gptResponse, ENT_QUOTES | ENT_HTML401, 'UTF-8');
154
-
155
- // Decode HTML special characters that might still be encoded
156
- $gptResponse = htmlspecialchars_decode($gptResponse, ENT_QUOTES | ENT_HTML401);
157
-
158
- // Handle code blocks with optional language identifiers (```csharp ... ``` or ``` ... ```)
159
- $gptResponse = preg_replace_callback('/```(\w+)?\s*([\s\S]*?)\s*```/m', function ($matches) {
160
- $languageClass = isset($matches[1]) ? ' class="language-' . htmlspecialchars($matches[1], ENT_QUOTES | ENT_HTML401) . '"' : '';
161
- return '<pre><code' . $languageClass . '>' . htmlspecialchars($matches[2], ENT_QUOTES | ENT_HTML401) . '</code></pre>';
162
- }, $gptResponse);
163
-
164
- // Convert inline code (`code`) to <code></code> without double escaping
165
- $gptResponse = preg_replace_callback('/`([^`]+)`/', function ($matches) {
166
- return '<code>' . htmlspecialchars($matches[1], ENT_QUOTES | ENT_HTML401) . '</code>';
167
- }, $gptResponse);
168
-
169
- // Convert bold text (e.g., **text** or __text__ -> <strong>text</strong>)
170
- $gptResponse = preg_replace('/(\*\*|__)(.*?)\1/', '<strong>$2</strong>', $gptResponse);
171
-
172
- // Convert italic text (e.g., *text* or _text_ -> <em>text</em>)
173
- $gptResponse = preg_replace('/(\*|_)(.*?)\1/', '<em>$2</em>', $gptResponse);
174
-
175
- // Convert strikethrough text (e.g., ~~text~~ -> <del>text</del>)
176
- $gptResponse = preg_replace('/~~(.*?)~~/s', '<del>$1</del>', $gptResponse);
177
-
178
- // Convert Markdown links [text](url) to HTML links <a href="url">text</a>
179
- $gptResponse = preg_replace_callback('/\[(.*?)\]\((https?:\/\/[^\s]+)\)/', function ($matches) {
180
- return '<a href="' . htmlspecialchars($matches[2], ENT_QUOTES | ENT_HTML401) . '" target="_blank">' . htmlspecialchars($matches[1], ENT_QUOTES | ENT_HTML401) . '</a>';
181
- }, $gptResponse);
182
-
183
- // Auto-detect and convert raw URLs to clickable links
184
- $gptResponse = preg_replace('/(?<!href="|">)(https?:\/\/[^\s]+)/', '<a href="$1" target="_blank">$1</a>', $gptResponse);
185
-
186
- // Convert headers (e.g., # Header -> <h1>Header</h1>)
187
- $gptResponse = preg_replace_callback('/^(#{1,6})\s*(.*?)$/m', function ($matches) {
188
- $level = strlen($matches[1]);
189
- return '<h' . $level . '>' . htmlspecialchars($matches[2], ENT_QUOTES | ENT_HTML401) . '</h' . $level . '>';
190
- }, $gptResponse);
191
-
192
- // Convert blockquotes (e.g., > quote -> <blockquote>quote</blockquote>)
193
- $gptResponse = preg_replace('/^>\s*(.*?)$/m', '<blockquote>$1</blockquote>', $gptResponse);
194
-
195
- // Convert unordered lists (e.g., - item or * item -> <ul><li>item</li></ul>)
196
- $gptResponse = preg_replace_callback('/(?:^\s*[-*]\s+.*$(?:\n|$))+/m', function ($matches) {
197
- return '<ul>' . preg_replace('/^\s*[-*]\s+(.*)$/m', '<li>$1</li>', $matches[0]) . '</ul>';
198
- }, $gptResponse);
199
-
200
- // Convert ordered lists (e.g., 1. item -> <ol><li>item</li></ol>)
201
- $gptResponse = preg_replace_callback('/(?:^\d+\.\s+.*$(?:\n|$))+/m', function ($matches) {
202
- return '<ol>' . preg_replace('/^\s*\d+\.\s+(.*)$/m', '<li>$1</li>', $matches[0]) . '</ol>';
203
- }, $gptResponse);
204
-
205
- // Convert newlines to <br> for better formatting in HTML, except within <pre> and <code> tags
206
- $gptResponse = preg_replace_callback('/<(pre|code)>(.*?)<\/\1>/s', function ($matches) {
207
- return $matches[0]; // Keep preformatted text as it is
208
- }, $gptResponse);
209
-
210
- // Convert remaining newlines to <br>
211
- $gptResponse = nl2br($gptResponse);
212
-
213
- return $gptResponse;
214
- } catch (\Throwable) {
215
- return $gptResponse;
144
+ throw new RuntimeException("API request failed: " . $e->getMessage(), 0, $e);
216
145
  }
217
146
  }
218
147
  }