create-prisma-php-app 1.28.7 → 2.0.0-alpha.1

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
- #!/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}}
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"],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.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 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);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&&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="${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.2.1",
36
+ "@types/browser-sync": "^2.29.0",
37
+ "@types/node": "^22.10.7",
38
+ "@types/prompts": "^2.4.9",
39
+ "@prisma/internals": "^6.2.1",
40
+ autoprefixer: "^10.4.20",
41
+ "browser-sync": "^3.0.3",
42
+ chalk: "^5.4.1",
43
+ "chokidar-cli": "^3.0.0",
44
+ "http-proxy-middleware": "^3.0.3",
45
+ "npm-run-all": "^4.1.5",
46
+ "php-parser": "^3.2.2",
47
+ prompts: "^2.4.2",
48
+ postcss: "^8.5.1",
49
+ "postcss-cli": "^11.0.0",
50
+ prisma: "^6.2.1",
51
+ tailwindcss: "^4.0.0",
52
+ "@tailwindcss/postcss": "^4.0.0",
53
+ tsx: "^4.19.2",
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);
@@ -117,45 +142,45 @@ async function main() {
117
142
  : path.join(currentDir, answer.projectName);
118
143
  if (!projectName) process.chdir(answer.projectName);
119
144
  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",
145
+ pkg("typescript"),
146
+ pkg("@types/node"),
147
+ pkg("tsx"),
148
+ pkg("http-proxy-middleware"),
149
+ pkg("chalk"),
150
+ pkg("npm-run-all"),
151
+ pkg("browser-sync"),
152
+ pkg("@types/browser-sync"),
153
+ pkg("php-parser"),
130
154
  ];
131
155
  if (answer.swaggerDocs) {
132
- dependencies.push("swagger-jsdoc", "@types/swagger-jsdoc");
156
+ dependencies.push(pkg("swagger-jsdoc"), pkg("@types/swagger-jsdoc"));
133
157
  }
134
158
  if (answer.swaggerDocs && answer.prisma) {
135
- dependencies.push("prompts", "@types/prompts", "@prisma/internals");
159
+ dependencies.push(
160
+ pkg("prompts"),
161
+ pkg("@types/prompts"),
162
+ pkg("@prisma/internals")
163
+ );
136
164
  }
137
165
  if (answer.tailwindcss) {
138
166
  dependencies.push(
139
- "tailwindcss",
140
- "autoprefixer",
141
- "postcss",
142
- "postcss-cli",
143
- "cssnano",
144
- "@jridgewell/gen-mapping"
167
+ pkg("tailwindcss"),
168
+ pkg("postcss"),
169
+ pkg("postcss-cli"),
170
+ pkg("@tailwindcss/postcss")
145
171
  );
146
172
  }
147
173
  if (answer.websocket) {
148
- dependencies.push("chokidar-cli");
174
+ dependencies.push(pkg("chokidar-cli"));
149
175
  }
150
176
  if (answer.prisma) {
151
- dependencies.push("prisma", "@prisma/client");
177
+ dependencies.push(pkg("prisma"), pkg("@prisma/client"));
178
+ execSync("npm install -g prisma-client-php", { stdio: "inherit" });
152
179
  }
153
180
  await installDependencies(projectPath, dependencies, true);
154
181
  if (!projectName) {
155
182
  execSync(`npx tsc --init`, { stdio: "inherit" });
156
183
  }
157
- if (answer.tailwindcss)
158
- execSync(`npx tailwindcss init -p`, { stdio: "inherit" });
159
184
  if (answer.prisma) {
160
185
  if (!fs.existsSync(path.join(projectPath, "prisma")))
161
186
  execSync(`npx prisma init`, { stdio: "inherit" });
@@ -240,7 +265,7 @@ async function main() {
240
265
  );
241
266
  }
