lapeh 2.4.12 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/bin/index.js +669 -942
  2. package/dist/generated/prisma/internal/prismaNamespace.d.ts +21 -16
  3. package/dist/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
  4. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +3 -3
  5. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
  6. package/dist/lib/bootstrap.d.ts.map +1 -1
  7. package/dist/lib/bootstrap.js +34 -11
  8. package/dist/lib/core/database.d.ts.map +1 -1
  9. package/dist/lib/core/database.js +7 -1
  10. package/dist/lib/utils/response.d.ts +1 -1
  11. package/dist/lib/utils/response.d.ts.map +1 -1
  12. package/dist/src/config/app.d.ts +10 -0
  13. package/dist/src/config/app.d.ts.map +1 -0
  14. package/dist/src/config/app.js +12 -0
  15. package/dist/src/config/cors.d.ts +6 -0
  16. package/dist/src/config/cors.d.ts.map +1 -0
  17. package/dist/src/config/cors.js +8 -0
  18. package/dist/src/modules/Auth/auth.controller.d.ts +11 -0
  19. package/dist/src/modules/Auth/auth.controller.d.ts.map +1 -0
  20. package/dist/src/modules/Auth/auth.controller.js +414 -0
  21. package/dist/src/modules/Pets/pets.controller.d.ts +7 -0
  22. package/dist/src/modules/Pets/pets.controller.d.ts.map +1 -0
  23. package/dist/src/modules/Pets/pets.controller.js +163 -0
  24. package/dist/src/modules/Rbac/rbac.controller.d.ts +16 -0
  25. package/dist/src/modules/Rbac/rbac.controller.d.ts.map +1 -0
  26. package/dist/src/modules/Rbac/rbac.controller.js +437 -0
  27. package/dist/src/routes/auth.js +9 -9
  28. package/dist/src/routes/pets.js +1 -1
  29. package/dist/src/routes/rbac.js +15 -15
  30. package/lib/bootstrap.ts +34 -13
  31. package/lib/core/database.ts +7 -1
  32. package/lib/utils/response.ts +4 -4
  33. package/package.json +4 -5
  34. package/prisma/base.prisma.template +0 -1
  35. package/prisma/schema.prisma +0 -1
  36. package/prisma.config.ts +14 -0
  37. package/scripts/compile-schema.js +28 -10
  38. package/scripts/make-module.js +100 -158
  39. package/src/config/app.ts +9 -0
  40. package/src/config/cors.ts +5 -0
  41. package/src/routes/auth.ts +1 -1
  42. package/src/routes/pets.ts +1 -1
  43. package/src/routes/rbac.ts +1 -1
  44. package/storage/logs/.0337f5062fe676994d1dc340156e089444e3d6e0-audit.json +5 -0
  45. package/storage/logs/lapeh-2025-12-29.log +94 -0
  46. package/scripts/make-controller.js +0 -205
  47. package/scripts/make-model.js +0 -42
  48. /package/src/{controllers/authController.ts → modules/Auth/auth.controller.ts} +0 -0
  49. /package/src/{models/core.prisma → modules/Auth/auth.prisma} +0 -0
  50. /package/src/{controllers/petController.ts → modules/Pets/pets.controller.ts} +0 -0
  51. /package/src/{models → modules/Pets}/pets.prisma +0 -0
  52. /package/src/{controllers/rbacController.ts → modules/Rbac/rbac.controller.ts} +0 -0
package/bin/index.js CHANGED
@@ -1,943 +1,670 @@
1
1
  #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync } = require('child_process');
