create-prisma-php-app 3.5.4 → 3.6.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/.htaccess +54 -41
- package/dist/bootstrap.php +12 -7
- package/dist/index.js +41 -30
- package/dist/settings/auto-swagger-docs.ts +195 -94
- package/dist/settings/bs-config.ts +53 -58
- package/dist/settings/project-name.ts +2 -0
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +44 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/Middleware/AuthMiddleware.php +6 -3
- package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
- package/dist/src/Lib/Websocket/websocket-server.php +105 -14
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
package/dist/.htaccess
CHANGED
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
# Turn on rewrite engine
|
|
2
2
|
RewriteEngine On
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# ------------------------------------------------------------------------------
|
|
5
|
+
# Block sensitive files (Apache 2.2 + 2.4 compatible)
|
|
6
|
+
# ------------------------------------------------------------------------------
|
|
5
7
|
<FilesMatch "(^\.htaccess|\.git|\.env|composer\.(json|lock)|package(-lock)?\.json|phpunit\.xml)$">
|
|
8
|
+
<IfModule mod_authz_core.c>
|
|
9
|
+
Require all denied
|
|
10
|
+
</IfModule>
|
|
11
|
+
<IfModule !mod_authz_core.c>
|
|
6
12
|
Order allow,deny
|
|
7
13
|
Deny from all
|
|
14
|
+
</IfModule>
|
|
8
15
|
</FilesMatch>
|
|
9
16
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
|
|
15
|
-
</IfModule>
|
|
17
|
+
# ------------------------------------------------------------------------------
|
|
18
|
+
# CORS: handled in PHP via Lib\Middleware\CorsMiddleware
|
|
19
|
+
# (Remove any Apache-level Access-Control-* headers to avoid conflicts)
|
|
20
|
+
# ------------------------------------------------------------------------------
|
|
16
21
|
|
|
17
|
-
#
|
|
22
|
+
# ------------------------------------------------------------------------------
|
|
23
|
+
# Content-Type with charset UTF-8 for HTML, CSS, and JS files
|
|
24
|
+
# ------------------------------------------------------------------------------
|
|
18
25
|
<IfModule mod_headers.c>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Header set Content-Type "application/javascript; charset=UTF-8"
|
|
32
|
-
</FilesMatch>
|
|
26
|
+
# HTML
|
|
27
|
+
<FilesMatch "\.(html|htm)$">
|
|
28
|
+
Header set Content-Type "text/html; charset=UTF-8"
|
|
29
|
+
</FilesMatch>
|
|
30
|
+
# CSS
|
|
31
|
+
<FilesMatch "\.(css)$">
|
|
32
|
+
Header set Content-Type "text/css; charset=UTF-8"
|
|
33
|
+
</FilesMatch>
|
|
34
|
+
# JS
|
|
35
|
+
<FilesMatch "\.(js)$">
|
|
36
|
+
Header set Content-Type "application/javascript; charset=UTF-8"
|
|
37
|
+
</FilesMatch>
|
|
33
38
|
</IfModule>
|
|
34
39
|
|
|
35
|
-
#
|
|
40
|
+
# ------------------------------------------------------------------------------
|
|
41
|
+
# Content Security Policy
|
|
42
|
+
# ------------------------------------------------------------------------------
|
|
36
43
|
<IfModule mod_headers.c>
|
|
37
44
|
Header set Content-Security-Policy "\
|
|
38
45
|
default-src 'self' https:; \
|
|
@@ -44,32 +51,38 @@ RewriteEngine On
|
|
|
44
51
|
object-src 'none';"
|
|
45
52
|
</IfModule>
|
|
46
53
|
|
|
47
|
-
#
|
|
54
|
+
# ------------------------------------------------------------------------------
|
|
55
|
+
# Security headers
|
|
56
|
+
# ------------------------------------------------------------------------------
|
|
48
57
|
<IfModule mod_headers.c>
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
# HSTS (only meaningful over HTTPS)
|
|
59
|
+
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
# XSS Protection (legacy but harmless)
|
|
62
|
+
Header set X-XSS-Protection "1; mode=block"
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
# Prevent MIME-type sniffing
|
|
65
|
+
Header set X-Content-Type-Options "nosniff"
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
# Clickjacking protection
|
|
68
|
+
Header always set X-Frame-Options "DENY"
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
# Referrer Policy
|
|
71
|
+
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
# Permissions Policy (tune as needed)
|
|
74
|
+
Header set Permissions-Policy "geolocation=(), microphone=(), camera=(), autoplay=()"
|
|
66
75
|
</IfModule>
|
|
67
76
|
|
|
68
|
-
#
|
|
77
|
+
# ------------------------------------------------------------------------------
|
|
78
|
+
# Preflight: route all OPTIONS to bootstrap so CorsMiddleware can reply 204
|
|
79
|
+
# ------------------------------------------------------------------------------
|
|
80
|
+
RewriteCond %{REQUEST_METHOD} =OPTIONS
|
|
81
|
+
RewriteRule ^ bootstrap.php [QSA,L]
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------------------
|
|
84
|
+
# Front controller: exclude static files, everything else -> bootstrap.php
|
|
85
|
+
# ------------------------------------------------------------------------------
|
|
69
86
|
RewriteCond %{REQUEST_URI} !\.(css|js|png|jpe?g|gif|svg|webp|woff2?|ttf|eot|ico|pdf|mp4|webm|mp3|ogg)$ [NC]
|
|
70
87
|
RewriteCond %{REQUEST_URI} !^/bootstrap.php
|
|
71
88
|
RewriteRule ^(.*)$ bootstrap.php [QSA,L]
|
|
72
|
-
|
|
73
|
-
# Ensure OPTIONS requests are handled correctly
|
|
74
|
-
RewriteCond %{REQUEST_METHOD} OPTIONS
|
|
75
|
-
RewriteRule ^ - [R=200,L]
|
package/dist/bootstrap.php
CHANGED
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
declare(strict_types=1);
|
|
4
4
|
|
|
5
|
-
if (session_status() === PHP_SESSION_NONE) {
|
|
6
|
-
session_start();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
10
6
|
require_once __DIR__ . '/settings/paths.php';
|
|
11
7
|
|
|
12
8
|
use Dotenv\Dotenv;
|
|
9
|
+
use Lib\Middleware\CorsMiddleware;
|
|
10
|
+
|
|
11
|
+
// Load environment variables
|
|
12
|
+
Dotenv::createImmutable(DOCUMENT_PATH)->load();
|
|
13
|
+
|
|
14
|
+
// CORS must run before sessions/any output
|
|
15
|
+
CorsMiddleware::handle();
|
|
16
|
+
|
|
17
|
+
if (session_status() === PHP_SESSION_NONE) {
|
|
18
|
+
session_start();
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
use Lib\Request;
|
|
14
22
|
use Lib\PrismaPHPSettings;
|
|
15
23
|
use Lib\StateManager;
|
|
@@ -56,9 +64,6 @@ final class Bootstrap extends RuntimeException
|
|
|
56
64
|
|
|
57
65
|
public static function run(): void
|
|
58
66
|
{
|
|
59
|
-
// Load environment variables
|
|
60
|
-
Dotenv::createImmutable(DOCUMENT_PATH)->load();
|
|
61
|
-
|
|
62
67
|
// Set timezone
|
|
63
68
|
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
|
|
64
69
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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"];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){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 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\\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 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"});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||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)},n=await prompts(s,{onCancel:t}),c=[];n.backendOnly??e.backendOnly??!1?(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:!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"})):(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"];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.mcp&&(n.scripts={...n.scripts,mcp:"tsx settings/restart-mcp.ts"},c.push("mcp")),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){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 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\\lib\\websocket"))return;if(!t.mcp&&n.includes("src\\lib\\mcp"))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"))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: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"});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)}"\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${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||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)},n=await prompts(s,{onCancel:t}),c=[];n.backendOnly??e.backendOnly??!1?(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:!1,active:"Yes",inactive:"No"}),e.mcp||c.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||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"})):(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.mcp||c.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||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,mcp:o.mcp??e.mcp??!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}}
|
|
3
3
|
/**
|
|
4
4
|
* Install dependencies in the specified directory.
|
|
5
5
|
* @param {string} baseDir - The base directory where to install the dependencies.
|
|
@@ -140,7 +140,6 @@ const npmPinnedVersions = {
|
|
|
140
140
|
"@types/prompts": "^2.4.9",
|
|
141
141
|
"browser-sync": "^3.0.4",
|
|
142
142
|
chalk: "^5.5.0",
|
|
143
|
-
"chokidar-cli": "^3.0.0",
|
|
144
143
|
cssnano: "^7.1.0",
|
|
145
144
|
"http-proxy-middleware": "^3.0.5",
|
|
146
145
|
"npm-run-all": "^4.1.5",
|
|
@@ -165,6 +164,7 @@ const composerPinnedVersions = {
|
|
|
165
164
|
"brick/math": "^0.13.1",
|
|
166
165
|
"cboden/ratchet": "^0.4.4",
|
|
167
166
|
"tsnc/prisma-php": "^1.0.0",
|
|
167
|
+
"php-mcp/server": "3.3.0",
|
|
168
168
|
};
|
|
169
169
|
function composerPkg(name) {
|
|
170
170
|
return composerPinnedVersions[name]
|
|
@@ -196,6 +196,7 @@ async function main() {
|
|
|
196
196
|
swaggerDocs: localSettings.swaggerDocs,
|
|
197
197
|
tailwindcss: localSettings.tailwindcss,
|
|
198
198
|
websocket: localSettings.websocket,
|
|
199
|
+
mcp: localSettings.mcp,
|
|
199
200
|
prisma: localSettings.prisma,
|
|
200
201
|
docker: localSettings.docker,
|
|
201
202
|
isUpdate: true,
|
|
@@ -215,6 +216,7 @@ async function main() {
|
|
|
215
216
|
websocket: args.includes("--websocket") || localSettings.websocket,
|
|
216
217
|
prisma: args.includes("--prisma") || localSettings.prisma,
|
|
217
218
|
docker: args.includes("--docker") || localSettings.docker,
|
|
219
|
+
mcp: args.includes("--mcp") || localSettings.mcp,
|
|
218
220
|
};
|
|
219
221
|
answer = await getAnswer(predefinedAnswers);
|
|
220
222
|
// IMPORTANT: Update updateAnswer with the NEW answer after getting user input
|
|
@@ -225,6 +227,7 @@ async function main() {
|
|
|
225
227
|
swaggerDocs: answer.swaggerDocs,
|
|
226
228
|
tailwindcss: answer.tailwindcss,
|
|
227
229
|
websocket: answer.websocket,
|
|
230
|
+
mcp: answer.mcp,
|
|
228
231
|
prisma: answer.prisma,
|
|
229
232
|
docker: answer.docker,
|
|
230
233
|
isUpdate: true,
|
|
@@ -239,6 +242,7 @@ async function main() {
|
|
|
239
242
|
let useSwaggerDocs = args.includes("--swagger-docs");
|
|
240
243
|
let useTailwind = args.includes("--tailwindcss");
|
|
241
244
|
let useWebsocket = args.includes("--websocket");
|
|
245
|
+
let useMcp = args.includes("--mcp");
|
|
242
246
|
let usePrisma = args.includes("--prisma");
|
|
243
247
|
let useDocker = args.includes("--docker");
|
|
244
248
|
const predefinedAnswers = {
|
|
@@ -247,6 +251,7 @@ async function main() {
|
|
|
247
251
|
swaggerDocs: useSwaggerDocs,
|
|
248
252
|
tailwindcss: useTailwind,
|
|
249
253
|
websocket: useWebsocket,
|
|
254
|
+
mcp: useMcp,
|
|
250
255
|
prisma: usePrisma,
|
|
251
256
|
docker: useDocker,
|
|
252
257
|
};
|
|
@@ -334,9 +339,11 @@ async function main() {
|
|
|
334
339
|
);
|
|
335
340
|
}
|
|
336
341
|
if (answer.websocket) {
|
|
337
|
-
npmDependencies.push(npmPkg("chokidar-cli"));
|
|
338
342
|
composerDependencies.push("cboden/ratchet");
|
|
339
343
|
}
|
|
344
|
+
if (answer.mcp) {
|
|
345
|
+
composerDependencies.push("php-mcp/server");
|
|
346
|
+
}
|
|
340
347
|
if (answer.prisma) {
|
|
341
348
|
execSync("npm install -g prisma-client-php", { stdio: "inherit" });
|
|
342
349
|
}
|
|
@@ -489,10 +496,7 @@ async function main() {
|
|
|
489
496
|
});
|
|
490
497
|
}
|
|
491
498
|
if (!updateAnswer.websocket) {
|
|
492
|
-
const websocketFiles = [
|
|
493
|
-
"restart-websocket.ts",
|
|
494
|
-
"restart-websocket.bat",
|
|
495
|
-
];
|
|
499
|
+
const websocketFiles = ["restart-websocket.ts"];
|
|
496
500
|
websocketFiles.forEach((file) => {
|
|
497
501
|
const filePath = path.join(projectPath, "settings", file);
|
|
498
502
|
if (fs.existsSync(filePath)) {
|
|
@@ -510,14 +514,30 @@ async function main() {
|
|
|
510
514
|
fs.rmSync(websocketFolder, { recursive: true, force: true });
|
|
511
515
|
console.log(`Websocket folder was deleted successfully.`);
|
|
512
516
|
}
|
|
513
|
-
//
|
|
514
|
-
if (isNpmPackageInstalled("chokidar-cli")) {
|
|
515
|
-
updateUninstallNpmDependencies.push("chokidar-cli");
|
|
516
|
-
}
|
|
517
|
+
// composer package for websocket only
|
|
517
518
|
if (isComposerPackageInstalled("cboden/ratchet")) {
|
|
518
519
|
updateUninstallComposerDependencies.push("cboden/ratchet");
|
|
519
520
|
}
|
|
520
521
|
}
|
|
522
|
+
if (!updateAnswer.mcp) {
|
|
523
|
+
const mcpFiles = ["restart-mcp.ts"];
|
|
524
|
+
mcpFiles.forEach((file) => {
|
|
525
|
+
const filePath = path.join(projectPath, "settings", file);
|
|
526
|
+
if (fs.existsSync(filePath)) {
|
|
527
|
+
fs.unlinkSync(filePath);
|
|
528
|
+
console.log(`${file} was deleted successfully.`);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
const mcpFolder = path.join(projectPath, "src", "Lib", "MCP");
|
|
532
|
+
if (fs.existsSync(mcpFolder)) {
|
|
533
|
+
fs.rmSync(mcpFolder, { recursive: true, force: true });
|
|
534
|
+
console.log(`MCP folder was deleted successfully.`);
|
|
535
|
+
}
|
|
536
|
+
// composer package for MCP only
|
|
537
|
+
if (isComposerPackageInstalled("php-mcp/server")) {
|
|
538
|
+
updateUninstallComposerDependencies.push("php-mcp/server");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
521
541
|
if (!updateAnswer.prisma) {
|
|
522
542
|
const prismaPackages = [
|
|
523
543
|
"prisma",
|
|
@@ -546,28 +566,18 @@ async function main() {
|
|
|
546
566
|
});
|
|
547
567
|
}
|
|
548
568
|
// Only uninstall if there are packages to uninstall
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
);
|
|
555
|
-
await uninstallNpmDependencies(
|
|
556
|
-
projectPath,
|
|
557
|
-
updateUninstallNpmDependencies,
|
|
558
|
-
true
|
|
559
|
-
);
|
|
569
|
+
const uniq = (arr) => Array.from(new Set(arr));
|
|
570
|
+
const npmToUninstall = uniq(updateUninstallNpmDependencies);
|
|
571
|
+
const composerToUninstall = uniq(updateUninstallComposerDependencies);
|
|
572
|
+
if (npmToUninstall.length > 0) {
|
|
573
|
+
console.log(`Uninstalling npm packages: ${npmToUninstall.join(", ")}`);
|
|
574
|
+
await uninstallNpmDependencies(projectPath, npmToUninstall, true);
|
|
560
575
|
}
|
|
561
|
-
if (
|
|
576
|
+
if (composerToUninstall.length > 0) {
|
|
562
577
|
console.log(
|
|
563
|
-
`Uninstalling composer packages: ${
|
|
564
|
-
", "
|
|
565
|
-
)}`
|
|
566
|
-
);
|
|
567
|
-
await uninstallComposerDependencies(
|
|
568
|
-
projectPath,
|
|
569
|
-
updateUninstallComposerDependencies
|
|
578
|
+
`Uninstalling composer packages: ${composerToUninstall.join(", ")}`
|
|
570
579
|
);
|
|
580
|
+
await uninstallComposerDependencies(projectPath, composerToUninstall);
|
|
571
581
|
}
|
|
572
582
|
}
|
|
573
583
|
const projectPathModified = projectPath.replace(/\\/g, "\\");
|
|
@@ -583,6 +593,7 @@ async function main() {
|
|
|
583
593
|
swaggerDocs: answer.swaggerDocs,
|
|
584
594
|
tailwindcss: answer.tailwindcss,
|
|
585
595
|
websocket: answer.websocket,
|
|
596
|
+
mcp: answer.mcp,
|
|
586
597
|
prisma: answer.prisma,
|
|
587
598
|
docker: answer.docker,
|
|
588
599
|
version: latestVersionOfCreatePrismaPhpApp,
|