242
267
  if (!updateAnswer.tailwindcss) {
243
- const tailwindFiles = ["postcss.config.js", "tailwind.config.js"];
268
+ const tailwindFiles = ["postcss.config.js"];
244
269
  tailwindFiles.forEach((file) => {
245
270
  const filePath = path.join(projectPath, file);
246
271
  if (fs.existsSync(filePath)) {
@@ -252,11 +277,9 @@ async function main() {
252
277
  });
253
278
  updateUninstallDependencies.push(
254
279
  "tailwindcss",
255
- "autoprefixer",
256
280
  "postcss",
257
281
  "postcss-cli",
258
- "cssnano",
259
- "@jridgewell/gen-mapping"
282
+ "@tailwindcss/postcss"
260
283
  );
261
284
  }
262
285
  if (!updateAnswer.websocket) {
@@ -315,13 +338,11 @@ async function main() {
315
338
  }
316
339
  const projectPathModified = projectPath.replace(/\\/g, "\\");
317
340
  const bsConfig = bsConfigUrls(projectPathModified);
318
- const phpGenerateClassPath = answer.prisma ? "src/Lib/Prisma/Classes" : "";
319
341
  const prismaPhpConfig = {
320
342
  projectName: answer.projectName,
321
343
  projectRootPath: projectPathModified,
322
344
  phpEnvironment: "XAMPP",
323
345
  phpRootPathExe: "C:\\xampp\\php\\php.exe",
324
- phpGenerateClassPath,
325
346
  bsTarget: bsConfig.bsTarget,
326
347
  bsPathRewrite: bsConfig.bsPathRewrite,
327
348
  backendOnly: answer.backendOnly,
@@ -1,7 +1,5 @@
1
1
  export default {
2
2
  plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- cssnano: {},
3
+ "@tailwindcss/postcss": {},
6
4
  },
7
5
  };
@@ -22,7 +22,7 @@
22
22
  // },
23
23
  // ];
24
24
 
25
- async function main() {
25
+ // async function main() {
26
26
  // ========================================
27
27
  // Code for PostgreSQL
28
28
  // ----------------------------------------
@@ -63,7 +63,7 @@ async function main() {
63
63
  // await prisma.user.deleteMany();
64
64
  // await prisma.user.createMany({ data: userData });
65
65
  // ========================================
66
- }
66
+ // }
67
67
 
68
68
  // main()
69
69
  // .catch((e) => {
@@ -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);
@@ -22,3 +22,7 @@ export const prismaSdk = async (): Promise<void> => {
22
22
  console.error("Error parsing schema:", error);
23
23
  }
24
24
  };
25
+
26
+ if (process.argv[1] && process.argv[1].endsWith("prisma-sdk.ts")) {
27
+ prismaSdk();
28
+ }
@@ -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
  }
@@ -0,0 +1,121 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Lib;
6
+
7
+ use Lib\PrismaPHPSettings;
8
+
9
+ class CacheHandler
10
+ {
11
+ public static bool $isCacheable = true; // Enable or disable caching by route
12
+ public static int $ttl = 0; // Time to live in seconds (0 = no action taken)
13
+
14
+ private static string $cacheDir = DOCUMENT_PATH . '/caches';
15
+ private static bool $cacheDirChecked = false;
16
+
17
+ private static function ensureCacheDirectoryExists(): void
18
+ {
19
+ if (self::$cacheDirChecked) {
20
+ return;
21
+ }
22
+
23
+ if (!is_dir(self::$cacheDir) && !mkdir(self::$cacheDir, 0777, true) && !is_dir(self::$cacheDir)) {
24
+ die("Error: Unable to create cache directory at: " . self::$cacheDir);
25
+ }
26
+
27
+ self::$cacheDirChecked = true;
28
+ }
29
+
30
+ public static function getCacheFilePath(string $uri): string
31
+ {
32
+ $requestFilesData = PrismaPHPSettings::$includeFiles;
33
+ $fileName = $requestFilesData[$uri]['fileName'] ?? '';
34
+ $isCacheable = $requestFilesData[$uri]['isCacheable'] ?? self::$isCacheable;
35
+
36
+ if (!$isCacheable || $fileName === '') {
37
+ return '';
38
+ }
39
+
40
+ return self::$cacheDir . '/' . $fileName . '.html';
41
+ }
42
+
43
+ private static function isExpired(string $cacheFile, int $ttlSeconds = 600): bool
44
+ {
45
+ if (!file_exists($cacheFile)) {
46
+ return true;
47
+ }
48
+ $fileAge = time() - filemtime($cacheFile);
49
+ return $fileAge > $ttlSeconds;
50
+ }
51
+
52
+ /**
53
+ * Serve cache if available, not expired, and route is marked cacheable.
54
+ * We look up a route-specific TTL if defined, otherwise use a fallback.
55
+ */
56
+ public static function serveCache(string $uri, int $defaultTtl = 600): void
57
+ {
58
+ if ($uri === '') {
59
+ return;
60
+ }
61
+
62
+ $requestFilesData = PrismaPHPSettings::$includeFiles;
63
+
64
+ // Get the route-specific TTL if set, or default to 0
65
+ $routeTtl = $requestFilesData[$uri]['cacheTtl'] ?? 0;
66
+
67
+ // If the route has a TTL greater than 0, use that.
68
+ // Otherwise (0 or not defined), use the default.
69
+ $ttlSeconds = ($routeTtl > 0) ? $routeTtl : $defaultTtl;
70
+
71
+ $cacheFile = self::getCacheFilePath($uri);
72
+
73
+ if ($cacheFile === '' || !file_exists($cacheFile) || self::isExpired($cacheFile, $ttlSeconds)) {
74
+ return;
75
+ }
76
+
77
+ echo "<!-- Cached copy generated at: " . date('Y-m-d H:i:s', filemtime($cacheFile)) . " -->\n";
78
+ readfile($cacheFile);
79
+ exit;
80
+ }
81
+
82
+ public static function saveCache(string $uri, string $content, bool $useLock = true): void
83
+ {
84
+ if ($uri === '') {
85
+ return;
86
+ }
87
+ self::ensureCacheDirectoryExists();
88
+
89
+ $cacheFile = self::getCacheFilePath($uri);
90
+ if ($cacheFile === '') {
91
+ return;
92
+ }
93
+
94
+ $flags = $useLock ? LOCK_EX : 0;
95
+ $written = @file_put_contents($cacheFile, $content, $flags);
96
+
97
+ if ($written === false) {
98
+ die("Error: Failed to write cache file: $cacheFile");
99
+ }
100
+ }
101
+
102
+ public static function resetCache(?string $uri = null): void
103
+ {
104
+ self::ensureCacheDirectoryExists();
105
+
106
+ if ($uri !== null) {
107
+ $cacheFile = self::getCacheFilePath($uri);
108
+ if ($cacheFile !== '' && file_exists($cacheFile)) {
109
+ unlink($cacheFile);
110
+ }
111
+ return;
112
+ }
113
+
114
+ $files = glob(self::$cacheDir . '/*.html') ?: [];
115
+ foreach ($files as $file) {
116
+ if (is_file($file)) {
117
+ unlink($file);
118
+ }
119
+ }
120
+ }
121
+ }