6
- const readline = require('readline');
7
-
8
- const args = process.argv.slice(2);
9
- const command = args[0];
10
-
11
- // Telemetry Logic
12
- async function sendTelemetry(cmd, errorInfo = null) {
13
- try {
14
- const os = require('os');
15
-
16
- const payload = {
17
- command: cmd,
18
- nodeVersion: process.version,
19
- osPlatform: os.platform(),
20
- osRelease: os.release(),
21
- timestamp: new Date().toISOString()
22
- };
23
-
24
- if (errorInfo) {
25
- payload.error = errorInfo.message;
26
- payload.stack = errorInfo.stack;
27
- }
28
-
29
- const data = JSON.stringify(payload);
30
-
31
- // Parse URL from env or use default
32
- const apiUrl = process.env.LAPEH_API_URL || 'https://lapeh-doc.vercel.app/api/telemetry';
33
- const url = new URL(apiUrl);
34
- const isHttps = url.protocol === 'https:';
35
- const client = isHttps ? require('https') : require('http');
36
-
37
- const options = {
38
- hostname: url.hostname,
39
- port: url.port || (isHttps ? 443 : 80),
40
- path: url.pathname,
41
- method: 'POST',
42
- headers: {
43
- 'Content-Type': 'application/json',
44
- 'Content-Length': Buffer.byteLength(data)
45
- },
46
- timeout: 2000 // Slightly longer for crash reports
47
- };
48
-
49
- const req = client.request(options, (res) => {
50
- res.resume();
51
- });
52
-
53
- req.on('error', (e) => {
54
- // Silent fail
55
- });
56
-
57
- req.write(data);
58
- req.end();
59
- } catch (e) {
60
- // Silent fail
61
- }
62
- }
63
-
64
- // Global Error Handler for Crash Reporting
65
- process.on('uncaughtException', async (err) => {
66
- console.error('❌ Unexpected Error:', err);
67
- console.log('📝 Sending crash report...');
68
- try {
69
- // Send crash report synchronously-ish (we can't truly await in uncaughtException easily if we want to exit fast,
70
- // but we should try to keep process alive just long enough)
71
- sendTelemetry(command || 'unknown', err);
72
-
73
- // Give it a moment to send
74
- setTimeout(() => {
75
- process.exit(1);
76
- }, 1000);
77
- } catch (e) {
78
- process.exit(1);
79
- }
80
- });
81
-
82
- // Send telemetry for every command (only if not crashing immediately)
83
- sendTelemetry(command || 'init');
84
-
85
- switch (command) {
86
- case 'dev':
87
- runDev();
88
- break;
89
- case 'start':
90
- runStart();
91
- break;
92
- case 'build':
93
- runBuild();
94
- break;
95
- case 'upgrade':
96
- (async () => {
97
- await upgradeProject();
98
- })();
99
- break;
100
- default:
101
- createProject();
102
- break;
103
- }
104
-
105
- function runDev() {
106
- console.log('🚀 Starting Lapeh in development mode...');
107
- try {
108
- // Generate Prisma Client before starting
109
- console.log('🔄 Generating Prisma Client...');
110
- const compileSchemaPath = path.join(process.cwd(), 'scripts/compile-schema.js');
111
- if (fs.existsSync(compileSchemaPath)) {
112
- try {
113
- execSync('node scripts/compile-schema.js', { stdio: 'inherit' });
114
- } catch (e) {
115
- console.warn('⚠️ Failed to run compile-schema.js', e.message);
116
- }
117
- }
118
-
119
- try {
120
- execSync('npx prisma generate', { stdio: 'inherit' });
121
- } catch (e) {
122
- console.warn('⚠️ Failed to run prisma generate. Continuing...', e.message);
123
- }
124
-
125
- const tsNodePath = require.resolve('ts-node/register');
126
- const tsConfigPathsPath = require.resolve('tsconfig-paths/register');
127
-
128
- // Resolve bootstrap file
129
- // 1. Try to find it in the current project's node_modules (preferred)
130
- const localBootstrapPath = path.join(process.cwd(), 'node_modules/lapeh/lib/bootstrap.ts');
131
-
132
- // 2. Fallback to relative to this script (if running from source or global cache without local install)
133
- const fallbackBootstrapPath = path.resolve(__dirname, '../lib/bootstrap.ts');
134
-
135
- const bootstrapPath = fs.existsSync(localBootstrapPath) ? localBootstrapPath : fallbackBootstrapPath;
136
-
137
- // We execute a script that requires ts-node to run lib/bootstrap.ts
138
- // Use JSON.stringify to properly escape paths for the shell command
139
- const nodeArgs = `-r ${JSON.stringify(tsNodePath)} -r ${JSON.stringify(tsConfigPathsPath)} ${JSON.stringify(bootstrapPath)}`;
140
- const isWin = process.platform === 'win32';
141
-
142
- let cmd;
143
- if (isWin) {
144
- // On Windows, escape inner quotes
145
- const escapedArgs = nodeArgs.replace(/"/g, '\\"');
146
- cmd = `npx nodemon --watch src --watch lib --ext ts,json --exec "node ${escapedArgs}"`;
147
- } else {
148
- // On Linux/Mac, use single quotes for the outer wrapper
149
- cmd = `npx nodemon --watch src --watch lib --ext ts,json --exec 'node ${nodeArgs}'`;
150
- }
151
-
152
- execSync(cmd, { stdio: 'inherit' });
153
- } catch (error) {
154
- // Ignore error
155
- }
156
- }
157
-
158
- function runStart() {
159
- console.log('🚀 Starting Lapeh production server...');
160
-
161
- // In framework-as-dependency model, the bootstrap logic is inside node_modules/lapeh/dist/lib/bootstrap.js
162
- // But wait, the user's project is compiled to `dist/` in their project root.
163
- // The user's `dist` folder will contain their compiled code (src/*).
164
- // But where is the framework code?
165
- // 1. If user builds their project, they build `src` -> `dist/src`.
166
- // 2. The framework code resides in `node_modules/lapeh/dist` (if lapeh is installed as dependency) OR `node_modules/lapeh/lib` (if ts-node).
167
-
168
- // We need to resolve where `bootstrap` is.
169
- // Since we are running `lapeh start` from the CLI package itself.
170
-
171
- let bootstrapPath;
172
- try {
173
- // Try to resolve from the project's node_modules
174
- const projectNodeModules = path.join(process.cwd(), 'node_modules');
175
- const lapehDist = path.join(projectNodeModules, 'lapeh', 'dist', 'lib', 'bootstrap.js');
176
- const lapehLib = path.join(projectNodeModules, 'lapeh', 'lib', 'bootstrap.js');
177
-
178
- if (fs.existsSync(lapehDist)) {
179
- bootstrapPath = lapehDist;
180
- } else if (fs.existsSync(lapehLib)) {
181
- // Fallback to lib if dist doesn't exist (e.g. in dev environment or simple install)
182
- // But `start` implies production, so we should prefer compiled JS.
183
- // If lapeh package.json "main" points to index.js (in root) or lib/bootstrap.ts?
184
- // Actually, for `start` we usually run `node dist/src/index.js` or similar?
185
- // No, Lapeh framework entry point is `bootstrap()`.
186
-
187
- // Let's rely on `require.resolve` relative to the CWD
188
- // We want to require 'lapeh/lib/bootstrap' or 'lapeh/dist/lib/bootstrap'
189
-
190
- // If we are in `node_modules/lapeh/bin/index.js`, `..` is `node_modules/lapeh`.
191
- // So we can require('../lib/bootstrap') directly?
192
- // Yes, if we are running the CLI from the installed package.
193
- bootstrapPath = path.resolve(__dirname, '../lib/bootstrap.js');
194
- if (!fs.existsSync(bootstrapPath)) {
195
- // Try typescript source? No, production run shouldn't use TS.
196
- bootstrapPath = path.resolve(__dirname, '../dist/lib/bootstrap.js');
197
- }
198
- }
199
-
200
- // Correct approach:
201
- // The CLI is running. We want to execute the bootstrap function.
202
- // We can import it right here in this process!
203
- // But `runStart` is inside `bin/index.js` which is likely a JS file.
204
- // We can require('../lib/bootstrap') or '../dist/lib/bootstrap'.
205
-
206
- const frameworkBootstrap = require('../lib/bootstrap');
207
- frameworkBootstrap.bootstrap();
208
- return; // Exit this function, let the bootstrap take over
209
-
210
- } catch (e) {
211
- // If direct require fails (maybe because of ESM/CJS mix or path issues), fallback to child process
212
- }
213
-
214
- // Fallback to previous logic if direct require fails, but fixed path
215
- // The error showed: '.../dist/lib/bootstrap.js' not found.
216
- // Because `lapeh` package structure might be:
217
- // lapeh/
218
- // bin/index.js
219
- // lib/bootstrap.ts (and compiled .js?)
220
- // package.json
221
-
222
- // If we are running from `bin/index.js`, the bootstrap is at `../lib/bootstrap.js` (if compiled) or we need to compile it?
223
- // "lapeh" framework is usually shipped as JS.
224
-
225
- const possiblePaths = [
226
- path.join(__dirname, '../lib/bootstrap.js'),
227
- path.join(__dirname, '../dist/lib/bootstrap.js'),
228
- path.join(process.cwd(), 'node_modules/lapeh/lib/bootstrap.js')
229
- ];
230
-
231
- bootstrapPath = possiblePaths.find(p => fs.existsSync(p));
232
-
233
- if (!bootstrapPath) {
234
- console.error('❌ Could not find Lapeh bootstrap file.');
235
- console.error(' Searched in:', possiblePaths);
236
- process.exit(1);
237
- }
238
-
239
- let cmd;
240
- if (bootstrapPath.endsWith('.ts')) {
241
- // If we found a TS file, we need to use ts-node
242
- // Try to resolve ts-node/register from the framework's dependencies or project's
243
- let tsNodePath;
244
- let tsConfigPathsPath;
245
-
246
- try {
247
- // Try to resolve from current project first
248
- const projectNodeModules = path.join(process.cwd(), 'node_modules');
249
- tsNodePath = require.resolve('ts-node/register', { paths: [projectNodeModules, __dirname] });
250
- tsConfigPathsPath = require.resolve('tsconfig-paths/register', { paths: [projectNodeModules, __dirname] });
251
- } catch (e) {
252
- // Fallback to resolving relative to this script
253
- try {
254
- tsNodePath = require.resolve('ts-node/register');
255
- tsConfigPathsPath = require.resolve('tsconfig-paths/register');
256
- } catch (e2) {
257
- console.warn('⚠️ Could not resolve ts-node/register. Trying npx...');
258
- }
259
- }
260
-
261
- if (tsNodePath && tsConfigPathsPath) {
262
- const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
263
- cmd = `node -r ${JSON.stringify(tsNodePath)} -r ${JSON.stringify(tsConfigPathsPath)} -e ${JSON.stringify(script)}`;
264
- } else {
265
- // Fallback to npx if resolution fails
266
- const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
267
- cmd = `npx ts-node -r tsconfig-paths/register -e ${JSON.stringify(script)}`;
268
- }
269
- } else {
270
- // JS file, run with node
271
- const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
272
- cmd = `node -e ${JSON.stringify(script)}`;
273
- }
274
-
275
- execSync(cmd, {
276
- stdio: 'inherit',
277
- env: { ...process.env, NODE_ENV: 'production' }
278
- });
279
- }
280
-
281
- function runBuild() {
282
- console.log('🛠️ Building Lapeh project...');
283
-
284
- // Compile schema if script exists
285
- const compileSchemaPath = path.join(process.cwd(), 'scripts/compile-schema.js');
286
- if (fs.existsSync(compileSchemaPath)) {
287
- try {
288
- execSync('node scripts/compile-schema.js', { stdio: 'inherit' });
289
- } catch (e) {
290
- console.error('❌ Failed to compile schema.');
291
- process.exit(1);
292
- }
293
- }
294
-
295
- // Generate prisma client
296
- try {
297
- execSync('npx prisma generate', { stdio: 'inherit' });
298
- } catch (e) {
299
- console.error('❌ Failed to generate prisma client.');
300
- process.exit(1);
301
- }
302
-
303
- // Compile TS
304
- try {
305
- execSync('npx tsc -p tsconfig.build.json && npx tsc-alias -p tsconfig.build.json', { stdio: 'inherit' });
306
- } catch (e) {
307
- console.error('❌ Build failed.');
308
- process.exit(1);
309
- }
310
-
311
- console.log('✅ Build complete.');
312
- }
313
-
314
- async function upgradeProject() {
315
- const currentDir = process.cwd();
316
- const templateDir = path.join(__dirname, '..');
317
-
318
- console.log(`🚀 Upgrading Lapeh project in ${currentDir}...`);
319
-
320
- // Check if package.json exists
321
- const packageJsonPath = path.join(currentDir, 'package.json');
322
- if (!fs.existsSync(packageJsonPath)) {
323
- console.error('❌ No package.json found. Are you in the root of a Lapeh project?');
324
- process.exit(1);
325
- }
326
-
327
- // Files/Folders to overwrite/copy
328
- const filesToSync = [
329
- // 'bin', // Removed: CLI script is managed by package
330
- 'lib', // Ensure core framework files are updated
331
- 'scripts',
332
- 'docker-compose.yml',
333
- '.env.example',
334
- '.vscode',
335
- 'tsconfig.json',
336
- 'README.md',
337
- 'ecosystem.config.js',
338
- 'src/redis.ts', // Core framework file
339
- 'src/prisma.ts', // Core framework file
340
- ];
341
-
342
- // Helper to sync directory (copy new/updated, delete removed)
343
- function syncDirectory(src, dest) {
344
- if (!fs.existsSync(src)) return;
345
-
346
- // Ensure dest exists
347
- if (!fs.existsSync(dest)) {
348
- fs.mkdirSync(dest, { recursive: true });
349
- }
350
-
351
- // 1. Copy/Update files from src to dest
352
- const srcEntries = fs.readdirSync(src, { withFileTypes: true });
353
- const srcEntryNames = new Set();
354
-
355
- for (const entry of srcEntries) {
356
- srcEntryNames.add(entry.name);
357
- const srcPath = path.join(src, entry.name);
358
- const destPath = path.join(dest, entry.name);
359
-
360
- if (entry.isDirectory()) {
361
- syncDirectory(srcPath, destPath);
362
- } else {
363
- fs.copyFileSync(srcPath, destPath);
364
- }
365
- }
366
-
367
- // 2. Delete files in dest that are not in src (only if we are syncing a folder)
368
- const destEntries = fs.readdirSync(dest, { withFileTypes: true });
369
- for (const entry of destEntries) {
370
- if (!srcEntryNames.has(entry.name)) {
371
- const destPath = path.join(dest, entry.name);
372
- console.log(`🗑️ Removing obsolete file/directory: ${destPath}`);
373
- if (entry.isDirectory()) {
374
- fs.rmSync(destPath, { recursive: true, force: true });
375
- } else {
376
- fs.unlinkSync(destPath);
377
- }
378
- }
379
- }
380
- }
381
-
382
- // Helper to copy recursive (legacy, kept for other uses if any, but replaced by syncDirectory for upgrade)
383
- function copyRecursive(src, dest) {
384
- if (!fs.existsSync(src)) return;
385
- const stats = fs.statSync(src);
386
- if (stats.isDirectory()) {
387
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
388
- fs.readdirSync(src).forEach(childItemName => {
389
- copyRecursive(path.join(src, childItemName), path.join(dest, childItemName));
390
- });
391
- } else {
392
- // Ensure destination directory exists
393
- const destDir = path.dirname(dest);
394
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
395
- fs.copyFileSync(src, dest);
396
- }
397
- }
398
-
399
- // 1. Migration: Rename .model -> .prisma
400
- const modelsDir = path.join(currentDir, 'src', 'models');
401
- if (fs.existsSync(modelsDir)) {
402
- console.log('🔄 Checking for legacy .model files...');
403
- const files = fs.readdirSync(modelsDir);
404
- let renamedCount = 0;
405
- files.forEach(file => {
406
- if (file.endsWith('.model')) {
407
- const oldPath = path.join(modelsDir, file);
408
- const newPath = path.join(modelsDir, file.replace('.model', '.prisma'));
409
- fs.renameSync(oldPath, newPath);
410
- renamedCount++;
411
- }
412
- });
413
- if (renamedCount > 0) {
414
- console.log(`✅ Migrated ${renamedCount} files from .model to .prisma`);
415
- }
416
- }
417
-
418
- for (const item of filesToSync) {
419
- const srcPath = path.join(templateDir, item);
420
- const destPath = path.join(currentDir, item);
421
-
422
- if (fs.existsSync(srcPath)) {
423
- const stats = fs.statSync(srcPath);
424
- if (stats.isDirectory()) {
425
- console.log(`🔄 Syncing directory ${item}...`);
426
- // Strict sync for 'lib' (framework core), safe sync for others (scripts, etc)
427
- const shouldClean = item === 'lib';
428
- syncDirectory(srcPath, destPath, shouldClean);
429
- } else {
430
- console.log(`🔄 Updating file ${item}...`);
431
- // Ensure dir exists
432
- const destDir = path.dirname(destPath);
433
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
434
- fs.copyFileSync(srcPath, destPath);
435
- }
436
- }
437
- }
438
-
439
- // Merge package.json
440
- console.log('📝 Updating package.json...');
441
- const currentPackageJson = require(packageJsonPath);
442
- const templatePackageJson = require(path.join(templateDir, 'package.json'));
443
-
444
- // Update scripts
445
- currentPackageJson.scripts = {
446
- ...currentPackageJson.scripts,
447
- ...templatePackageJson.scripts,
448
- "dev": "lapeh dev",
449
- "start": "lapeh start",
450
- "build": "lapeh build",
451
- "start:prod": "lapeh start"
452
- };
453
-
454
- // Update dependencies
455
- currentPackageJson.dependencies = {
456
- ...currentPackageJson.dependencies,
457
- ...templatePackageJson.dependencies
458
- };
459
-
460
- // Update devDependencies
461
- currentPackageJson.devDependencies = {
462
- ...currentPackageJson.devDependencies,
463
- ...templatePackageJson.devDependencies
464
- };
465
-
466
- // Update Lapeh version tag
467
- // For local development, we use file reference. For production publish, use version.
468
- currentPackageJson.dependencies["lapeh"] = "file:../";
469
-
470
- fs.writeFileSync(packageJsonPath, JSON.stringify(currentPackageJson, null, 2));
471
-
472
- // Update tsconfig.json to support framework-as-dependency
473
- console.log('🔧 Configuring tsconfig.json...');
474
- const tsconfigPath = path.join(currentDir, 'tsconfig.json');
475
- if (fs.existsSync(tsconfigPath)) {
476
- // Use comment-json or just basic parsing if no comments (standard JSON)
477
- // Since our template tsconfig is standard JSON, require is fine or JSON.parse
478
- const tsconfig = require(tsconfigPath);
479
-
480
- // Update paths
481
- if (tsconfig.compilerOptions && tsconfig.compilerOptions.paths) {
482
- tsconfig.compilerOptions.paths["@lapeh/*"] = ["./node_modules/lapeh/lib/*"];
483
- }
484
-
485
- // Add ts-node ignore configuration
486
- tsconfig["ts-node"] = {
487
- "ignore": ["node_modules/(?!lapeh)"]
488
- };
489
-
490
- fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
491
- }
492
-
493
- // Run npm install
494
- console.log('📦 Installing updated dependencies...');
495
- try {
496
- execSync('npm install', { cwd: currentDir, stdio: 'inherit' });
497
- } catch (error) {
498
- console.error('❌ Error installing dependencies.');
499
- process.exit(1);
500
- }
501
-
502
- console.log('\n✅ Upgrade completed successfully!');
503
- console.log(' Please check your .env file against .env.example for any new required variables.');
504
- }
505
-
506
- function createProject() {
507
- const projectName = args.find(arg => !arg.startsWith('-'));
508
- const isFull = args.includes('--full');
509
- // Allow -y alias for --defaults
510
- const useDefaults = args.includes('--defaults') || args.includes('-y');
511
-
512
- // Helper to parse arguments like --key=value
513
- const getArg = (key) => {
514
- const prefix = `--${key}=`;
515
- const arg = args.find(a => a.startsWith(prefix));
516
- return arg ? arg.substring(prefix.length) : undefined;
517
- };
518
-
519
- const dbTypeArg = getArg('db-type');
520
- const dbHostArg = getArg('db-host');
521
- const dbPortArg = getArg('db-port');
522
- const dbUserArg = getArg('db-user');
523
- const dbPassArg = getArg('db-pass');
524
- const dbNameArg = getArg('db-name');
525
-
526
- if (!projectName) {
527
- console.error('❌ Please specify the project name:');
528
- console.error(' npx lapeh-cli <project-name> [--full] [--defaults|-y]');
529
- console.error(' Options:');
530
- console.error(' --full : Run full setup including seed and dev server');
531
- console.error(' --defaults, -y: Use default configuration (can be overridden with args)');
532
- console.error(' --db-type= : pgsql | mysql');
533
- console.error(' --db-host= : Database host');
534
- console.error(' --db-port= : Database port');
535
- console.error(' --db-user= : Database user');
536
- console.error(' --db-pass= : Database password');
537
- console.error(' --db-name= : Database name');
538
- console.error(' OR');
539
- console.error(' npx lapeh-cli upgrade (inside existing project)');
540
- process.exit(1);
541
- }
542
-
543
- const currentDir = process.cwd();
544
- const projectDir = path.join(currentDir, projectName);
545
- const templateDir = path.join(__dirname, '..');
546
-
547
- if (fs.existsSync(projectDir)) {
548
- console.error(`❌ Directory ${projectName} already exists.`);
549
- process.exit(1);
550
- }
551
-
552
- // Setup readline interface
553
- const rl = readline.createInterface({
554
- input: process.stdin,
555
- output: process.stdout,
556
- });
557
-
558
- const ask = (query, defaultVal) => {
559
- return new Promise((resolve) => {
560
- rl.question(`${query} ${defaultVal ? `[${defaultVal}]` : ""}: `, (answer) => {
561
- resolve(answer.trim() || defaultVal);
562
- });
563
- });
564
- };
565
-
566
- const selectOption = async (query, options) => {
567
- console.log(query);
568
- options.forEach((opt, idx) => {
569
- console.log(` [${opt.key}] ${opt.label}`);
570
- });
571
-
572
- while (true) {
573
- const answer = await ask(">", options[0].key);
574
- const selected = options.find(o => o.key.toLowerCase() === answer.toLowerCase());
575
- if (selected) return selected;
576
-
577
- const byLabel = options.find(o => o.label.toLowerCase().includes(answer.toLowerCase()));
578
- if (byLabel) return byLabel;
579
-
580
- console.log("Pilihan tidak valid. Silakan coba lagi.");
581
- }
582
- };
583
-
584
- (async () => {
585
- console.log(`🚀 Creating a new API Lapeh project in ${projectDir}...`);
586
- fs.mkdirSync(projectDir);
587
-
588
- // --- DATABASE SELECTION ---
589
- console.log("\n--- Database Configuration ---");
590
- let dbType, host, port, user, password, dbName;
591
-
592
- if (useDefaults) {
593
- console.log("ℹ️ Using default configuration (--defaults)...");
594
-
595
- // Default to PostgreSQL
596
- dbType = { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" };
597
- host = "localhost";
598
- port = "5432";
599
- user = "postgres";
600
- password = "password";
601
- dbName = projectName.replace(/-/g, '_');
602
-
603
- // Override with CLI args
604
- if (dbTypeArg) {
605
- if (dbTypeArg.toLowerCase() === 'mysql') {
606
- dbType = { key: "mysql", label: "MySQL", provider: "mysql", defaultPort: "3306" };
607
- port = "3306";
608
- } else if (dbTypeArg.toLowerCase() === 'pgsql') {
609
- dbType = { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" };
610
- port = "5432";
611
- } else if (dbTypeArg.toLowerCase() === 'mongo' || dbTypeArg.toLowerCase() === 'mongodb') {
612
- dbType = { key: "mongo", label: "MongoDB", provider: "mongodb", defaultPort: "27017" };
613
- port = "27017";
614
- user = ""; // MongoDB usually doesn't have default root user in connection string if not auth enabled
615
- }
616
- }
617
-
618
- if (dbHostArg) host = dbHostArg;
619
- if (dbPortArg) port = dbPortArg;
620
- if (dbUserArg) user = dbUserArg;
621
- if (dbPassArg) password = dbPassArg;
622
- if (dbNameArg) dbName = dbNameArg;
623
-
624
- } else {
625
- dbType = await selectOption("Database apa yang akan digunakan?", [
626
- { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" },
627
- { key: "mysql", label: "MySQL", provider: "mysql", defaultPort: "3306" },
628
- { key: "mongo", label: "MongoDB", provider: "mongodb", defaultPort: "27017" },
629
- ]);
630
-
631
- host = await ask("Database Host", "localhost");
632
- port = await ask("Database Port", dbType.defaultPort);
633
- user = await ask("Database User", dbType.key === "mongo" ? "" : "root");
634
- password = await ask("Database Password", "");
635
- dbName = await ask("Database Name", projectName.replace(/-/g, '_')); // Default db name based on project name
636
- }
637
-
638
- let dbUrl = "";
639
- let dbProvider = dbType.provider;
640
-
641
- if (dbType.key === "pgsql") {
642
- dbUrl = `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
643
- } else if (dbType.key === "mysql") {
644
- dbUrl = `mysql://${user}:${password}@${host}:${port}/${dbName}`;
645
- } else if (dbType.key === "mongo") {
646
- const auth = user ? `${user}:${password}@` : "";
647
- dbUrl = `mongodb://${auth}${host}:${port}/${dbName}?authSource=admin`;
648
- }
649
-
650
- if (!useDefaults) {
651
- rl.close();
652
- } else {
653
- // If we didn't use rl, we might not need to close it if we didn't open it?
654
- // Actually rl is created at the top. We should close it.
655
- rl.close();
656
- }
657
-
658
- // List of files/folders to exclude
659
- const ignoreList = [
660
- 'node_modules',
661
- 'dist',
662
- '.git',
663
- '.env',
664
- 'bin', // Exclude bin folder, using dependency instead
665
- 'lib', // Exclude lib folder, using dependency instead
666
- 'package-lock.json',
667
- '.DS_Store',
668
- 'prisma/migrations', // Exclude existing migrations
669
- 'prisma/dev.db', // Exclude sqlite db if exists
670
- 'prisma/dev.db-journal',
671
- 'website',
672
- 'init',
673
- 'test-local-run',
674
- 'coverage',
675
- projectName // Don't copy the destination folder itself if creating inside the template
676
- ];
677
-
678
- function copyDir(src, dest) {
679
- const entries = fs.readdirSync(src, { withFileTypes: true });
680
-
681
- for (const entry of entries) {
682
- const srcPath = path.join(src, entry.name);
683
- const destPath = path.join(dest, entry.name);
684
-
685
- if (ignoreList.includes(entry.name)) {
686
- continue;
687
- }
688
-
689
- // Explicitly check for prisma/migrations to ensure it's skipped at any depth if logic changes
690
- if (entry.name === 'migrations' && srcPath.includes('prisma')) {
691
- continue;
692
- }
693
-
694
- if (entry.isDirectory()) {
695
- fs.mkdirSync(destPath);
696
- copyDir(srcPath, destPath);
697
- } else {
698
- fs.copyFileSync(srcPath, destPath);
699
- }
700
- }
701
- }
702
-
703
- console.log('\n📂 Copying template files...');
704
- copyDir(templateDir, projectDir);
705
-
706
- // Rename gitignore.template to .gitignore
707
- const gitignoreTemplate = path.join(projectDir, 'gitignore.template');
708
- if (fs.existsSync(gitignoreTemplate)) {
709
- fs.renameSync(gitignoreTemplate, path.join(projectDir, '.gitignore'));
710
- }
711
-
712
- // Update package.json
713
- console.log('📝 Updating package.json...');
714
- const packageJsonPath = path.join(projectDir, 'package.json');
715
- const packageJson = require(packageJsonPath);
716
-
717
- packageJson.name = projectName;
718
- // Add lapeh framework version to dependencies to track it like react-router
719
- packageJson.dependencies = packageJson.dependencies || {};
720
-
721
- // Smart dependency resolution:
722
- // If running from node_modules (installed via npm), use the version number.
723
- // If running locally (dev mode), use the absolute file path.
724
- const frameworkPackageJson = require(path.join(__dirname, '../package.json'));
725
-
726
- if (__dirname.includes('node_modules')) {
727
- packageJson.dependencies["lapeh"] = `^${frameworkPackageJson.version}`;
728
- } else {
729
- // Local development
730
- const lapehPath = path.resolve(__dirname, '..').replace(/\\/g, '/');
731
- packageJson.dependencies["lapeh"] = `file:${lapehPath}`;
732
- }
733
-
734
- // Ensure prisma CLI is available in devDependencies for the new project
735
- packageJson.devDependencies = packageJson.devDependencies || {};
736
- packageJson.devDependencies["prisma"] = "5.22.0";
737
- packageJson.devDependencies["dotenv"] = "^16.4.5"; // Ensure dotenv is available for seed script
738
-
739
- // Add missing types for dev
740
- packageJson.devDependencies["@types/express"] = "^5.0.0";
741
- packageJson.devDependencies["@types/compression"] = "^1.7.5";
742
-
743
- packageJson.version = '1.0.0';
744
- packageJson.description = 'Generated by lapeh';
745
- delete packageJson.bin; // Remove the bin entry from the generated project
746
- delete packageJson.repository; // Remove repository info if specific to the template
747
-
748
- // Update scripts to use lapeh binary
749
- packageJson.scripts = {
750
- ...packageJson.scripts,
751
- "postinstall": "node scripts/compile-schema.js && prisma generate",
752
- "dev": "lapeh dev",
753
- "start": "lapeh start",
754
- "build": "lapeh build",
755
- "start:prod": "lapeh start"
756
- };
757
-
758
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
759
-
760
- // Update tsconfig.json to support framework-as-dependency
761
- console.log('🔧 Configuring tsconfig.json...');
762
- const tsconfigPath = path.join(projectDir, 'tsconfig.json');
763
- if (fs.existsSync(tsconfigPath)) {
764
- // Use comment-json or just basic parsing if no comments (standard JSON)
765
- // Since our template tsconfig is standard JSON, require is fine or JSON.parse
766
- const tsconfig = require(tsconfigPath);
767
-
768
- // Update paths
769
- if (tsconfig.compilerOptions && tsconfig.compilerOptions.paths) {
770
- tsconfig.compilerOptions.paths["@lapeh/*"] = ["./node_modules/lapeh/lib/*"];
771
- // Ensure @lapeh/core/database maps correctly to the actual file location
772
- tsconfig.compilerOptions.paths["@lapeh/core/database"] = ["./node_modules/lapeh/lib/core/database.ts"];
773
- }
774
-
775
- // Add baseUrl
776
- tsconfig.compilerOptions.baseUrl = ".";
777
-
778
- // Add ts-node ignore configuration
779
- tsconfig["ts-node"] = {
780
- "ignore": ["node_modules/(?!lapeh)"]
781
- };
782
-
783
- fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
784
- }
785
-
786
- // Configure prisma.config.ts to use tsconfig-paths
787
- const prismaConfigPath = path.join(projectDir, 'prisma.config.ts');
788
- if (fs.existsSync(prismaConfigPath)) {
789
- console.log('🔧 Configuring prisma.config.ts...');
790
- let prismaConfigContent = fs.readFileSync(prismaConfigPath, 'utf8');
791
- prismaConfigContent = prismaConfigContent.replace(
792
- /seed:\s*"ts-node\s+prisma\/seed\.ts"/g,
793
- 'seed: "ts-node -r tsconfig-paths/register prisma/seed.ts"'
794
- );
795
- fs.writeFileSync(prismaConfigPath, prismaConfigContent);
796
- }
797
-
798
- // Configure prisma/seed.ts imports
799
- const prismaSeedPath = path.join(projectDir, 'prisma', 'seed.ts');
800
- if (fs.existsSync(prismaSeedPath)) {
801
- console.log('🔧 Configuring prisma/seed.ts...');
802
- let seedContent = fs.readFileSync(prismaSeedPath, 'utf8');
803
-
804
- // Add dotenv config if missing
805
- if (!seedContent.includes('dotenv.config()')) {
806
- seedContent = 'import dotenv from "dotenv";\ndotenv.config();\n\n' + seedContent;
807
- }
808
-
809
- // Update import path
810
- // We want to import from @lapeh/core/database which maps to lib/prisma.ts
811
- // The alias is configured in tsconfig.json as "@lapeh/core/database": ["./node_modules/lapeh/lib/prisma.ts"]
812
-
813
- // If the template uses relative path or old alias, replace it.
814
- // But wait, the template might already have `import { prisma } from "@lapeh/core/database"`.
815
- // The error is TS2307: Cannot find module. This means ts-node/tsconfig-paths isn't resolving the alias correctly in the seeder context.
816
-
817
- // Ensure seed content uses the alias
818
- seedContent = seedContent.replace(
819
- /import\s+{\s*prisma\s*}\s+from\s+["']\.\.\/src\/prisma["']/,
820
- 'import { prisma } from "@lapeh/core/database"'
821
- );
822
-
823
- // Also handle if it was already replaced or in different format
824
- if (!seedContent.includes('@lapeh/core/database')) {
825
- seedContent = seedContent.replace(
826
- /import\s+{\s*prisma\s*}\s+from\s+["'].*prisma["']/,
827
- 'import { prisma } from "@lapeh/core/database"'
828
- );
829
- }
830
-
831
- // Remove default demo data (Pets) from seed.ts ONLY if NOT using --full
832
- // We want to keep users/roles as they are essential, but remove the demo 'Pets' data
833
- // This matches from "// 6. Seed Pets" up to the completion log message
834
- if (!isFull) {
835
- seedContent = seedContent.replace(
836
- /\/\/ 6\. Seed Pets[\s\S]*?console\.log\("Finished seeding 50,000 pets\."\);/,
837
- '// 6. Seed Pets (Skipped by default. Use --full to include demo data)'
838
- );
839
- }
840
-
841
- fs.writeFileSync(prismaSeedPath, seedContent);
842
- }
843
-
844
- // Create .env from .env.example with correct DB config
845
- console.log('⚙️ Configuring environment...');
846
- const envExamplePath = path.join(projectDir, '.env.example');
847
- const envPath = path.join(projectDir, '.env');
848
- const prismaBaseFile = path.join(projectDir, "prisma", "base.prisma.template");
849
-
850
- if (fs.existsSync(envExamplePath)) {
851
- let envContent = fs.readFileSync(envExamplePath, 'utf8');
852
-
853
- // Replace DATABASE_URL and DATABASE_PROVIDER
854
- if (envContent.includes("DATABASE_URL=")) {
855
- envContent = envContent.replace(/DATABASE_URL=".+"/g, `DATABASE_URL="${dbUrl}"`);
856
- envContent = envContent.replace(/DATABASE_URL=.+/g, `DATABASE_URL="${dbUrl}"`);
857
- } else {
858
- envContent += `\nDATABASE_URL="${dbUrl}"`;
859
- }
860
-
861
- if (envContent.includes("DATABASE_PROVIDER=")) {
862
- envContent = envContent.replace(/DATABASE_PROVIDER=".+"/g, `DATABASE_PROVIDER="${dbProvider}"`);
863
- envContent = envContent.replace(/DATABASE_PROVIDER=.+/g, `DATABASE_PROVIDER="${dbProvider}"`);
864
- } else {
865
- envContent += `\nDATABASE_PROVIDER="${dbProvider}"`;
866
- }
867
-
868
- fs.writeFileSync(envPath, envContent);
869
- }
870
-
871
- // Update prisma/base.prisma.template
872
- console.log("📄 Updating prisma/base.prisma.template...");
873
- if (fs.existsSync(prismaBaseFile)) {
874
- let baseContent = fs.readFileSync(prismaBaseFile, "utf8");
875
- // Replace provider in datasource block
876
- baseContent = baseContent.replace(
877
- /(datasource\s+db\s+\{[\s\S]*?provider\s*=\s*")[^"]+(")/,
878
- `$1${dbProvider}$2`
879
- );
880
- fs.writeFileSync(prismaBaseFile, baseContent);
881
- }
882
-
883
- // Install dependencies
884
- console.log('📦 Installing dependencies (this might take a while)...');
885
- try {
886
- execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
887
- } catch (error) {
888
- console.error('❌ Error installing dependencies.');
889
- process.exit(1);
890
- }
891
-
892
- // Generate JWT Secret
893
- console.log('🔑 Generating JWT Secret...');
894
- try {
895
- execSync('npm run generate:jwt', { cwd: projectDir, stdio: 'inherit' });
896
- } catch (error) {
897
- console.warn('⚠️ Could not generate JWT secret automatically.');
898
- }
899
-
900
- // Generate Prisma Client & Migrate
901
- console.log('🗄️ Setting up database...');
902
- try {
903
- console.log(' Compiling schema...');
904
- execSync('node scripts/compile-schema.js', { cwd: projectDir, stdio: 'inherit' });
905
-
906
- // Try to migrate (this will create the DB if it doesn't exist)
907
- console.log(' Running migration (creates DB if missing)...');
908
- if (dbProvider === 'mongodb') {
909
- execSync('npx prisma db push', { cwd: projectDir, stdio: 'inherit' });
910
- } else {
911
- execSync('npx prisma migrate dev --name init_setup', { cwd: projectDir, stdio: 'inherit' });
912
- }
913
-
914
- // Seed (Users & Roles are mandatory, Pets are demo data)
915
- console.log(' Seeding mandatory data (Users, Roles, Permissions)...');
916
-
917
- if (isFull) {
918
- try {
919
- execSync('npm run db:seed', { cwd: projectDir, stdio: 'inherit' });
920
- } catch (error) {
921
- console.warn('⚠️ Database setup encountered an issue.');
922
- console.warn(' You may need to check your .env credentials and run:');
923
- console.warn(` cd ${projectName}`);
924
- console.warn(' npm run prisma:migrate');
925
- }
926
- } else {
927
- console.log(' ℹ️ Skipping database seeding (use --full to seed default data)...');
928
- }
929
-
930
- console.log(`\n✅ Project ${projectName} created successfully!`);
931
- console.log('\nNext steps:');
932
- console.log(` cd ${projectName}`);
933
- console.log(' npm run dev');
934
- } catch (error) {
935
- console.error('❌ Error setting up database:', error.message);
936
- console.log(`\n✅ Project ${projectName} created, but database setup failed.`);
937
- console.log(' Please check your database credentials in .env and run:');
938
- console.log(` cd ${projectName}`);
939
- console.log(' npm run prisma:migrate');
940
- }
941
- })();
942
- }
943
-
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const readline = require('readline');
7
+
8
+ const args = process.argv.slice(2);
9
+ const command = args[0];
10
+
11
+ // Telemetry Logic
12
+ async function sendTelemetry(cmd, errorInfo = null) {
13
+ try {
14
+ const os = require('os');
15
+
16
+ const payload = {
17
+ command: cmd,
18
+ nodeVersion: process.version,
19
+ osPlatform: os.platform(),
20
+ osRelease: os.release(),
21
+ timestamp: new Date().toISOString()
22
+ };
23
+
24
+ if (errorInfo) {
25
+ payload.error = errorInfo.message;
26
+ payload.stack = errorInfo.stack;
27
+ }
28
+
29
+ const data = JSON.stringify(payload);
30
+
31
+ // Parse URL from env or use default
32
+ const apiUrl = process.env.LAPEH_API_URL || 'https://lapeh-doc.vercel.app/api/telemetry';
33
+ const url = new URL(apiUrl);
34
+ const isHttps = url.protocol === 'https:';
35
+ const client = isHttps ? require('https') : require('http');
36
+
37
+ const options = {
38
+ hostname: url.hostname,
39
+ port: url.port || (isHttps ? 443 : 80),
40
+ path: url.pathname,
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ 'Content-Length': Buffer.byteLength(data)
45
+ },
46
+ timeout: 2000 // Slightly longer for crash reports
47
+ };
48
+
49
+ const req = client.request(options, (res) => {
50
+ res.resume();
51
+ });
52
+
53
+ req.on('error', (e) => {
54
+ // Silent fail
55
+ });
56
+
57
+ req.write(data);
58
+ req.end();
59
+ } catch (e) {
60
+ // Silent fail
61
+ }
62
+ }
63
+
64
+ // Global Error Handler for Crash Reporting
65
+ process.on('uncaughtException', async (err) => {
66
+ console.error('❌ Unexpected Error:', err);
67
+ console.log('📝 Sending crash report...');
68
+ try {
69
+ sendTelemetry(command || 'unknown', err);
70
+
71
+ // Give it a moment to send
72
+ setTimeout(() => {
73
+ process.exit(1);
74
+ }, 1000);
75
+ } catch (e) {
76
+ process.exit(1);
77
+ }
78
+ });
79
+
80
+ // Send telemetry for every command (only if not crashing immediately)
81
+ sendTelemetry(command || 'init');
82
+
83
+ switch (command) {
84
+ case 'dev':
85
+ runDev();
86
+ break;
87
+ case 'start':
88
+ runStart();
89
+ break;
90
+ case 'build':
91
+ runBuild();
92
+ break;
93
+ case 'upgrade':
94
+ (async () => {
95
+ await upgradeProject();
96
+ })();
97
+ break;
98
+ default:
99
+ createProject();
100
+ break;
101
+ }
102
+
103
+ function runDev() {
104
+ console.log('🚀 Starting Lapeh in development mode...');
105
+ try {
106
+ // Generate Prisma Client before starting
107
+ console.log('🔄 Generating Prisma Client...');
108
+ const compileSchemaPath = path.join(process.cwd(), 'scripts/compile-schema.js');
109
+ if (fs.existsSync(compileSchemaPath)) {
110
+ try {
111
+ execSync('node scripts/compile-schema.js', { stdio: 'inherit' });
112
+ } catch (e) {
113
+ console.warn('⚠️ Failed to run compile-schema.js', e.message);
114
+ }
115
+ }
116
+
117
+ try {
118
+ execSync('npx prisma generate', { stdio: 'inherit' });
119
+ } catch (e) {
120
+ console.warn('⚠️ Failed to run prisma generate. Continuing...', e.message);
121
+ }
122
+
123
+ const tsNodePath = require.resolve('ts-node/register');
124
+ const tsConfigPathsPath = require.resolve('tsconfig-paths/register');
125
+
126
+ // Resolve bootstrap file
127
+ // 1. Try to find it in the current project's node_modules (preferred)
128
+ const localBootstrapPath = path.join(process.cwd(), 'node_modules/lapeh/lib/bootstrap.ts');
129
+
130
+ // 2. Fallback to relative to this script (if running from source or global cache without local install)
131
+ const fallbackBootstrapPath = path.resolve(__dirname, '../lib/bootstrap.ts');
132
+
133
+ const bootstrapPath = fs.existsSync(localBootstrapPath) ? localBootstrapPath : fallbackBootstrapPath;
134
+
135
+ // We execute a script that requires ts-node to run lib/bootstrap.ts
136
+ // Use JSON.stringify to properly escape paths for the shell command
137
+ const nodeArgs = `-r ${JSON.stringify(tsNodePath)} -r ${JSON.stringify(tsConfigPathsPath)} ${JSON.stringify(bootstrapPath)}`;
138
+ const isWin = process.platform === 'win32';
139
+
140
+ let cmd;
141
+ if (isWin) {
142
+ // On Windows, escape inner quotes
143
+ const escapedArgs = nodeArgs.replace(/"/g, '\\"');
144
+ cmd = `npx nodemon --watch src --watch lib --ext ts,json --exec "node ${escapedArgs}"`;
145
+ } else {
146
+ // On Linux/Mac, use single quotes for the outer wrapper
147
+ cmd = `npx nodemon --watch src --watch lib --ext ts,json --exec 'node ${nodeArgs}'`;
148
+ }
149
+
150
+ execSync(cmd, { stdio: 'inherit' });
151
+ } catch (error) {
152
+ // Ignore error
153
+ }
154
+ }
155
+
156
+ function runStart() {
157
+ console.log('🚀 Starting Lapeh production server...');
158
+
159
+ let bootstrapPath;
160
+ try {
161
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
162
+ const lapehDist = path.join(projectNodeModules, 'lapeh', 'dist', 'lib', 'bootstrap.js');
163
+ const lapehLib = path.join(projectNodeModules, 'lapeh', 'lib', 'bootstrap.js');
164
+
165
+ if (fs.existsSync(lapehDist)) {
166
+ bootstrapPath = lapehDist;
167
+ } else if (fs.existsSync(lapehLib)) {
168
+ bootstrapPath = path.resolve(__dirname, '../lib/bootstrap.js');
169
+ if (!fs.existsSync(bootstrapPath)) {
170
+ bootstrapPath = path.resolve(__dirname, '../dist/lib/bootstrap.js');
171
+ }
172
+ }
173
+
174
+ const frameworkBootstrap = require('../lib/bootstrap');
175
+ frameworkBootstrap.bootstrap();
176
+ return;
177
+
178
+ } catch (e) {
179
+ }
180
+
181
+ const possiblePaths = [
182
+ path.join(__dirname, '../lib/bootstrap.js'),
183
+ path.join(__dirname, '../dist/lib/bootstrap.js'),
184
+ path.join(process.cwd(), 'node_modules/lapeh/lib/bootstrap.js')
185
+ ];
186
+
187
+ bootstrapPath = possiblePaths.find(p => fs.existsSync(p));
188
+
189
+ if (!bootstrapPath) {
190
+ console.error('❌ Could not find Lapeh bootstrap file.');
191
+ console.error(' Searched in:', possiblePaths);
192
+ process.exit(1);
193
+ }
194
+
195
+ let cmd;
196
+ if (bootstrapPath.endsWith('.ts')) {
197
+ let tsNodePath;
198
+ let tsConfigPathsPath;
199
+
200
+ try {
201
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
202
+ tsNodePath = require.resolve('ts-node/register', { paths: [projectNodeModules, __dirname] });
203
+ tsConfigPathsPath = require.resolve('tsconfig-paths/register', { paths: [projectNodeModules, __dirname] });
204
+ } catch (e) {
205
+ try {
206
+ tsNodePath = require.resolve('ts-node/register');
207
+ tsConfigPathsPath = require.resolve('tsconfig-paths/register');
208
+ } catch (e2) {
209
+ console.warn('⚠️ Could not resolve ts-node/register. Trying npx...');
210
+ }
211
+ }
212
+
213
+ if (tsNodePath && tsConfigPathsPath) {
214
+ const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
215
+ cmd = `node -r ${JSON.stringify(tsNodePath)} -r ${JSON.stringify(tsConfigPathsPath)} -e ${JSON.stringify(script)}`;
216
+ } else {
217
+ const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
218
+ cmd = `npx ts-node -r tsconfig-paths/register -e ${JSON.stringify(script)}`;
219
+ }
220
+ } else {
221
+ const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
222
+ cmd = `node -e ${JSON.stringify(script)}`;
223
+ }
224
+
225
+ execSync(cmd, {
226
+ stdio: 'inherit',
227
+ env: { ...process.env, NODE_ENV: 'production' }
228
+ });
229
+ }
230
+
231
+ function runBuild() {
232
+ console.log('🛠️ Building Lapeh project...');
233
+
234
+ const compileSchemaPath = path.join(process.cwd(), 'scripts/compile-schema.js');
235
+ if (fs.existsSync(compileSchemaPath)) {
236
+ try {
237
+ execSync('node scripts/compile-schema.js', { stdio: 'inherit' });
238
+ } catch (e) {
239
+ console.error('❌ Failed to compile schema.');
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ try {
245
+ execSync('npx prisma generate', { stdio: 'inherit' });
246
+ } catch (e) {
247
+ console.error('❌ Failed to generate prisma client.');
248
+ process.exit(1);
249
+ }
250
+
251
+ try {
252
+ execSync('npx tsc -p tsconfig.build.json && npx tsc-alias -p tsconfig.build.json', { stdio: 'inherit' });
253
+ } catch (e) {
254
+ console.error('❌ Build failed.');
255
+ process.exit(1);
256
+ }
257
+
258
+ console.log('✅ Build complete.');
259
+ }
260
+
261
+ async function upgradeProject() {
262
+ const currentDir = process.cwd();
263
+ const templateDir = path.join(__dirname, '..');
264
+
265
+ console.log(`🚀 Upgrading Lapeh project in ${currentDir}...`);
266
+
267
+ const packageJsonPath = path.join(currentDir, 'package.json');
268
+ if (!fs.existsSync(packageJsonPath)) {
269
+ console.error('❌ No package.json found. Are you in the root of a Lapeh project?');
270
+ process.exit(1);
271
+ }
272
+
273
+ const filesToSync = [
274
+ 'lib',
275
+ 'scripts',
276
+ 'docker-compose.yml',
277
+ '.env.example',
278
+ '.vscode',
279
+ 'tsconfig.json',
280
+ 'README.md',
281
+ 'ecosystem.config.js',
282
+ 'src/redis.ts',
283
+ 'src/prisma.ts',
284
+ 'prisma/base.prisma.template', // Sync base template for upgrade
285
+ 'prisma.config.ts' // Sync prisma config for upgrade
286
+ ];
287
+
288
+ function syncDirectory(src, dest, clean = false) {
289
+ if (!fs.existsSync(src)) return;
290
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
291
+
292
+ const srcEntries = fs.readdirSync(src, { withFileTypes: true });
293
+ const srcEntryNames = new Set();
294
+
295
+ for (const entry of srcEntries) {
296
+ srcEntryNames.add(entry.name);
297
+ const srcPath = path.join(src, entry.name);
298
+ const destPath = path.join(dest, entry.name);
299
+
300
+ if (entry.isDirectory()) {
301
+ syncDirectory(srcPath, destPath, clean);
302
+ } else {
303
+ fs.copyFileSync(srcPath, destPath);
304
+ }
305
+ }
306
+
307
+ if (clean) {
308
+ const destEntries = fs.readdirSync(dest, { withFileTypes: true });
309
+ for (const entry of destEntries) {
310
+ if (!srcEntryNames.has(entry.name)) {
311
+ const destPath = path.join(dest, entry.name);
312
+ console.log(`🗑️ Removing obsolete file/directory: ${destPath}`);
313
+ if (entry.isDirectory()) {
314
+ fs.rmSync(destPath, { recursive: true, force: true });
315
+ } else {
316
+ fs.unlinkSync(destPath);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ // Rename .model -> .prisma (Legacy migration)
324
+ const modelsDir = path.join(currentDir, 'src', 'models');
325
+ if (fs.existsSync(modelsDir)) {
326
+ console.log('🔄 Checking for legacy .model files...');
327
+ const files = fs.readdirSync(modelsDir);
328
+ let renamedCount = 0;
329
+ files.forEach(file => {
330
+ if (file.endsWith('.model')) {
331
+ const oldPath = path.join(modelsDir, file);
332
+ const newPath = path.join(modelsDir, file.replace('.model', '.prisma'));
333
+ fs.renameSync(oldPath, newPath);
334
+ renamedCount++;
335
+ }
336
+ });
337
+ if (renamedCount > 0) {
338
+ console.log(`✅ Migrated ${renamedCount} files from .model to .prisma`);
339
+ }
340
+ }
341
+
342
+ for (const item of filesToSync) {
343
+ const srcPath = path.join(templateDir, item);
344
+ const destPath = path.join(currentDir, item);
345
+
346
+ if (fs.existsSync(srcPath)) {
347
+ const stats = fs.statSync(srcPath);
348
+ if (stats.isDirectory()) {
349
+ console.log(`🔄 Syncing directory ${item}...`);
350
+ syncDirectory(srcPath, destPath, item === 'lib');
351
+ } else {
352
+ console.log(`🔄 Updating file ${item}...`);
353
+ const destDir = path.dirname(destPath);
354
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
355
+ fs.copyFileSync(srcPath, destPath);
356
+ }
357
+ }
358
+ }
359
+
360
+ console.log('📝 Updating package.json...');
361
+ const currentPackageJson = require(packageJsonPath);
362
+ const templatePackageJson = require(path.join(templateDir, 'package.json'));
363
+
364
+ currentPackageJson.scripts = {
365
+ ...currentPackageJson.scripts,
366
+ ...templatePackageJson.scripts,
367
+ "dev": "lapeh dev",
368
+ "start": "lapeh start",
369
+ "build": "lapeh build",
370
+ "start:prod": "lapeh start"
371
+ };
372
+
373
+ currentPackageJson.dependencies = {
374
+ ...currentPackageJson.dependencies,
375
+ ...templatePackageJson.dependencies
376
+ };
377
+
378
+ currentPackageJson.devDependencies = {
379
+ ...currentPackageJson.devDependencies,
380
+ ...templatePackageJson.devDependencies
381
+ };
382
+
383
+ currentPackageJson.dependencies["lapeh"] = "file:../";
384
+
385
+ fs.writeFileSync(packageJsonPath, JSON.stringify(currentPackageJson, null, 2));
386
+
387
+ console.log('🔧 Configuring tsconfig.json...');
388
+ const tsconfigPath = path.join(currentDir, 'tsconfig.json');
389
+ if (fs.existsSync(tsconfigPath)) {
390
+ const tsconfig = require(tsconfigPath);
391
+ if (tsconfig.compilerOptions && tsconfig.compilerOptions.paths) {
392
+ tsconfig.compilerOptions.paths["@lapeh/*"] = ["./node_modules/lapeh/lib/*"];
393
+ }
394
+ tsconfig["ts-node"] = {
395
+ "ignore": ["node_modules/(?!lapeh)"]
396
+ };
397
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
398
+ }
399
+
400
+ console.log('📦 Installing updated dependencies...');
401
+ try {
402
+ execSync('npm install', { cwd: currentDir, stdio: 'inherit' });
403
+ } catch (error) {
404
+ console.error('❌ Error installing dependencies.');
405
+ process.exit(1);
406
+ }
407
+
408
+ console.log('\n✅ Upgrade completed successfully!');
409
+ console.log(' Please check your .env file against .env.example for any new required variables.');
410
+ }
411
+
412
+ function createProject() {
413
+ const projectName = args.find(arg => !arg.startsWith('-'));
414
+ const isFull = args.includes('--full');
415
+ const useDefaults = args.includes('--defaults') || args.includes('--default') || args.includes('-y');
416
+
417
+ if (!projectName) {
418
+ console.error('❌ Please specify the project name:');
419
+ console.error(' npx lapeh-cli <project-name> [--full] [--defaults|-y]');
420
+ process.exit(1);
421
+ }
422
+
423
+ const currentDir = process.cwd();
424
+ const projectDir = path.join(currentDir, projectName);
425
+ const templateDir = path.join(__dirname, '..');
426
+
427
+ if (fs.existsSync(projectDir)) {
428
+ console.error(`❌ Directory ${projectName} already exists.`);
429
+ process.exit(1);
430
+ }
431
+
432
+ const rl = readline.createInterface({
433
+ input: process.stdin,
434
+ output: process.stdout,
435
+ });
436
+
437
+ const ask = (query, defaultVal) => {
438
+ return new Promise((resolve) => {
439
+ rl.question(`${query} ${defaultVal ? `[${defaultVal}]` : ""}: `, (answer) => {
440
+ resolve(answer.trim() || defaultVal);
441
+ });
442
+ });
443
+ };
444
+
445
+ const selectOption = async (query, options) => {
446
+ console.log(query);
447
+ options.forEach((opt, idx) => {
448
+ console.log(` [${opt.key}] ${opt.label}`);
449
+ });
450
+
451
+ while (true) {
452
+ const answer = await ask(">", options[0].key);
453
+ const selected = options.find(o => o.key.toLowerCase() === answer.toLowerCase());
454
+ if (selected) return selected;
455
+
456
+ const byLabel = options.find(o => o.label.toLowerCase().includes(answer.toLowerCase()));
457
+ if (byLabel) return byLabel;
458
+
459
+ console.log("Pilihan tidak valid. Silakan coba lagi.");
460
+ }
461
+ };
462
+
463
+ (async () => {
464
+ console.log(`🚀 Creating a new API Lapeh project in ${projectDir}...`);
465
+ fs.mkdirSync(projectDir);
466
+
467
+ console.log("\n--- ORM Configuration ---");
468
+ let usePrisma = true;
469
+
470
+ if (!useDefaults) {
471
+ const ormChoice = await selectOption("Apakah ingin menggunakan ORM (Prisma)?", [
472
+ { key: "Y", label: "Ya (Disarankan)" },
473
+ { key: "T", label: "Tidak (Setup Manual)" }
474
+ ]);
475
+ usePrisma = ormChoice.key === "Y";
476
+ }
477
+
478
+ let dbType, host, port, user, password, dbName;
479
+ let dbUrl = "";
480
+ let dbProvider = "postgresql";
481
+
482
+ if (usePrisma) {
483
+ if (useDefaults) {
484
+ console.log("ℹ️ Using default database configuration (PostgreSQL)...");
485
+ dbType = { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" };
486
+ host = "localhost";
487
+ port = "5432";
488
+ user = "postgres";
489
+ password = "password";
490
+ dbName = projectName.replace(/-/g, '_');
491
+ } else {
492
+ console.log("\n--- Database Configuration ---");
493
+ dbType = await selectOption("Database apa yang akan digunakan?", [
494
+ { key: "pgsql", label: "PostgreSQL", provider: "postgresql", defaultPort: "5432" },
495
+ { key: "mysql", label: "MySQL", provider: "mysql", defaultPort: "3306" },
496
+ ]);
497
+
498
+ host = await ask("Database Host", "localhost");
499
+ port = await ask("Database Port", dbType.defaultPort);
500
+ user = await ask("Database User", "root");
501
+ password = await ask("Database Password", "");
502
+ dbName = await ask("Database Name", projectName.replace(/-/g, '_'));
503
+ }
504
+
505
+ dbProvider = dbType.provider;
506
+ if (dbType.key === "pgsql") {
507
+ dbUrl = `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=public`;
508
+ } else if (dbType.key === "mysql") {
509
+ dbUrl = `mysql://${user}:${password}@${host}:${port}/${dbName}`;
510
+ }
511
+ } else {
512
+ console.log("ℹ️ Skipping ORM setup. You will need to configure your own database access.");
513
+ }
514
+
515
+ const ignoreList = [
516
+ 'node_modules', 'dist', '.git', '.env', 'bin', 'lib',
517
+ 'package-lock.json', '.DS_Store', 'prisma/migrations',
518
+ 'prisma/dev.db', 'prisma/dev.db-journal', 'website',
519
+ 'init', 'test-local-run', 'coverage', projectName
520
+ ];
521
+
522
+ function copyDir(src, dest) {
523
+ const entries = fs.readdirSync(src, { withFileTypes: true });
524
+ for (const entry of entries) {
525
+ if (ignoreList.includes(entry.name)) continue;
526
+ const srcPath = path.join(src, entry.name);
527
+ const destPath = path.join(dest, entry.name);
528
+
529
+ if (entry.name === 'migrations' && srcPath.includes('prisma')) continue;
530
+
531
+ if (entry.isDirectory()) {
532
+ fs.mkdirSync(destPath);
533
+ copyDir(srcPath, destPath);
534
+ } else {
535
+ fs.copyFileSync(srcPath, destPath);
536
+ }
537
+ }
538
+ }
539
+
540
+ console.log('\n📂 Copying template files...');
541
+ copyDir(templateDir, projectDir);
542
+
543
+ const gitignoreTemplate = path.join(projectDir, 'gitignore.template');
544
+ if (fs.existsSync(gitignoreTemplate)) {
545
+ fs.renameSync(gitignoreTemplate, path.join(projectDir, '.gitignore'));
546
+ }
547
+
548
+ console.log('⚙️ Configuring environment...');
549
+ const envExamplePath = path.join(projectDir, '.env.example');
550
+ const envPath = path.join(projectDir, '.env');
551
+
552
+ if (fs.existsSync(envExamplePath)) {
553
+ let envContent = fs.readFileSync(envExamplePath, 'utf8');
554
+ if (usePrisma) {
555
+ envContent = envContent.replace(/DATABASE_URL=".+"/g, `DATABASE_URL="${dbUrl}"`);
556
+ envContent = envContent.replace(/DATABASE_URL=.+/g, `DATABASE_URL="${dbUrl}"`);
557
+ envContent = envContent.replace(/DATABASE_PROVIDER=".+"/g, `DATABASE_PROVIDER="${dbProvider}"`);
558
+ envContent = envContent.replace(/DATABASE_PROVIDER=.+/g, `DATABASE_PROVIDER="${dbProvider}"`);
559
+ } else {
560
+ envContent = envContent.replace(/DATABASE_URL=".+"/g, `DATABASE_URL=""`);
561
+ envContent = envContent.replace(/DATABASE_URL=.+/g, `DATABASE_URL=""`);
562
+ envContent = envContent.replace(/DATABASE_PROVIDER=".+"/g, `DATABASE_PROVIDER="none"`);
563
+ envContent = envContent.replace(/DATABASE_PROVIDER=.+/g, `DATABASE_PROVIDER="none"`);
564
+ }
565
+ fs.writeFileSync(envPath, envContent);
566
+ }
567
+
568
+ console.log('📝 Updating package.json...');
569
+ const packageJsonPath = path.join(projectDir, 'package.json');
570
+ const packageJson = require(packageJsonPath);
571
+ packageJson.name = projectName;
572
+
573
+ const frameworkPackageJson = require(path.join(__dirname, '../package.json'));
574
+ if (__dirname.includes('node_modules')) {
575
+ packageJson.dependencies["lapeh"] = `^${frameworkPackageJson.version}`;
576
+ } else {
577
+ const lapehPath = path.resolve(__dirname, '..').replace(/\\/g, '/');
578
+ packageJson.dependencies["lapeh"] = `file:${lapehPath}`;
579
+ }
580
+
581
+ packageJson.version = '1.0.0';
582
+ delete packageJson.bin;
583
+
584
+ packageJson.scripts = {
585
+ ...packageJson.scripts,
586
+ "dev": "lapeh dev",
587
+ "start": "lapeh start",
588
+ "build": "lapeh build",
589
+ "start:prod": "lapeh start"
590
+ };
591
+
592
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
593
+
594
+ // Update tsconfig.json for aliases
595
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
596
+ if (fs.existsSync(tsconfigPath)) {
597
+ try {
598
+ const tsconfig = require(tsconfigPath);
599
+ if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
600
+ if (!tsconfig.compilerOptions.paths) tsconfig.compilerOptions.paths = {};
601
+
602
+ // Ensure @lapeh/* points to the installed package
603
+ tsconfig.compilerOptions.paths["@lapeh/*"] = ["./node_modules/lapeh/dist/lib/*"];
604
+
605
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
606
+ } catch (e) {
607
+ console.warn('⚠️ Failed to update tsconfig.json aliases.');
608
+ }
609
+ }
610
+
611
+ const prismaBaseFile = path.join(projectDir, "prisma", "base.prisma.template");
612
+ if (usePrisma && fs.existsSync(prismaBaseFile)) {
613
+ let baseContent = fs.readFileSync(prismaBaseFile, "utf8");
614
+ // Update provider
615
+ baseContent = baseContent.replace(
616
+ /(datasource\s+db\s+\{[\s\S]*?provider\s*=\s*")[^"]+(")/,
617
+ `$1${dbProvider}$2`
618
+ );
619
+ fs.writeFileSync(prismaBaseFile, baseContent);
620
+ }
621
+
622
+ console.log('📦 Installing dependencies...');
623
+ try {
624
+ execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
625
+ } catch (e) {
626
+ console.error('❌ Error installing dependencies.');
627
+ process.exit(1);
628
+ }
629
+
630
+ try {
631
+ execSync('npm run generate:jwt', { cwd: projectDir, stdio: 'inherit' });
632
+ } catch (e) {}
633
+
634
+ if (usePrisma) {
635
+ console.log('🗄️ Setting up database...');
636
+ try {
637
+ execSync('node scripts/compile-schema.js', { cwd: projectDir, stdio: 'inherit' });
638
+
639
+ console.log(' Running migration...');
640
+ if (dbProvider === 'mongodb') {
641
+ execSync('npx prisma db push', { cwd: projectDir, stdio: 'inherit' });
642
+ } else {
643
+ // For Prisma v7, ensure prisma.config.ts is used/detected
644
+ execSync('npx prisma migrate dev --name init_setup', { cwd: projectDir, stdio: 'inherit' });
645
+ }
646
+
647
+ let runSeed = false;
648
+ if (!useDefaults) {
649
+ const seedChoice = await selectOption("Jalankan seeder?", [
650
+ { key: "Y", label: "Ya" },
651
+ { key: "T", label: "Tidak" }
652
+ ]);
653
+ runSeed = seedChoice.key === "Y";
654
+ } else {
655
+ runSeed = isFull;
656
+ }
657
+
658
+ if (runSeed) {
659
+ console.log(' Seeding database...');
660
+ execSync('npm run db:seed', { cwd: projectDir, stdio: 'inherit' });
661
+ }
662
+ } catch (e) {
663
+ console.warn('⚠️ Database setup failed. Check .env and run manually.');
664
+ }
665
+ }
666
+
667
+ console.log(`\n✅ Project ${projectName} created successfully!`);
668
+ rl.close();
669
+ })();
670
+ }