create-prisma-php-app 4.0.0-alpha.47 → 4.0.0-alpha.49
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 +166 -1051
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,857 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync, spawnSync } from "child_process";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import chalk from "chalk";
|
|
7
|
-
import prompts from "prompts";
|
|
8
|
-
import https from "https";
|
|
9
|
-
import { randomBytes } from "crypto";
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
let updateAnswer = null;
|
|
13
|
-
const nonBackendFiles = [
|
|
14
|
-
"favicon.ico",
|
|
15
|
-
"\\src\\app\\index.php",
|
|
16
|
-
"metadata.php",
|
|
17
|
-
"not-found.php",
|
|
18
|
-
"error.php",
|
|
19
|
-
];
|
|
20
|
-
const dockerFiles = [
|
|
21
|
-
".dockerignore",
|
|
22
|
-
"docker-compose.yml",
|
|
23
|
-
"Dockerfile",
|
|
24
|
-
"apache.conf",
|
|
25
|
-
];
|
|
26
|
-
const STARTER_KITS = {
|
|
27
|
-
basic: {
|
|
28
|
-
id: "basic",
|
|
29
|
-
name: "Basic PHP Application",
|
|
30
|
-
description: "Simple PHP backend with minimal dependencies",
|
|
31
|
-
features: {
|
|
32
|
-
backendOnly: true,
|
|
33
|
-
tailwindcss: false,
|
|
34
|
-
websocket: false,
|
|
35
|
-
prisma: false,
|
|
36
|
-
docker: false,
|
|
37
|
-
swaggerDocs: false,
|
|
38
|
-
mcp: false,
|
|
39
|
-
},
|
|
40
|
-
requiredFiles: [
|
|
41
|
-
"bootstrap.php",
|
|
42
|
-
".htaccess",
|
|
43
|
-
"src/app/layout.php",
|
|
44
|
-
"src/app/index.php",
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
fullstack: {
|
|
48
|
-
id: "fullstack",
|
|
49
|
-
name: "Full-Stack Application",
|
|
50
|
-
description: "Complete web application with frontend and backend",
|
|
51
|
-
features: {
|
|
52
|
-
backendOnly: false,
|
|
53
|
-
tailwindcss: true,
|
|
54
|
-
websocket: false,
|
|
55
|
-
prisma: true,
|
|
56
|
-
docker: false,
|
|
57
|
-
swaggerDocs: true,
|
|
58
|
-
mcp: false,
|
|
59
|
-
},
|
|
60
|
-
requiredFiles: [
|
|
61
|
-
"bootstrap.php",
|
|
62
|
-
".htaccess",
|
|
63
|
-
"postcss.config.js",
|
|
64
|
-
"src/app/layout.php",
|
|
65
|
-
"src/app/index.php",
|
|
66
|
-
"src/app/js/index.js",
|
|
67
|
-
"src/app/css/tailwind.css",
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
api: {
|
|
71
|
-
id: "api",
|
|
72
|
-
name: "REST API",
|
|
73
|
-
description: "Backend API with database and documentation",
|
|
74
|
-
features: {
|
|
75
|
-
backendOnly: true,
|
|
76
|
-
tailwindcss: false,
|
|
77
|
-
websocket: false,
|
|
78
|
-
prisma: true,
|
|
79
|
-
docker: true,
|
|
80
|
-
swaggerDocs: true,
|
|
81
|
-
mcp: false,
|
|
82
|
-
},
|
|
83
|
-
requiredFiles: [
|
|
84
|
-
"bootstrap.php",
|
|
85
|
-
".htaccess",
|
|
86
|
-
"docker-compose.yml",
|
|
87
|
-
"Dockerfile",
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
realtime: {
|
|
91
|
-
id: "realtime",
|
|
92
|
-
name: "Real-time Application",
|
|
93
|
-
description: "Application with WebSocket support and MCP",
|
|
94
|
-
features: {
|
|
95
|
-
backendOnly: false,
|
|
96
|
-
tailwindcss: true,
|
|
97
|
-
websocket: true,
|
|
98
|
-
prisma: true,
|
|
99
|
-
docker: false,
|
|
100
|
-
swaggerDocs: true,
|
|
101
|
-
mcp: true,
|
|
102
|
-
},
|
|
103
|
-
requiredFiles: [
|
|
104
|
-
"bootstrap.php",
|
|
105
|
-
".htaccess",
|
|
106
|
-
"postcss.config.js",
|
|
107
|
-
"src/lib/websocket",
|
|
108
|
-
"src/lib/mcp",
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
// Custom starter kit examples
|
|
112
|
-
ecommerce: {
|
|
113
|
-
id: "ecommerce",
|
|
114
|
-
name: "E-commerce Starter",
|
|
115
|
-
description: "Full e-commerce application with cart, payments, and admin",
|
|
116
|
-
features: {
|
|
117
|
-
backendOnly: false,
|
|
118
|
-
tailwindcss: true,
|
|
119
|
-
websocket: false,
|
|
120
|
-
prisma: true,
|
|
121
|
-
docker: true,
|
|
122
|
-
swaggerDocs: true,
|
|
123
|
-
mcp: false,
|
|
124
|
-
},
|
|
125
|
-
requiredFiles: [],
|
|
126
|
-
source: {
|
|
127
|
-
type: "git",
|
|
128
|
-
url: "https://github.com/your-org/prisma-php-ecommerce-starter",
|
|
129
|
-
branch: "main",
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
blog: {
|
|
133
|
-
id: "blog",
|
|
134
|
-
name: "Blog CMS",
|
|
135
|
-
description: "Blog content management system",
|
|
136
|
-
features: {
|
|
137
|
-
backendOnly: false,
|
|
138
|
-
tailwindcss: true,
|
|
139
|
-
websocket: false,
|
|
140
|
-
prisma: true,
|
|
141
|
-
docker: false,
|
|
142
|
-
swaggerDocs: false,
|
|
143
|
-
mcp: false,
|
|
144
|
-
},
|
|
145
|
-
requiredFiles: [],
|
|
146
|
-
source: {
|
|
147
|
-
type: "git",
|
|
148
|
-
url: "https://github.com/your-org/prisma-php-blog-starter",
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
function bsConfigUrls(projectRootPath) {
|
|
153
|
-
// Identify the base path dynamically up to and including 'htdocs'
|
|
154
|
-
const htdocsIndex = projectRootPath.indexOf("\\htdocs\\");
|
|
155
|
-
if (htdocsIndex === -1) {
|
|
156
|
-
console.error(
|
|
157
|
-
"Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"
|
|
158
|
-
);
|
|
159
|
-
return {
|
|
160
|
-
bsTarget: "",
|
|
161
|
-
bsPathRewrite: {},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
// Extract the path up to and including 'htdocs\\'
|
|
165
|
-
const basePathToRemove = projectRootPath.substring(
|
|
166
|
-
0,
|
|
167
|
-
htdocsIndex + "\\htdocs\\".length
|
|
168
|
-
);
|
|
169
|
-
// Escape backslashes for the regex pattern
|
|
170
|
-
const escapedBasePathToRemove = basePathToRemove.replace(/\\/g, "\\\\");
|
|
171
|
-
// Remove the base path and replace backslashes with forward slashes for URL compatibility
|
|
172
|
-
const relativeWebPath = projectRootPath
|
|
173
|
-
.replace(new RegExp(`^${escapedBasePathToRemove}`), "")
|
|
174
|
-
.replace(/\\/g, "/");
|
|
175
|
-
// Construct the Browser Sync command with the correct proxy URL, being careful not to affect the protocol part
|
|
176
|
-
let proxyUrl = `http://localhost/${relativeWebPath}`;
|
|
177
|
-
// Ensure the proxy URL does not end with a slash before appending '/public'
|
|
178
|
-
proxyUrl = proxyUrl.endsWith("/") ? proxyUrl.slice(0, -1) : proxyUrl;
|
|
179
|
-
// Clean the URL by replacing "//" with "/" but not affecting "http://"
|
|
180
|
-
// We replace instances of "//" that are not preceded by ":"
|
|
181
|
-
const cleanUrl = proxyUrl.replace(/(?<!:)(\/\/+)/g, "/");
|
|
182
|
-
const cleanRelativeWebPath = relativeWebPath.replace(/\/\/+/g, "/");
|
|
183
|
-
// Correct the relativeWebPath to ensure it does not start with a "/"
|
|
184
|
-
const adjustedRelativeWebPath = cleanRelativeWebPath.startsWith("/")
|
|
185
|
-
? cleanRelativeWebPath.substring(1)
|
|
186
|
-
: cleanRelativeWebPath;
|
|
187
|
-
return {
|
|
188
|
-
bsTarget: `${cleanUrl}/`,
|
|
189
|
-
bsPathRewrite: {
|
|
190
|
-
"^/": `/${adjustedRelativeWebPath}/`,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
async function updatePackageJson(baseDir, answer) {
|
|
195
|
-
const packageJsonPath = path.join(baseDir, "package.json");
|
|
196
|
-
if (checkExcludeFiles(packageJsonPath)) return;
|
|
197
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
198
|
-
packageJson.scripts = {
|
|
199
|
-
...packageJson.scripts,
|
|
200
|
-
projectName: "tsx settings/project-name.ts",
|
|
201
|
-
};
|
|
202
|
-
let answersToInclude = [];
|
|
203
|
-
if (answer.tailwindcss) {
|
|
204
|
-
packageJson.scripts = {
|
|
205
|
-
...packageJson.scripts,
|
|
206
|
-
tailwind:
|
|
207
|
-
"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch",
|
|
208
|
-
"tailwind:build":
|
|
209
|
-
"postcss src/app/css/tailwind.css -o src/app/css/styles.css",
|
|
210
|
-
};
|
|
211
|
-
answersToInclude.push("tailwind");
|
|
212
|
-
}
|
|
213
|
-
if (answer.websocket) {
|
|
214
|
-
packageJson.scripts = {
|
|
215
|
-
...packageJson.scripts,
|
|
216
|
-
websocket: "tsx settings/restart-websocket.ts",
|
|
217
|
-
};
|
|
218
|
-
answersToInclude.push("websocket");
|
|
219
|
-
}
|
|
220
|
-
if (answer.mcp) {
|
|
221
|
-
packageJson.scripts = {
|
|
222
|
-
...packageJson.scripts,
|
|
223
|
-
mcp: "tsx settings/restart-mcp.ts",
|
|
224
|
-
};
|
|
225
|
-
answersToInclude.push("mcp");
|
|
226
|
-
}
|
|
227
|
-
if (answer.docker) {
|
|
228
|
-
packageJson.scripts = {
|
|
229
|
-
...packageJson.scripts,
|
|
230
|
-
docker: "docker-compose up",
|
|
231
|
-
};
|
|
232
|
-
answersToInclude.push("docker");
|
|
233
|
-
}
|
|
234
|
-
if (answer.swaggerDocs) {
|
|
235
|
-
const swaggerDocsExecuteScript = answer.prisma
|
|
236
|
-
? "tsx settings/auto-swagger-docs.ts"
|
|
237
|
-
: "tsx settings/swagger-config.ts";
|
|
238
|
-
packageJson.scripts = {
|
|
239
|
-
...packageJson.scripts,
|
|
240
|
-
"create-swagger-docs": swaggerDocsExecuteScript,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
// Initialize with existing scripts
|
|
244
|
-
let updatedScripts = {
|
|
245
|
-
...packageJson.scripts,
|
|
246
|
-
};
|
|
247
|
-
updatedScripts.browserSync = "tsx settings/bs-config.ts";
|
|
248
|
-
updatedScripts["browserSync:build"] = "tsx settings/build.ts";
|
|
249
|
-
updatedScripts.dev = `npm-run-all projectName -p browserSync ${answersToInclude.join(
|
|
250
|
-
" "
|
|
251
|
-
)}`;
|
|
252
|
-
updatedScripts.build = `npm-run-all${
|
|
253
|
-
answer.tailwindcss ? " tailwind:build" : ""
|
|
254
|
-
} browserSync:build`;
|
|
255
|
-
// Finally, assign the updated scripts back to packageJson
|
|
256
|
-
packageJson.scripts = updatedScripts;
|
|
257
|
-
packageJson.type = "module";
|
|
258
|
-
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
259
|
-
}
|
|
260
|
-
async function updateComposerJson(baseDir) {
|
|
261
|
-
const composerJsonPath = path.join(baseDir, "composer.json");
|
|
262
|
-
if (checkExcludeFiles(composerJsonPath)) return;
|
|
263
|
-
}
|
|
264
|
-
async function updateIndexJsForWebSocket(baseDir, answer) {
|
|
265
|
-
if (!answer.websocket) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
const indexPath = path.join(baseDir, "src", "app", "js", "index.js");
|
|
269
|
-
if (checkExcludeFiles(indexPath)) return;
|
|
270
|
-
let indexContent = fs.readFileSync(indexPath, "utf8");
|
|
271
|
-
// WebSocket initialization code to be appended
|
|
272
|
-
const webSocketCode = `
|
|
273
|
-
// WebSocket initialization
|
|
274
|
-
var ws = new WebSocket("ws://localhost:8080");
|
|
275
|
-
`;
|
|
276
|
-
// Append WebSocket code if user chose to use WebSocket
|
|
277
|
-
indexContent += webSocketCode;
|
|
278
|
-
fs.writeFileSync(indexPath, indexContent, "utf8");
|
|
279
|
-
console.log("WebSocket code added to index.js successfully.");
|
|
280
|
-
}
|
|
281
|
-
function generateAuthSecret() {
|
|
282
|
-
// Generate 33 random bytes and encode them as a base64 string
|
|
283
|
-
return randomBytes(33).toString("base64");
|
|
284
|
-
}
|
|
285
|
-
function generateHexEncodedKey(size = 16) {
|
|
286
|
-
return randomBytes(size).toString("hex"); // Hex encoding ensures safe session keys
|
|
287
|
-
}
|
|
288
|
-
// Recursive copy function
|
|
289
|
-
function copyRecursiveSync(src, dest, answer) {
|
|
290
|
-
const exists = fs.existsSync(src);
|
|
291
|
-
const stats = exists && fs.statSync(src);
|
|
292
|
-
const isDirectory = exists && stats && stats.isDirectory();
|
|
293
|
-
if (isDirectory) {
|
|
294
|
-
const destLower = dest.toLowerCase();
|
|
295
|
-
if (!answer.websocket && destLower.includes("src\\lib\\websocket")) return;
|
|
296
|
-
if (!answer.mcp && destLower.includes("src\\lib\\mcp")) return;
|
|
297
|
-
if (
|
|
298
|
-
(answer.backendOnly && destLower.includes("src\\app\\js")) ||
|
|
299
|
-
(answer.backendOnly && destLower.includes("src\\app\\css")) ||
|
|
300
|
-
(answer.backendOnly && destLower.includes("src\\app\\assets"))
|
|
301
|
-
)
|
|
302
|
-
return;
|
|
303
|
-
if (!answer.swaggerDocs && destLower.includes("src\\app\\swagger-docs"))
|
|
304
|
-
return;
|
|
305
|
-
const destModified = dest.replace(/\\/g, "/");
|
|
306
|
-
if (updateAnswer?.excludeFilePath?.includes(destModified)) return;
|
|
307
|
-
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
308
|
-
fs.readdirSync(src).forEach((childItemName) => {
|
|
309
|
-
copyRecursiveSync(
|
|
310
|
-
path.join(src, childItemName),
|
|
311
|
-
path.join(dest, childItemName),
|
|
312
|
-
answer
|
|
313
|
-
);
|
|
314
|
-
});
|
|
315
|
-
} else {
|
|
316
|
-
if (checkExcludeFiles(dest)) return;
|
|
317
|
-
if (
|
|
318
|
-
!answer.tailwindcss &&
|
|
319
|
-
(dest.includes("tailwind.css") || dest.includes("styles.css"))
|
|
320
|
-
)
|
|
321
|
-
return;
|
|
322
|
-
if (!answer.websocket && dest.includes("restart-websocket.ts")) return;
|
|
323
|
-
if (!answer.mcp && dest.includes("restart-mcp.ts")) return;
|
|
324
|
-
if (!answer.docker && dockerFiles.some((file) => dest.includes(file)))
|
|
325
|
-
return;
|
|
326
|
-
if (
|
|
327
|
-
answer.backendOnly &&
|
|
328
|
-
nonBackendFiles.some((file) => dest.includes(file))
|
|
329
|
-
)
|
|
330
|
-
return;
|
|
331
|
-
if (!answer.backendOnly && dest.includes("route.php")) return;
|
|
332
|
-
if (
|
|
333
|
-
answer.backendOnly &&
|
|
334
|
-
!answer.swaggerDocs &&
|
|
335
|
-
dest.includes("layout.php")
|
|
336
|
-
)
|
|
337
|
-
return;
|
|
338
|
-
if (!answer.swaggerDocs && dest.includes("swagger-config.ts")) return;
|
|
339
|
-
if (answer.tailwindcss && dest.includes("index.css")) return;
|
|
340
|
-
if (
|
|
341
|
-
(!answer.swaggerDocs || !answer.prisma) &&
|
|
342
|
-
(dest.includes("auto-swagger-docs.ts") ||
|
|
343
|
-
dest.includes("prisma-schema-config.json"))
|
|
344
|
-
)
|
|
345
|
-
return;
|
|
346
|
-
fs.copyFileSync(src, dest, 0);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
// Function to execute the recursive copy for entire directories
|
|
350
|
-
async function executeCopy(baseDir, directoriesToCopy, answer) {
|
|
351
|
-
directoriesToCopy.forEach(({ src: srcDir, dest: destDir }) => {
|
|
352
|
-
const sourcePath = path.join(__dirname, srcDir);
|
|
353
|
-
const destPath = path.join(baseDir, destDir);
|
|
354
|
-
copyRecursiveSync(sourcePath, destPath, answer);
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
function modifyPostcssConfig(baseDir) {
|
|
358
|
-
const filePath = path.join(baseDir, "postcss.config.js");
|
|
359
|
-
if (checkExcludeFiles(filePath)) return;
|
|
360
|
-
const newContent = `export default {
|
|
361
|
-
plugins: {
|
|
362
|
-
"@tailwindcss/postcss": {},
|
|
363
|
-
cssnano: {},
|
|
364
|
-
},
|
|
365
|
-
};`;
|
|
366
|
-
fs.writeFileSync(filePath, newContent, { flag: "w" });
|
|
367
|
-
console.log(chalk.green("postcss.config.js updated successfully."));
|
|
368
|
-
}
|
|
369
|
-
function modifyLayoutPHP(baseDir, answer) {
|
|
370
|
-
const layoutPath = path.join(baseDir, "src", "app", "layout.php");
|
|
371
|
-
if (checkExcludeFiles(layoutPath)) return;
|
|
372
|
-
try {
|
|
373
|
-
let indexContent = fs.readFileSync(layoutPath, "utf8");
|
|
374
|
-
let stylesAndLinks = "";
|
|
375
|
-
if (!answer.backendOnly) {
|
|
376
|
-
if (!answer.tailwindcss) {
|
|
377
|
-
stylesAndLinks = `\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />`;
|
|
378
|
-
}
|
|
379
|
-
stylesAndLinks += `\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"></script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"></script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"></script>`;
|
|
380
|
-
}
|
|
381
|
-
// Tailwind CSS link or CDN script
|
|
382
|
-
let tailwindLink = "";
|
|
383
|
-
if (!answer.backendOnly) {
|
|
384
|
-
tailwindLink = answer.tailwindcss
|
|
385
|
-
? ` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${stylesAndLinks}`
|
|
386
|
-
: stylesAndLinks;
|
|
387
|
-
}
|
|
388
|
-
// Insert before the closing </head> tag
|
|
389
|
-
indexContent = indexContent.replace(
|
|
390
|
-
"</head>",
|
|
391
|
-
`${tailwindLink}
|
|
392
|
-
</head>`
|
|
393
|
-
);
|
|
394
|
-
fs.writeFileSync(layoutPath, indexContent, { flag: "w" });
|
|
395
|
-
console.log(
|
|
396
|
-
chalk.green(
|
|
397
|
-
`layout.php modified successfully for ${
|
|
398
|
-
answer.tailwindcss ? "local Tailwind CSS" : "Tailwind CSS CDN"
|
|
399
|
-
}.`
|
|
400
|
-
)
|
|
401
|
-
);
|
|
402
|
-
} catch (error) {
|
|
403
|
-
console.error(chalk.red("Error modifying layout.php:"), error);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
// This function updates or creates the .env file
|
|
407
|
-
async function createOrUpdateEnvFile(baseDir, content) {
|
|
408
|
-
const envPath = path.join(baseDir, ".env");
|
|
409
|
-
if (checkExcludeFiles(envPath)) return;
|
|
410
|
-
console.log("🚀 ~ content:", content);
|
|
411
|
-
fs.writeFileSync(envPath, content, { flag: "w" });
|
|
412
|
-
}
|
|
413
|
-
function checkExcludeFiles(destPath) {
|
|
414
|
-
if (!updateAnswer?.isUpdate) return false;
|
|
415
|
-
return (
|
|
416
|
-
updateAnswer?.excludeFilePath?.includes(destPath.replace(/\\/g, "/")) ??
|
|
417
|
-
false
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
async function createDirectoryStructure(baseDir, answer) {
|
|
421
|
-
console.log("🚀 ~ baseDir:", baseDir);
|
|
422
|
-
console.log("🚀 ~ answer:", answer);
|
|
423
|
-
const filesToCopy = [
|
|
424
|
-
{ src: "/bootstrap.php", dest: "/bootstrap.php" },
|
|
425
|
-
{ src: "/.htaccess", dest: "/.htaccess" },
|
|
426
|
-
{ src: "/tsconfig.json", dest: "/tsconfig.json" },
|
|
427
|
-
{ src: "/app-gitignore", dest: "/.gitignore" },
|
|
428
|
-
];
|
|
429
|
-
if (answer.tailwindcss) {
|
|
430
|
-
filesToCopy.push({ src: "/postcss.config.js", dest: "/postcss.config.js" });
|
|
431
|
-
}
|
|
432
|
-
const directoriesToCopy = [
|
|
433
|
-
{
|
|
434
|
-
src: "/settings",
|
|
435
|
-
dest: "/settings",
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
src: "/src",
|
|
439
|
-
dest: "/src",
|
|
440
|
-
},
|
|
441
|
-
];
|
|
442
|
-
if (answer.docker) {
|
|
443
|
-
directoriesToCopy.push(
|
|
444
|
-
{ src: "/.dockerignore", dest: "/.dockerignore" },
|
|
445
|
-
{ src: "/docker-compose.yml", dest: "/docker-compose.yml" },
|
|
446
|
-
{ src: "/Dockerfile", dest: "/Dockerfile" },
|
|
447
|
-
{ src: "/apache.conf", dest: "/apache.conf" }
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
console.log("🚀 ~ directoriesToCopy:", directoriesToCopy);
|
|
451
|
-
filesToCopy.forEach(({ src, dest }) => {
|
|
452
|
-
const sourcePath = path.join(__dirname, src);
|
|
453
|
-
const destPath = path.join(baseDir, dest);
|
|
454
|
-
if (checkExcludeFiles(destPath)) return;
|
|
455
|
-
const code = fs.readFileSync(sourcePath, "utf8");
|
|
456
|
-
fs.writeFileSync(destPath, code, { flag: "w" });
|
|
457
|
-
});
|
|
458
|
-
await executeCopy(baseDir, directoriesToCopy, answer);
|
|
459
|
-
await updatePackageJson(baseDir, answer);
|
|
460
|
-
await updateComposerJson(baseDir);
|
|
461
|
-
if (!answer.backendOnly) {
|
|
462
|
-
await updateIndexJsForWebSocket(baseDir, answer);
|
|
463
|
-
}
|
|
464
|
-
if (answer.tailwindcss) {
|
|
465
|
-
modifyPostcssConfig(baseDir);
|
|
466
|
-
}
|
|
467
|
-
if (answer.tailwindcss || !answer.backendOnly || answer.swaggerDocs) {
|
|
468
|
-
modifyLayoutPHP(baseDir, answer);
|
|
469
|
-
}
|
|
470
|
-
const authSecret = generateAuthSecret();
|
|
471
|
-
const localStoreKey = generateHexEncodedKey();
|
|
472
|
-
const authCookieName = generateHexEncodedKey(8);
|
|
473
|
-
const functionCallSecret = generateHexEncodedKey(32);
|
|
474
|
-
const prismaPHPEnvContent = `# Authentication secret key for JWT or session encryption.
|
|
475
|
-
AUTH_SECRET="${authSecret}"
|
|
476
|
-
# Name of the authentication cookie.
|
|
477
|
-
AUTH_COOKIE_NAME="${authCookieName}"
|
|
478
|
-
|
|
479
|
-
# PHPMailer SMTP configuration (uncomment and set as needed)
|
|
480
|
-
# SMTP_HOST="smtp.gmail.com" # Your SMTP host
|
|
481
|
-
# SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username
|
|
482
|
-
# SMTP_PASSWORD="123456" # Your SMTP password
|
|
483
|
-
# SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port
|
|
484
|
-
# SMTP_ENCRYPTION="ssl" # ssl or tls
|
|
485
|
-
# MAIL_FROM="john.doe@gmail.com" # Sender email address
|
|
486
|
-
# MAIL_FROM_NAME="John Doe" # Sender name
|
|
487
|
-
|
|
488
|
-
# Show errors in the browser (development only). Set to false in production.
|
|
489
|
-
SHOW_ERRORS="true"
|
|
490
|
-
|
|
491
|
-
# Application timezone (default: UTC)
|
|
492
|
-
APP_TIMEZONE="UTC"
|
|
493
|
-
|
|
494
|
-
# Application environment (development or production)
|
|
495
|
-
APP_ENV="development"
|
|
496
|
-
|
|
497
|
-
# Enable or disable application cache (default: false)
|
|
498
|
-
CACHE_ENABLED="false"
|
|
499
|
-
# Cache time-to-live in seconds (default: 600)
|
|
500
|
-
CACHE_TTL="600"
|
|
501
|
-
|
|
502
|
-
# Local storage key for browser storage (auto-generated if not set).
|
|
503
|
-
# Spaces will be replaced with underscores and converted to lowercase.
|
|
504
|
-
LOCALSTORE_KEY="${localStoreKey}"
|
|
505
|
-
|
|
506
|
-
# Secret key for encrypting function calls.
|
|
507
|
-
FUNCTION_CALL_SECRET="${functionCallSecret}"
|
|
508
|
-
|
|
509
|
-
# Single or multiple origins (CSV or JSON array)
|
|
510
|
-
CORS_ALLOWED_ORIGINS=[]
|
|
511
|
-
|
|
512
|
-
# If you need cookies/Authorization across origins, keep this true
|
|
513
|
-
CORS_ALLOW_CREDENTIALS="true"
|
|
514
|
-
|
|
515
|
-
# Optional tuning
|
|
516
|
-
CORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
|
517
|
-
CORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"
|
|
518
|
-
CORS_EXPOSE_HEADERS=""
|
|
519
|
-
CORS_MAX_AGE="86400"`;
|
|
520
|
-
if (answer.prisma) {
|
|
521
|
-
const prismaEnvContent = `# Environment variables declared in this file are automatically made available to Prisma.
|
|
522
|
-
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
|
523
|
-
|
|
524
|
-
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
|
525
|
-
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
|
526
|
-
|
|
527
|
-
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"`;
|
|
528
|
-
const envContent = `${prismaEnvContent}\n\n${prismaPHPEnvContent}`;
|
|
529
|
-
await createOrUpdateEnvFile(baseDir, envContent);
|
|
530
|
-
} else {
|
|
531
|
-
await createOrUpdateEnvFile(baseDir, prismaPHPEnvContent);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
async function getAnswer(predefinedAnswers = {}) {
|
|
535
|
-
console.log("🚀 ~ predefinedAnswers:", predefinedAnswers);
|
|
536
|
-
// If starter kit is specified, use non-interactive mode
|
|
537
|
-
if (predefinedAnswers.starterKit) {
|
|
538
|
-
const selectedKit = predefinedAnswers.starterKit;
|
|
539
|
-
let starterKit = null;
|
|
540
|
-
// Check built-in starter kits
|
|
541
|
-
if (STARTER_KITS[selectedKit]) {
|
|
542
|
-
starterKit = STARTER_KITS[selectedKit];
|
|
543
|
-
}
|
|
544
|
-
if (starterKit) {
|
|
545
|
-
console.log(chalk.blue(`Using starter kit: ${starterKit.name}`));
|
|
546
|
-
console.log(chalk.gray(starterKit.description));
|
|
547
|
-
const answer = {
|
|
548
|
-
projectName: predefinedAnswers.projectName ?? "my-app",
|
|
549
|
-
starterKit: selectedKit,
|
|
550
|
-
starterKitSource: predefinedAnswers.starterKitSource,
|
|
551
|
-
backendOnly: starterKit.features.backendOnly ?? false,
|
|
552
|
-
tailwindcss: starterKit.features.tailwindcss ?? false,
|
|
553
|
-
websocket: starterKit.features.websocket ?? false,
|
|
554
|
-
prisma: starterKit.features.prisma ?? false,
|
|
555
|
-
docker: starterKit.features.docker ?? false,
|
|
556
|
-
swaggerDocs: starterKit.features.swaggerDocs ?? false,
|
|
557
|
-
mcp: starterKit.features.mcp ?? false,
|
|
558
|
-
};
|
|
559
|
-
// Allow CLI overrides
|
|
560
|
-
const args = process.argv.slice(2);
|
|
561
|
-
if (args.includes("--backend-only")) answer.backendOnly = true;
|
|
562
|
-
if (args.includes("--swagger-docs")) answer.swaggerDocs = true;
|
|
563
|
-
if (args.includes("--tailwindcss")) answer.tailwindcss = true;
|
|
564
|
-
if (args.includes("--websocket")) answer.websocket = true;
|
|
565
|
-
if (args.includes("--mcp")) answer.mcp = true;
|
|
566
|
-
if (args.includes("--prisma")) answer.prisma = true;
|
|
567
|
-
if (args.includes("--docker")) answer.docker = true;
|
|
568
|
-
return answer; // ✅ Return immediately - no interactive prompts
|
|
569
|
-
}
|
|
570
|
-
// Handle custom starter kit
|
|
571
|
-
else if (predefinedAnswers.starterKitSource) {
|
|
572
|
-
console.log(
|
|
573
|
-
chalk.blue(
|
|
574
|
-
`Using custom starter kit from: ${predefinedAnswers.starterKitSource}`
|
|
575
|
-
)
|
|
576
|
-
);
|
|
577
|
-
const answer = {
|
|
578
|
-
projectName: predefinedAnswers.projectName ?? "my-app",
|
|
579
|
-
starterKit: selectedKit,
|
|
580
|
-
starterKitSource: predefinedAnswers.starterKitSource,
|
|
581
|
-
// Default features - will be overridden by starter kit config
|
|
582
|
-
backendOnly: false,
|
|
583
|
-
tailwindcss: true,
|
|
584
|
-
websocket: false,
|
|
585
|
-
prisma: true,
|
|
586
|
-
docker: false,
|
|
587
|
-
swaggerDocs: true,
|
|
588
|
-
mcp: false,
|
|
589
|
-
};
|
|
590
|
-
// Allow CLI overrides
|
|
591
|
-
const args = process.argv.slice(2);
|
|
592
|
-
if (args.includes("--backend-only")) answer.backendOnly = true;
|
|
593
|
-
if (args.includes("--swagger-docs")) answer.swaggerDocs = true;
|
|
594
|
-
if (args.includes("--tailwindcss")) answer.tailwindcss = true;
|
|
595
|
-
if (args.includes("--websocket")) answer.websocket = true;
|
|
596
|
-
if (args.includes("--mcp")) answer.mcp = true;
|
|
597
|
-
if (args.includes("--prisma")) answer.prisma = true;
|
|
598
|
-
if (args.includes("--docker")) answer.docker = true;
|
|
599
|
-
return answer; // ✅ Return immediately - no interactive prompts
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const questionsArray = [];
|
|
603
|
-
// Ask for project name if not provided
|
|
604
|
-
if (!predefinedAnswers.projectName) {
|
|
605
|
-
questionsArray.push({
|
|
606
|
-
type: "text",
|
|
607
|
-
name: "projectName",
|
|
608
|
-
message: "What is your project named?",
|
|
609
|
-
initial: "my-app",
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
// IMPORTANT: skip asking backendOnly if updateAnswer.isUpdate is true
|
|
613
|
-
if (!predefinedAnswers.backendOnly && !updateAnswer?.isUpdate) {
|
|
614
|
-
questionsArray.push({
|
|
615
|
-
type: "toggle",
|
|
616
|
-
name: "backendOnly",
|
|
617
|
-
message: `Would you like to create a ${chalk.blue(
|
|
618
|
-
"backend-only project"
|
|
619
|
-
)}?`,
|
|
620
|
-
initial: false,
|
|
621
|
-
active: "Yes",
|
|
622
|
-
inactive: "No",
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
const onCancel = () => {
|
|
626
|
-
console.log(chalk.red("Operation cancelled by the user."));
|
|
627
|
-
process.exit(0);
|
|
628
|
-
};
|
|
629
|
-
const initialResponse = await prompts(questionsArray, { onCancel });
|
|
630
|
-
console.log("🚀 ~ initialResponse:", initialResponse);
|
|
631
|
-
const nonBackendOnlyQuestionsArray = [];
|
|
632
|
-
const isBackendOnly =
|
|
633
|
-
initialResponse.backendOnly ?? predefinedAnswers.backendOnly ?? false;
|
|
634
|
-
if (isBackendOnly) {
|
|
635
|
-
// For backend-only project (skip Tailwind), but still ask other features
|
|
636
|
-
if (!predefinedAnswers.swaggerDocs) {
|
|
637
|
-
nonBackendOnlyQuestionsArray.push({
|
|
638
|
-
type: "toggle",
|
|
639
|
-
name: "swaggerDocs",
|
|
640
|
-
message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
|
|
641
|
-
initial: false,
|
|
642
|
-
active: "Yes",
|
|
643
|
-
inactive: "No",
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
if (!predefinedAnswers.websocket) {
|
|
647
|
-
nonBackendOnlyQuestionsArray.push({
|
|
648
|
-
type: "toggle",
|
|
649
|
-
name: "websocket",
|
|
650
|
-
message: `Would you like to use ${chalk.blue("Websocket")}?`,
|
|
651
|
-
initial: false,
|
|
652
|
-
active: "Yes",
|
|
653
|
-
inactive: "No",
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
if (!predefinedAnswers.mcp) {
|
|
657
|
-
nonBackendOnlyQuestionsArray.push({
|
|
658
|
-
type: "toggle",
|
|
659
|
-
name: "mcp",
|
|
660
|
-
message: `Would you like to use ${chalk.blue(
|
|
661
|
-
"MCP (Model Context Protocol)"
|
|
662
|
-
)}?`,
|
|
663
|
-
initial: false,
|
|
664
|
-
active: "Yes",
|
|
665
|
-
inactive: "No",
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
if (!predefinedAnswers.prisma) {
|
|
669
|
-
nonBackendOnlyQuestionsArray.push({
|
|
670
|
-
type: "toggle",
|
|
671
|
-
name: "prisma",
|
|
672
|
-
message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
|
|
673
|
-
initial: false,
|
|
674
|
-
active: "Yes",
|
|
675
|
-
inactive: "No",
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
if (!predefinedAnswers.docker) {
|
|
679
|
-
nonBackendOnlyQuestionsArray.push({
|
|
680
|
-
type: "toggle",
|
|
681
|
-
name: "docker",
|
|
682
|
-
message: `Would you like to use ${chalk.blue("Docker")}?`,
|
|
683
|
-
initial: false,
|
|
684
|
-
active: "Yes",
|
|
685
|
-
inactive: "No",
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
} else {
|
|
689
|
-
// For full-stack project, include Tailwind
|
|
690
|
-
if (!predefinedAnswers.swaggerDocs) {
|
|
691
|
-
nonBackendOnlyQuestionsArray.push({
|
|
692
|
-
type: "toggle",
|
|
693
|
-
name: "swaggerDocs",
|
|
694
|
-
message: `Would you like to use ${chalk.blue("Swagger Docs")}?`,
|
|
695
|
-
initial: false,
|
|
696
|
-
active: "Yes",
|
|
697
|
-
inactive: "No",
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
if (!predefinedAnswers.tailwindcss) {
|
|
701
|
-
nonBackendOnlyQuestionsArray.push({
|
|
702
|
-
type: "toggle",
|
|
703
|
-
name: "tailwindcss",
|
|
704
|
-
message: `Would you like to use ${chalk.blue("Tailwind CSS")}?`,
|
|
705
|
-
initial: false,
|
|
706
|
-
active: "Yes",
|
|
707
|
-
inactive: "No",
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
if (!predefinedAnswers.websocket) {
|
|
711
|
-
nonBackendOnlyQuestionsArray.push({
|
|
712
|
-
type: "toggle",
|
|
713
|
-
name: "websocket",
|
|
714
|
-
message: `Would you like to use ${chalk.blue("Websocket")}?`,
|
|
715
|
-
initial: false,
|
|
716
|
-
active: "Yes",
|
|
717
|
-
inactive: "No",
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
if (!predefinedAnswers.mcp) {
|
|
721
|
-
nonBackendOnlyQuestionsArray.push({
|
|
722
|
-
type: "toggle",
|
|
723
|
-
name: "mcp",
|
|
724
|
-
message: `Would you like to use ${chalk.blue(
|
|
725
|
-
"MCP (Model Context Protocol)"
|
|
726
|
-
)}?`,
|
|
727
|
-
initial: false,
|
|
728
|
-
active: "Yes",
|
|
729
|
-
inactive: "No",
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
if (!predefinedAnswers.prisma) {
|
|
733
|
-
nonBackendOnlyQuestionsArray.push({
|
|
734
|
-
type: "toggle",
|
|
735
|
-
name: "prisma",
|
|
736
|
-
message: `Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,
|
|
737
|
-
initial: false,
|
|
738
|
-
active: "Yes",
|
|
739
|
-
inactive: "No",
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
if (!predefinedAnswers.docker) {
|
|
743
|
-
nonBackendOnlyQuestionsArray.push({
|
|
744
|
-
type: "toggle",
|
|
745
|
-
name: "docker",
|
|
746
|
-
message: `Would you like to use ${chalk.blue("Docker")}?`,
|
|
747
|
-
initial: false,
|
|
748
|
-
active: "Yes",
|
|
749
|
-
inactive: "No",
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
const nonBackendOnlyResponse = await prompts(nonBackendOnlyQuestionsArray, {
|
|
754
|
-
onCancel,
|
|
755
|
-
});
|
|
756
|
-
console.log("🚀 ~ nonBackendOnlyResponse:", nonBackendOnlyResponse);
|
|
757
|
-
return {
|
|
758
|
-
projectName: initialResponse.projectName
|
|
759
|
-
? String(initialResponse.projectName).trim().replace(/ /g, "-")
|
|
760
|
-
: predefinedAnswers.projectName ?? "my-app",
|
|
761
|
-
backendOnly:
|
|
762
|
-
initialResponse.backendOnly ?? predefinedAnswers.backendOnly ?? false,
|
|
763
|
-
swaggerDocs:
|
|
764
|
-
nonBackendOnlyResponse.swaggerDocs ??
|
|
765
|
-
predefinedAnswers.swaggerDocs ??
|
|
766
|
-
false,
|
|
767
|
-
tailwindcss:
|
|
768
|
-
nonBackendOnlyResponse.tailwindcss ??
|
|
769
|
-
predefinedAnswers.tailwindcss ??
|
|
770
|
-
false,
|
|
771
|
-
websocket:
|
|
772
|
-
nonBackendOnlyResponse.websocket ?? predefinedAnswers.websocket ?? false,
|
|
773
|
-
mcp: nonBackendOnlyResponse.mcp ?? predefinedAnswers.mcp ?? false,
|
|
774
|
-
prisma: nonBackendOnlyResponse.prisma ?? predefinedAnswers.prisma ?? false,
|
|
775
|
-
docker: nonBackendOnlyResponse.docker ?? predefinedAnswers.docker ?? false,
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
async function uninstallNpmDependencies(baseDir, dependencies, isDev = false) {
|
|
779
|
-
console.log("Uninstalling dependencies:");
|
|
780
|
-
dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
|
|
781
|
-
// Prepare the npm uninstall command with the appropriate flag for dev dependencies
|
|
782
|
-
const npmUninstallCommand = `npm uninstall ${
|
|
783
|
-
isDev ? "--save-dev" : "--save"
|
|
784
|
-
} ${dependencies.join(" ")}`;
|
|
785
|
-
// Execute the npm uninstall command
|
|
786
|
-
execSync(npmUninstallCommand, {
|
|
787
|
-
stdio: "inherit",
|
|
788
|
-
cwd: baseDir,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
async function uninstallComposerDependencies(baseDir, dependencies) {
|
|
792
|
-
console.log("Uninstalling Composer dependencies:");
|
|
793
|
-
dependencies.forEach((dep) => console.log(`- ${chalk.blue(dep)}`));
|
|
794
|
-
// Prepare the composer remove command
|
|
795
|
-
const composerRemoveCommand = `C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${dependencies.join(
|
|
796
|
-
" "
|
|
797
|
-
)}`;
|
|
798
|
-
// Execute the composer remove command
|
|
799
|
-
execSync(composerRemoveCommand, {
|
|
800
|
-
stdio: "inherit",
|
|
801
|
-
cwd: baseDir,
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
function fetchPackageVersion(packageName) {
|
|
805
|
-
return new Promise((resolve, reject) => {
|
|
806
|
-
https
|
|
807
|
-
.get(`https://registry.npmjs.org/${packageName}`, (res) => {
|
|
808
|
-
let data = "";
|
|
809
|
-
res.on("data", (chunk) => (data += chunk));
|
|
810
|
-
res.on("end", () => {
|
|
811
|
-
try {
|
|
812
|
-
const parsed = JSON.parse(data);
|
|
813
|
-
resolve(parsed["dist-tags"].latest);
|
|
814
|
-
} catch (error) {
|
|
815
|
-
reject(new Error("Failed to parse JSON response"));
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
})
|
|
819
|
-
.on("error", (err) => reject(err));
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
const readJsonFile = (filePath) => {
|
|
823
|
-
const jsonData = fs.readFileSync(filePath, "utf8");
|
|
824
|
-
return JSON.parse(jsonData);
|
|
825
|
-
};
|
|
826
|
-
function compareVersions(installedVersion, currentVersion) {
|
|
827
|
-
const installedVersionArray = installedVersion.split(".").map(Number);
|
|
828
|
-
const currentVersionArray = currentVersion.split(".").map(Number);
|
|
829
|
-
for (let i = 0; i < installedVersionArray.length; i++) {
|
|
830
|
-
if (installedVersionArray[i] > currentVersionArray[i]) {
|
|
831
|
-
return 1;
|
|
832
|
-
} else if (installedVersionArray[i] < currentVersionArray[i]) {
|
|
833
|
-
return -1;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
return 0;
|
|
837
|
-
}
|
|
838
|
-
function getInstalledPackageVersion(packageName) {
|
|
839
|
-
try {
|
|
840
|
-
const output = execSync(`npm list -g ${packageName} --depth=0`).toString();
|
|
841
|
-
const versionMatch = output.match(
|
|
842
|
-
new RegExp(`${packageName}@(\\d+\\.\\d+\\.\\d+)`)
|
|
843
|
-
);
|
|
844
|
-
if (versionMatch) {
|
|
845
|
-
return versionMatch[1];
|
|
846
|
-
} else {
|
|
847
|
-
console.error(`Package ${packageName} is not installed`);
|
|
848
|
-
return null;
|
|
849
|
-
}
|
|
850
|
-
} catch (error) {
|
|
851
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
2
|
+
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],dockerFiles=[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!1,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","src/app/layout.php","src/app/index.php"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/app/layout.php","src/app/index.php","src/app/js/index.js","src/app/css/tailwind.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","docker-compose.yml","Dockerfile"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,websocket:!0,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!0},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/lib/websocket","src/lib/mcp"]},ecommerce:{id:"ecommerce",name:"E-commerce Starter",description:"Full e-commerce application with cart, payments, and admin",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-ecommerce-starter",branch:"main"}},blog:{id:"blog",name:"Blog CMS",description:"Blog content management system",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-blog-starter"}}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),c=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let n=`http://localhost/${c}`;n=n.endsWith("/")?n.slice(0,-1):n;const i=n.replace(/(?<!:)(\/\/+)/g,"/"),o=c.replace(/\/\/+/g,"/");return{bsTarget:`${i}/`,bsPathRewrite:{"^/":`/${o.startsWith("/")?o.substring(1):o}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const c=JSON.parse(fs.readFileSync(t,"utf8"));c.scripts={...c.scripts,projectName:"tsx settings/project-name.ts"};let n=[];if(s.tailwindcss&&(c.scripts={...c.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch","tailwind:build":"postcss src/app/css/tailwind.css -o src/app/css/styles.css"},n.push("tailwind")),s.websocket&&(c.scripts={...c.scripts,websocket:"tsx settings/restart-websocket.ts"},n.push("websocket")),s.mcp&&(c.scripts={...c.scripts,mcp:"tsx settings/restart-mcp.ts"},n.push("mcp")),s.docker&&(c.scripts={...c.scripts,docker:"docker-compose up"},n.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";c.scripts={...c.scripts,"create-swagger-docs":e}}let i={...c.scripts};i.browserSync="tsx settings/bs-config.ts",i["browserSync:build"]="tsx settings/build.ts",i.dev=`npm-run-all projectName -p browserSync ${n.join(" ")}`,i.build=`npm-run-all${s.tailwindcss?" tailwind:build":""} browserSync:build`,c.scripts=i,c.type="module",fs.writeFileSync(t,JSON.stringify(c,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let c=fs.readFileSync(t,"utf8");c+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,c,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const c=fs.existsSync(e),n=c&&fs.statSync(e);if(c&&n&&n.isDirectory()){const c=s.toLowerCase();if(!t.websocket&&c.includes("src\\lib\\websocket"))return;if(!t.mcp&&c.includes("src\\lib\\mcp"))return;if(t.backendOnly&&c.includes("src\\app\\js")||t.backendOnly&&c.includes("src\\app\\css")||t.backendOnly&&c.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&c.includes("src\\app\\swagger-docs"))return;const n=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(n))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((c=>{copyRecursiveSync(path.join(e,c),path.join(s,c),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&s.includes("restart-websocket.ts"))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(!t.docker&&dockerFiles.some((e=>s.includes(e))))return;if(t.backendOnly&&nonBackendFiles.some((e=>s.includes(e))))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({src:s,dest:c})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,c),t)}))}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),c="";s.backendOnly||(s.tailwindcss||(c='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),c+='\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let n="";s.backendOnly||(n=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${c}`:c),e=e.replace("</head>",`${n}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){return!!updateAnswer?.isUpdate&&(updateAnswer?.excludeFilePath?.includes(e.replace(/\\/g,"/"))??!1)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const c=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&c.push({src:"/.dockerignore",dest:"/.dockerignore"},{src:"/docker-compose.yml",dest:"/docker-compose.yml"},{src:"/Dockerfile",dest:"/Dockerfile"},{src:"/apache.conf",dest:"/apache.conf"}),t.forEach((({src:s,dest:t})=>{const c=path.join(__dirname,s),n=path.join(e,t);if(checkExcludeFiles(n))return;const i=fs.readFileSync(c,"utf8");fs.writeFileSync(n,i,{flag:"w"})})),await executeCopy(e,c,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const n=generateAuthSecret(),i=generateHexEncodedKey(),o=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${n}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# PHPMailer SMTP configuration (uncomment and set as needed)\n# SMTP_HOST="smtp.gmail.com" # Your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username\n# SMTP_PASSWORD="123456" # Your SMTP password\n# SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port\n# SMTP_ENCRYPTION="ssl" # ssl or tls\n# MAIL_FROM="john.doe@gmail.com" # Sender email address\n# MAIL_FROM_NAME="John Doe" # Sender name\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Local storage key for browser storage (auto-generated if not set).\n# Spaces will be replaced with underscores and converted to lowercase.\nLOCALSTORE_KEY="${i}"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins (CSV or JSON array)\nCORS_ALLOWED_ORIGINS=[]\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${o}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,o)}async function getAnswer(e={}){if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const c={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,websocket:t.features.websocket??!1,prisma:t.features.prisma??!1,docker:t.features.docker??!1,swaggerDocs:t.features.swaggerDocs??!1,mcp:t.features.mcp??!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(c.backendOnly=!0),n.includes("--swagger-docs")&&(c.swaggerDocs=!0),n.includes("--tailwindcss")&&(c.tailwindcss=!0),n.includes("--websocket")&&(c.websocket=!0),n.includes("--mcp")&&(c.mcp=!0),n.includes("--prisma")&&(c.prisma=!0),n.includes("--docker")&&(c.docker=!0),c}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},c=process.argv.slice(2);return c.includes("--backend-only")&&(t.backendOnly=!0),c.includes("--swagger-docs")&&(t.swaggerDocs=!0),c.includes("--tailwindcss")&&(t.tailwindcss=!0),c.includes("--websocket")&&(t.websocket=!0),c.includes("--mcp")&&(t.mcp=!0),c.includes("--prisma")&&(t.prisma=!0),c.includes("--docker")&&(t.docker=!0),t}}const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||s.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{process.exit(0)},c=await prompts(s,{onCancel:t}),n=[];c.backendOnly??e.backendOnly??!1?(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||n.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(n,{onCancel:t});return{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:c.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,mcp:i.mcp??e.mcp??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallNpmDependencies(e,s,t=!1){s.forEach((e=>{}));const c=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(c,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){s.forEach((e=>{}));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let c="";e.on("data",(e=>c+=e)),e.on("end",(()=>{try{const e=JSON.parse(c);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),c=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>c[e])return 1;if(t[e]<c[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:null}catch(e){return null}}
|
|
855
3
|
/**
|
|
856
4
|
* Install dependencies in the specified directory.
|
|
857
5
|
* @param {string} baseDir - The base directory where to install the dependencies.
|
|
@@ -1023,116 +171,6 @@ function composerPkg(name) {
|
|
|
1023
171
|
? `${name}:${composerPinnedVersions[name]}`
|
|
1024
172
|
: name;
|
|
1025
173
|
}
|
|
1026
|
-
async function downloadStarterKit(starterKit, tempDir) {
|
|
1027
|
-
if (!starterKit.source) {
|
|
1028
|
-
throw new Error("No source defined for starter kit");
|
|
1029
|
-
}
|
|
1030
|
-
const { type, url, branch = "main", subfolder } = starterKit.source;
|
|
1031
|
-
switch (type) {
|
|
1032
|
-
case "git":
|
|
1033
|
-
console.log(chalk.blue(`Cloning ${starterKit.name} from ${url}...`));
|
|
1034
|
-
const cloneCommand = branch
|
|
1035
|
-
? `git clone -b ${branch} --depth 1 ${url} ${tempDir}`
|
|
1036
|
-
: `git clone --depth 1 ${url} ${tempDir}`;
|
|
1037
|
-
execSync(cloneCommand, { stdio: "inherit" });
|
|
1038
|
-
// Remove .git directory
|
|
1039
|
-
const gitDir = path.join(tempDir, ".git");
|
|
1040
|
-
if (fs.existsSync(gitDir)) {
|
|
1041
|
-
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
1042
|
-
}
|
|
1043
|
-
// Return the subfolder if specified
|
|
1044
|
-
return subfolder ? path.join(tempDir, subfolder) : tempDir;
|
|
1045
|
-
case "npm":
|
|
1046
|
-
console.log(chalk.blue(`Downloading ${starterKit.name} from npm...`));
|
|
1047
|
-
execSync(`npm pack ${url}`, { cwd: tempDir, stdio: "inherit" });
|
|
1048
|
-
// Extract the tarball
|
|
1049
|
-
const tarball = fs.readdirSync(tempDir).find((f) => f.endsWith(".tgz"));
|
|
1050
|
-
if (tarball) {
|
|
1051
|
-
execSync(`tar -xzf ${tarball}`, { cwd: tempDir });
|
|
1052
|
-
fs.unlinkSync(path.join(tempDir, tarball));
|
|
1053
|
-
return path.join(tempDir, "package");
|
|
1054
|
-
}
|
|
1055
|
-
throw new Error("Failed to extract npm package");
|
|
1056
|
-
case "url":
|
|
1057
|
-
throw new Error("URL download not implemented yet");
|
|
1058
|
-
default:
|
|
1059
|
-
throw new Error(`Unsupported source type: ${type}`);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
async function mergeStarterKitFiles(starterKitPath, projectPath, answer) {
|
|
1063
|
-
console.log(chalk.blue("Merging starter kit files..."));
|
|
1064
|
-
// First, check if starter kit has a prisma-php.json to preserve excludeFiles
|
|
1065
|
-
const starterKitConfigPath = path.join(starterKitPath, "prisma-php.json");
|
|
1066
|
-
let starterKitExcludeFiles = [];
|
|
1067
|
-
if (fs.existsSync(starterKitConfigPath)) {
|
|
1068
|
-
try {
|
|
1069
|
-
const starterKitConfig = JSON.parse(
|
|
1070
|
-
fs.readFileSync(starterKitConfigPath, "utf8")
|
|
1071
|
-
);
|
|
1072
|
-
starterKitExcludeFiles = starterKitConfig.excludeFiles || [];
|
|
1073
|
-
console.log(
|
|
1074
|
-
chalk.blue(
|
|
1075
|
-
`Found ${starterKitExcludeFiles.length} excluded files in starter kit`
|
|
1076
|
-
)
|
|
1077
|
-
);
|
|
1078
|
-
} catch (error) {
|
|
1079
|
-
console.warn(chalk.yellow("Failed to parse starter kit prisma-php.json"));
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
// Temporarily store excludeFiles for the copy process
|
|
1083
|
-
const tempUpdateAnswer = updateAnswer;
|
|
1084
|
-
updateAnswer = {
|
|
1085
|
-
...answer,
|
|
1086
|
-
isUpdate: false, // Treat as new project for copying
|
|
1087
|
-
excludeFiles: starterKitExcludeFiles,
|
|
1088
|
-
excludeFilePath: starterKitExcludeFiles.map((file) =>
|
|
1089
|
-
path.join(projectPath, file).replace(/\\/g, "/")
|
|
1090
|
-
),
|
|
1091
|
-
};
|
|
1092
|
-
// Copy all files from starter kit
|
|
1093
|
-
copyRecursiveSync(starterKitPath, projectPath, answer);
|
|
1094
|
-
// Restore original updateAnswer
|
|
1095
|
-
updateAnswer = tempUpdateAnswer;
|
|
1096
|
-
// Handle starter kit specific configuration
|
|
1097
|
-
const starterKitConfig = path.join(starterKitPath, "starter-kit.json");
|
|
1098
|
-
if (fs.existsSync(starterKitConfig)) {
|
|
1099
|
-
const config = JSON.parse(fs.readFileSync(starterKitConfig, "utf8"));
|
|
1100
|
-
// Handle post-install scripts
|
|
1101
|
-
if (config.postInstall) {
|
|
1102
|
-
console.log(chalk.blue("Running post-install scripts..."));
|
|
1103
|
-
for (const script of config.postInstall) {
|
|
1104
|
-
console.log(chalk.gray(`Running: ${script}`));
|
|
1105
|
-
execSync(script, { cwd: projectPath, stdio: "inherit" });
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
// Handle additional dependencies
|
|
1109
|
-
if (config.additionalNpmDependencies) {
|
|
1110
|
-
await installNpmDependencies(
|
|
1111
|
-
projectPath,
|
|
1112
|
-
config.additionalNpmDependencies.map(npmPkg),
|
|
1113
|
-
true
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
if (config.additionalComposerDependencies) {
|
|
1117
|
-
await installComposerDependencies(
|
|
1118
|
-
projectPath,
|
|
1119
|
-
config.additionalComposerDependencies.map(composerPkg)
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
// Store the excludeFiles for later use in main()
|
|
1124
|
-
if (starterKitExcludeFiles.length > 0) {
|
|
1125
|
-
// Create a temporary marker file to pass excludeFiles to main()
|
|
1126
|
-
const tempConfigPath = path.join(
|
|
1127
|
-
projectPath,
|
|
1128
|
-
".temp-starter-kit-config.json"
|
|
1129
|
-
);
|
|
1130
|
-
fs.writeFileSync(
|
|
1131
|
-
tempConfigPath,
|
|
1132
|
-
JSON.stringify({ excludeFiles: starterKitExcludeFiles })
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
174
|
async function setupStarterKit(baseDir, answer) {
|
|
1137
175
|
if (!answer.starterKit) return;
|
|
1138
176
|
let starterKit = null;
|
|
@@ -1161,35 +199,53 @@ async function setupStarterKit(baseDir, answer) {
|
|
|
1161
199
|
return;
|
|
1162
200
|
}
|
|
1163
201
|
console.log(chalk.green(`Setting up ${starterKit.name}...`));
|
|
1164
|
-
// If it's a custom starter kit with source,
|
|
202
|
+
// If it's a custom starter kit with source, clone it directly to the target directory
|
|
1165
203
|
if (starterKit.source) {
|
|
1166
|
-
const tempDir = path.join(baseDir, ".temp-starter-kit");
|
|
1167
204
|
try {
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
|
|
205
|
+
// Clone directly to the target directory
|
|
206
|
+
const cloneCommand = starterKit.source.branch
|
|
207
|
+
? `git clone -b ${starterKit.source.branch} --depth 1 ${starterKit.source.url} ${baseDir}`
|
|
208
|
+
: `git clone --depth 1 ${starterKit.source.url} ${baseDir}`;
|
|
209
|
+
execSync(cloneCommand, { stdio: "inherit" });
|
|
210
|
+
// Remove .git directory
|
|
211
|
+
const gitDir = path.join(baseDir, ".git");
|
|
212
|
+
if (fs.existsSync(gitDir)) {
|
|
213
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
1171
214
|
}
|
|
1172
|
-
|
|
1173
|
-
//
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
215
|
+
console.log(chalk.blue("Starter kit cloned successfully!"));
|
|
216
|
+
// Update the project name in the existing prisma-php.json
|
|
217
|
+
const configPath = path.join(baseDir, "prisma-php.json");
|
|
218
|
+
if (fs.existsSync(configPath)) {
|
|
219
|
+
try {
|
|
220
|
+
const existingConfig = JSON.parse(
|
|
221
|
+
fs.readFileSync(configPath, "utf8")
|
|
222
|
+
);
|
|
223
|
+
// Only update project-specific fields, preserve everything else
|
|
224
|
+
const projectPathModified = baseDir.replace(/\\/g, "\\");
|
|
225
|
+
const bsConfig = bsConfigUrls(projectPathModified);
|
|
226
|
+
existingConfig.projectName = answer.projectName;
|
|
227
|
+
existingConfig.projectRootPath = projectPathModified;
|
|
228
|
+
existingConfig.bsTarget = bsConfig.bsTarget;
|
|
229
|
+
existingConfig.bsPathRewrite = bsConfig.bsPathRewrite;
|
|
230
|
+
// Update version to latest
|
|
231
|
+
const latestVersion = await fetchPackageVersion(
|
|
232
|
+
"create-prisma-php-app"
|
|
233
|
+
);
|
|
234
|
+
existingConfig.version = latestVersion;
|
|
235
|
+
fs.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2));
|
|
236
|
+
console.log(
|
|
237
|
+
chalk.green("Updated prisma-php.json with new project details")
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.warn(
|
|
241
|
+
chalk.yellow(
|
|
242
|
+
"Failed to update prisma-php.json, will create new one"
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
1184
246
|
}
|
|
1185
|
-
// Clean up temp directory
|
|
1186
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1187
247
|
} catch (error) {
|
|
1188
248
|
console.error(chalk.red(`Failed to setup starter kit: ${error}`));
|
|
1189
|
-
// Clean up temp directory on error
|
|
1190
|
-
if (fs.existsSync(tempDir)) {
|
|
1191
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1192
|
-
}
|
|
1193
249
|
throw error;
|
|
1194
250
|
}
|
|
1195
251
|
}
|
|
@@ -1243,10 +299,27 @@ async function main() {
|
|
|
1243
299
|
return;
|
|
1244
300
|
}
|
|
1245
301
|
let answer = null;
|
|
302
|
+
let isStarterKitProject = false;
|
|
1246
303
|
if (projectName) {
|
|
1247
304
|
const currentDir = process.cwd();
|
|
1248
305
|
const configPath = path.join(currentDir, "prisma-php.json");
|
|
1249
|
-
if
|
|
306
|
+
// Check if it's a starter kit project
|
|
307
|
+
if (starterKitFromArgs && starterKitSource) {
|
|
308
|
+
isStarterKitProject = true;
|
|
309
|
+
const predefinedAnswers = {
|
|
310
|
+
projectName,
|
|
311
|
+
starterKit: starterKitFromArgs,
|
|
312
|
+
starterKitSource: starterKitSource,
|
|
313
|
+
backendOnly: args.includes("--backend-only"),
|
|
314
|
+
swaggerDocs: args.includes("--swagger-docs"),
|
|
315
|
+
tailwindcss: args.includes("--tailwindcss"),
|
|
316
|
+
websocket: args.includes("--websocket"),
|
|
317
|
+
mcp: args.includes("--mcp"),
|
|
318
|
+
prisma: args.includes("--prisma"),
|
|
319
|
+
docker: args.includes("--docker"),
|
|
320
|
+
};
|
|
321
|
+
answer = await getAnswer(predefinedAnswers);
|
|
322
|
+
} else if (fs.existsSync(configPath)) {
|
|
1250
323
|
// It's an update - read existing settings
|
|
1251
324
|
const localSettings = readJsonFile(configPath);
|
|
1252
325
|
let excludeFiles = [];
|
|
@@ -1357,33 +430,86 @@ async function main() {
|
|
|
1357
430
|
const currentDir = process.cwd();
|
|
1358
431
|
let projectPath;
|
|
1359
432
|
if (projectName) {
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const projectNameConfigPath = path.join(
|
|
1364
|
-
projectNamePath,
|
|
1365
|
-
"prisma-php.json"
|
|
1366
|
-
);
|
|
1367
|
-
if (fs.existsSync(configPath)) {
|
|
1368
|
-
// We're updating an existing project in current directory
|
|
1369
|
-
projectPath = currentDir;
|
|
1370
|
-
} else if (
|
|
1371
|
-
fs.existsSync(projectNamePath) &&
|
|
1372
|
-
fs.existsSync(projectNameConfigPath)
|
|
1373
|
-
) {
|
|
1374
|
-
// We're updating an existing project in the named directory
|
|
1375
|
-
projectPath = projectNamePath;
|
|
1376
|
-
process.chdir(projectNamePath);
|
|
1377
|
-
} else {
|
|
1378
|
-
// We're creating a new project with the given name
|
|
433
|
+
if (isStarterKitProject) {
|
|
434
|
+
// For starter kit projects, create directory first
|
|
435
|
+
const projectNamePath = path.join(currentDir, projectName);
|
|
1379
436
|
if (!fs.existsSync(projectNamePath)) {
|
|
1380
437
|
fs.mkdirSync(projectNamePath, { recursive: true });
|
|
1381
438
|
}
|
|
1382
439
|
projectPath = projectNamePath;
|
|
1383
|
-
|
|
440
|
+
// Clone the starter kit first
|
|
441
|
+
await setupStarterKit(projectPath, answer);
|
|
442
|
+
// Change to project directory
|
|
443
|
+
process.chdir(projectPath);
|
|
444
|
+
// Now check if it has prisma-php.json and treat as update
|
|
445
|
+
const configPath = path.join(projectPath, "prisma-php.json");
|
|
446
|
+
if (fs.existsSync(configPath)) {
|
|
447
|
+
// Read the existing config and merge with CLI overrides
|
|
448
|
+
const existingConfig = JSON.parse(
|
|
449
|
+
fs.readFileSync(configPath, "utf8")
|
|
450
|
+
);
|
|
451
|
+
// Override with CLI arguments if provided
|
|
452
|
+
if (args.includes("--backend-only"))
|
|
453
|
+
existingConfig.backendOnly = true;
|
|
454
|
+
if (args.includes("--swagger-docs"))
|
|
455
|
+
existingConfig.swaggerDocs = true;
|
|
456
|
+
if (args.includes("--tailwindcss")) existingConfig.tailwindcss = true;
|
|
457
|
+
if (args.includes("--websocket")) existingConfig.websocket = true;
|
|
458
|
+
if (args.includes("--mcp")) existingConfig.mcp = true;
|
|
459
|
+
if (args.includes("--prisma")) existingConfig.prisma = true;
|
|
460
|
+
if (args.includes("--docker")) existingConfig.docker = true;
|
|
461
|
+
// Update answer with existing config
|
|
462
|
+
answer = {
|
|
463
|
+
...answer,
|
|
464
|
+
backendOnly: existingConfig.backendOnly,
|
|
465
|
+
swaggerDocs: existingConfig.swaggerDocs,
|
|
466
|
+
tailwindcss: existingConfig.tailwindcss,
|
|
467
|
+
websocket: existingConfig.websocket,
|
|
468
|
+
mcp: existingConfig.mcp,
|
|
469
|
+
prisma: existingConfig.prisma,
|
|
470
|
+
docker: existingConfig.docker,
|
|
471
|
+
};
|
|
472
|
+
// Set up as an update
|
|
473
|
+
let excludeFiles = [];
|
|
474
|
+
existingConfig.excludeFiles?.map((file) => {
|
|
475
|
+
const filePath = path.join(projectPath, file);
|
|
476
|
+
if (fs.existsSync(filePath))
|
|
477
|
+
excludeFiles.push(filePath.replace(/\\/g, "/"));
|
|
478
|
+
});
|
|
479
|
+
updateAnswer = {
|
|
480
|
+
...answer,
|
|
481
|
+
isUpdate: true,
|
|
482
|
+
excludeFiles: existingConfig.excludeFiles ?? [],
|
|
483
|
+
excludeFilePath: excludeFiles ?? [],
|
|
484
|
+
filePath: projectPath,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
// Regular project handling (existing logic)
|
|
489
|
+
const configPath = path.join(currentDir, "prisma-php.json");
|
|
490
|
+
const projectNamePath = path.join(currentDir, projectName);
|
|
491
|
+
const projectNameConfigPath = path.join(
|
|
492
|
+
projectNamePath,
|
|
493
|
+
"prisma-php.json"
|
|
494
|
+
);
|
|
495
|
+
if (fs.existsSync(configPath)) {
|
|
496
|
+
projectPath = currentDir;
|
|
497
|
+
} else if (
|
|
498
|
+
fs.existsSync(projectNamePath) &&
|
|
499
|
+
fs.existsSync(projectNameConfigPath)
|
|
500
|
+
) {
|
|
501
|
+
projectPath = projectNamePath;
|
|
502
|
+
process.chdir(projectNamePath);
|
|
503
|
+
} else {
|
|
504
|
+
if (!fs.existsSync(projectNamePath)) {
|
|
505
|
+
fs.mkdirSync(projectNamePath, { recursive: true });
|
|
506
|
+
}
|
|
507
|
+
projectPath = projectNamePath;
|
|
508
|
+
process.chdir(projectNamePath);
|
|
509
|
+
}
|
|
1384
510
|
}
|
|
1385
511
|
} else {
|
|
1386
|
-
// Interactive mode
|
|
512
|
+
// Interactive mode
|
|
1387
513
|
fs.mkdirSync(answer.projectName, { recursive: true });
|
|
1388
514
|
projectPath = path.join(currentDir, answer.projectName);
|
|
1389
515
|
process.chdir(answer.projectName);
|
|
@@ -1436,8 +562,8 @@ async function main() {
|
|
|
1436
562
|
if (answer.prisma) {
|
|
1437
563
|
execSync("npm install -g prisma-client-php", { stdio: "inherit" });
|
|
1438
564
|
}
|
|
1439
|
-
//
|
|
1440
|
-
if (answer.starterKit) {
|
|
565
|
+
// Only setup starter kit if it's not already done
|
|
566
|
+
if (answer.starterKit && !isStarterKitProject) {
|
|
1441
567
|
await setupStarterKit(projectPath, answer);
|
|
1442
568
|
}
|
|
1443
569
|
await installNpmDependencies(projectPath, npmDependencies, true);
|
|
@@ -1673,48 +799,37 @@ async function main() {
|
|
|
1673
799
|
await uninstallComposerDependencies(projectPath, composerToUninstall);
|
|
1674
800
|
}
|
|
1675
801
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
802
|
+
// Skip creating prismaPhpConfig if it's a starter kit project that already has one
|
|
803
|
+
if (
|
|
804
|
+
!isStarterKitProject ||
|
|
805
|
+
!fs.existsSync(path.join(projectPath, "prisma-php.json"))
|
|
806
|
+
) {
|
|
807
|
+
// Create prisma-php.json with all the existing logic
|
|
808
|
+
const projectPathModified = projectPath.replace(/\\/g, "\\");
|
|
809
|
+
const bsConfig = bsConfigUrls(projectPathModified);
|
|
810
|
+
const prismaPhpConfig = {
|
|
811
|
+
projectName: answer.projectName,
|
|
812
|
+
projectRootPath: projectPathModified,
|
|
813
|
+
phpEnvironment: "XAMPP",
|
|
814
|
+
phpRootPathExe: "C:\\xampp\\php\\php.exe",
|
|
815
|
+
bsTarget: bsConfig.bsTarget,
|
|
816
|
+
bsPathRewrite: bsConfig.bsPathRewrite,
|
|
817
|
+
backendOnly: answer.backendOnly,
|
|
818
|
+
swaggerDocs: answer.swaggerDocs,
|
|
819
|
+
tailwindcss: answer.tailwindcss,
|
|
820
|
+
websocket: answer.websocket,
|
|
821
|
+
mcp: answer.mcp,
|
|
822
|
+
prisma: answer.prisma,
|
|
823
|
+
docker: answer.docker,
|
|
824
|
+
version: latestVersionOfCreatePrismaPhpApp,
|
|
825
|
+
excludeFiles: updateAnswer?.excludeFiles ?? [],
|
|
826
|
+
};
|
|
827
|
+
fs.writeFileSync(
|
|
828
|
+
path.join(projectPath, "prisma-php.json"),
|
|
829
|
+
JSON.stringify(prismaPhpConfig, null, 2),
|
|
830
|
+
{ flag: "w" }
|
|
831
|
+
);
|
|
1691
832
|
}
|
|
1692
|
-
// Then update the prismaPhpConfig creation:
|
|
1693
|
-
const prismaPhpConfig = {
|
|
1694
|
-
projectName: answer.projectName,
|
|
1695
|
-
projectRootPath: projectPathModified,
|
|
1696
|
-
phpEnvironment: "XAMPP",
|
|
1697
|
-
phpRootPathExe: "C:\\xampp\\php\\php.exe",
|
|
1698
|
-
bsTarget: bsConfig.bsTarget,
|
|
1699
|
-
bsPathRewrite: bsConfig.bsPathRewrite,
|
|
1700
|
-
backendOnly: answer.backendOnly,
|
|
1701
|
-
swaggerDocs: answer.swaggerDocs,
|
|
1702
|
-
tailwindcss: answer.tailwindcss,
|
|
1703
|
-
websocket: answer.websocket,
|
|
1704
|
-
mcp: answer.mcp,
|
|
1705
|
-
prisma: answer.prisma,
|
|
1706
|
-
docker: answer.docker,
|
|
1707
|
-
version: latestVersionOfCreatePrismaPhpApp,
|
|
1708
|
-
excludeFiles:
|
|
1709
|
-
starterKitExcludeFiles.length > 0
|
|
1710
|
-
? starterKitExcludeFiles
|
|
1711
|
-
: updateAnswer?.excludeFiles ?? [],
|
|
1712
|
-
};
|
|
1713
|
-
fs.writeFileSync(
|
|
1714
|
-
path.join(projectPath, "prisma-php.json"),
|
|
1715
|
-
JSON.stringify(prismaPhpConfig, null, 2),
|
|
1716
|
-
{ flag: "w" }
|
|
1717
|
-
);
|
|
1718
833
|
if (updateAnswer?.isUpdate) {
|
|
1719
834
|
execSync(
|
|
1720
835
|
"C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar update",
|