lapeeh 1.0.0

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 (76) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/bin/index.js +934 -0
  4. package/doc/en/ARCHITECTURE_GUIDE.md +79 -0
  5. package/doc/en/CHANGELOG.md +203 -0
  6. package/doc/en/CHEATSHEET.md +90 -0
  7. package/doc/en/CLI.md +111 -0
  8. package/doc/en/CONTRIBUTING.md +119 -0
  9. package/doc/en/DEPLOYMENT.md +171 -0
  10. package/doc/en/FAQ.md +69 -0
  11. package/doc/en/FEATURES.md +99 -0
  12. package/doc/en/GETTING_STARTED.md +84 -0
  13. package/doc/en/INTRODUCTION.md +62 -0
  14. package/doc/en/PACKAGES.md +63 -0
  15. package/doc/en/PERFORMANCE.md +98 -0
  16. package/doc/en/ROADMAP.md +104 -0
  17. package/doc/en/SECURITY.md +95 -0
  18. package/doc/en/STRUCTURE.md +79 -0
  19. package/doc/en/TUTORIAL.md +145 -0
  20. package/doc/id/ARCHITECTURE_GUIDE.md +76 -0
  21. package/doc/id/CHANGELOG.md +203 -0
  22. package/doc/id/CHEATSHEET.md +90 -0
  23. package/doc/id/CLI.md +139 -0
  24. package/doc/id/CONTRIBUTING.md +119 -0
  25. package/doc/id/DEPLOYMENT.md +171 -0
  26. package/doc/id/FAQ.md +69 -0
  27. package/doc/id/FEATURES.md +169 -0
  28. package/doc/id/GETTING_STARTED.md +91 -0
  29. package/doc/id/INTRODUCTION.md +62 -0
  30. package/doc/id/PACKAGES.md +63 -0
  31. package/doc/id/PERFORMANCE.md +100 -0
  32. package/doc/id/ROADMAP.md +107 -0
  33. package/doc/id/SECURITY.md +94 -0
  34. package/doc/id/STRUCTURE.md +79 -0
  35. package/doc/id/TUTORIAL.md +145 -0
  36. package/docker-compose.yml +24 -0
  37. package/ecosystem.config.js +17 -0
  38. package/eslint.config.mjs +26 -0
  39. package/gitignore.template +30 -0
  40. package/lib/bootstrap.ts +210 -0
  41. package/lib/core/realtime.ts +34 -0
  42. package/lib/core/redis.ts +139 -0
  43. package/lib/core/serializer.ts +63 -0
  44. package/lib/core/server.ts +70 -0
  45. package/lib/core/store.ts +116 -0
  46. package/lib/middleware/auth.ts +63 -0
  47. package/lib/middleware/error.ts +50 -0
  48. package/lib/middleware/multipart.ts +13 -0
  49. package/lib/middleware/rateLimit.ts +14 -0
  50. package/lib/middleware/requestLogger.ts +27 -0
  51. package/lib/middleware/visitor.ts +178 -0
  52. package/lib/utils/logger.ts +100 -0
  53. package/lib/utils/pagination.ts +56 -0
  54. package/lib/utils/response.ts +88 -0
  55. package/lib/utils/validator.ts +394 -0
  56. package/nodemon.json +6 -0
  57. package/package.json +126 -0
  58. package/readme.md +357 -0
  59. package/scripts/check-update.js +92 -0
  60. package/scripts/config-clear.js +45 -0
  61. package/scripts/generate-jwt-secret.js +38 -0
  62. package/scripts/init-project.js +84 -0
  63. package/scripts/make-module.js +89 -0
  64. package/scripts/release.js +494 -0
  65. package/scripts/seed-json.js +158 -0
  66. package/scripts/verify-rbac-functional.js +187 -0
  67. package/src/config/app.ts +9 -0
  68. package/src/config/cors.ts +5 -0
  69. package/src/modules/Auth/auth.controller.ts +519 -0
  70. package/src/modules/Rbac/rbac.controller.ts +533 -0
  71. package/src/routes/auth.ts +74 -0
  72. package/src/routes/index.ts +7 -0
  73. package/src/routes/rbac.ts +42 -0
  74. package/storage/logs/.gitkeep +0 -0
  75. package/tsconfig.build.json +12 -0
  76. package/tsconfig.json +30 -0
package/bin/index.js ADDED
@@ -0,0 +1,934 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync, spawn } = require('child_process');
6
+ const readline = require('readline');
7
+
8
+ // --- Helper Functions for Animation ---
9
+
10
+ async function spin(text, fn) {
11
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12
+ let i = 0;
13
+ process.stdout.write(`\x1b[?25l`); // Hide cursor
14
+
15
+ const interval = setInterval(() => {
16
+ process.stdout.write(`\r\x1b[36m${frames[i]} ${text}\x1b[0m`);
17
+ i = (i + 1) % frames.length;
18
+ }, 80);
19
+
20
+ try {
21
+ const result = await fn();
22
+ clearInterval(interval);
23
+ process.stdout.write(`\r\x1b[32m✔ ${text}\x1b[0m\n`);
24
+ return result;
25
+ } catch (e) {
26
+ clearInterval(interval);
27
+ process.stdout.write(`\r\x1b[31m✖ ${text}\x1b[0m\n`);
28
+ throw e;
29
+ } finally {
30
+ process.stdout.write(`\x1b[?25h`); // Show cursor
31
+ }
32
+ }
33
+
34
+ function runCommand(cmd, cwd) {
35
+ return new Promise((resolve, reject) => {
36
+ // Use spawn to capture output or run silently
37
+ // Using shell: true to handle cross-platform command execution
38
+ const child = spawn(cmd, { cwd, shell: true, stdio: 'pipe' });
39
+ let output = '';
40
+
41
+ child.stdout.on('data', (data) => { output += data.toString(); });
42
+ child.stderr.on('data', (data) => { output += data.toString(); });
43
+
44
+ child.on('close', (code) => {
45
+ if (code === 0) resolve(output);
46
+ else reject(new Error(`Command failed with code ${code}\n${output}`));
47
+ });
48
+ });
49
+ }
50
+
51
+ const args = process.argv.slice(2);
52
+ const command = args[0];
53
+
54
+ // Telemetry Logic
55
+ async function sendTelemetry(cmd, errorInfo = null) {
56
+ try {
57
+ const os = require('os');
58
+
59
+ const payload = {
60
+ command: cmd,
61
+ nodeVersion: process.version,
62
+ osPlatform: os.platform(),
63
+ osRelease: os.release(),
64
+ timestamp: new Date().toISOString()
65
+ };
66
+
67
+ if (errorInfo) {
68
+ payload.error = errorInfo.message;
69
+ payload.stack = errorInfo.stack;
70
+ }
71
+
72
+ // Add version to payload
73
+ try {
74
+ const pkg = require(path.join(__dirname, '../package.json'));
75
+ payload.cliVersion = pkg.version;
76
+ } catch (e) {
77
+ payload.cliVersion = 'unknown';
78
+ }
79
+
80
+ const data = JSON.stringify(payload);
81
+
82
+ // Parse URL from env or use default
83
+ const apiUrl = process.env.lapeeh_API_URL || 'https://lapeeh.vercel.app/api/telemetry';
84
+ const url = new URL(apiUrl);
85
+ const isHttps = url.protocol === 'https:';
86
+ const client = isHttps ? require('https') : require('http');
87
+
88
+ const options = {
89
+ hostname: url.hostname,
90
+ port: url.port || (isHttps ? 443 : 80),
91
+ path: url.pathname,
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ 'Content-Length': Buffer.byteLength(data)
96
+ },
97
+ timeout: 2000 // Slightly longer for crash reports
98
+ };
99
+
100
+ const req = client.request(options, (res) => {
101
+ res.resume();
102
+ });
103
+
104
+ req.on('error', (e) => {
105
+ // Silent fail
106
+ });
107
+
108
+ req.write(data);
109
+ req.end();
110
+ } catch (e) {
111
+ // Silent fail
112
+ }
113
+ }
114
+
115
+ // Global Error Handler for Crash Reporting
116
+ process.on('uncaughtException', async (err) => {
117
+ console.error('❌ Unexpected Error:', err);
118
+ console.log('📝 Sending crash report...');
119
+ try {
120
+ sendTelemetry(command || 'unknown', err);
121
+
122
+ // Give it a moment to send
123
+ setTimeout(() => {
124
+ process.exit(1);
125
+ }, 1000);
126
+ } catch (e) {
127
+ process.exit(1);
128
+ }
129
+ });
130
+
131
+ function showHelp() {
132
+ console.log('\n\x1b[36m L A P E H F R A M E W O R K C L I\x1b[0m\n');
133
+ console.log('Usage: npx lapeeh <command> [options]\n');
134
+ console.log('Commands:');
135
+ console.log(' create <name> Create a new lapeeh project');
136
+ console.log(' dev Start development server (with update check)');
137
+ console.log(' start Start production server');
138
+ console.log(' build Build the project for production');
139
+ console.log(' upgrade Upgrade project files to match framework version');
140
+ console.log(' module <name> Create a new module (controller, routes, etc.)');
141
+ console.log(' help Show this help message');
142
+ console.log('\nOptions:');
143
+ console.log(' --full Create project with full example (auth, users, etc)');
144
+ console.log(' -y, --defaults Skip prompts and use defaults');
145
+ console.log(' -h, --help Show this help message');
146
+ console.log('\nExamples:');
147
+ console.log(' npx lapeeh my-app');
148
+ console.log(' npx lapeeh create my-app --full');
149
+ console.log(' npx lapeeh dev');
150
+ console.log('\n');
151
+ }
152
+
153
+ // Handle Help or No Args
154
+ if (!command || ['help', '--help', '-h'].includes(command)) {
155
+ showHelp();
156
+ sendTelemetry('help');
157
+ process.exit(0);
158
+ }
159
+
160
+ // Send telemetry for every command (only if not crashing immediately)
161
+ sendTelemetry(command);
162
+
163
+ switch (command) {
164
+ case 'dev':
165
+ (async () => { await runDev(); })();
166
+ break;
167
+ case 'start':
168
+ (async () => { await runStart(); })();
169
+ break;
170
+ case 'build':
171
+ (async () => { await runBuild(); })();
172
+ break;
173
+ case 'upgrade':
174
+ (async () => {
175
+ await upgradeProject();
176
+ })();
177
+ break;
178
+ case 'make:module':
179
+ case 'module':
180
+ const moduleName = args[1];
181
+ if (!moduleName) {
182
+ console.error('❌ Please specify the module name.');
183
+ console.error(' Usage: npx lapeeh module <ModuleName>');
184
+ process.exit(1);
185
+ }
186
+ createModule(moduleName);
187
+ break;
188
+ case 'init':
189
+ case 'create':
190
+ createProject(true);
191
+ break;
192
+ default:
193
+ createProject(false);
194
+ break;
195
+ }
196
+
197
+ async function checkUpdate() {
198
+ try {
199
+ const pkg = require(path.join(__dirname, '../package.json'));
200
+ const currentVersion = pkg.version;
201
+
202
+ // Fetch latest version from npm registry
203
+ const latestVersion = await new Promise((resolve) => {
204
+ const https = require('https');
205
+ const req = https.get('https://registry.npmjs.org/lapeeh/latest', {
206
+ headers: { 'User-Agent': 'lapeeh-CLI' },
207
+ timeout: 1500 // 1.5s timeout
208
+ }, (res) => {
209
+ let data = '';
210
+ res.on('data', chunk => data += chunk);
211
+ res.on('end', () => {
212
+ try {
213
+ const json = JSON.parse(data);
214
+ resolve(json.version);
215
+ } catch (e) {
216
+ resolve(null);
217
+ }
218
+ });
219
+ });
220
+
221
+ req.on('error', () => resolve(null));
222
+ req.on('timeout', () => {
223
+ req.destroy();
224
+ resolve(null);
225
+ });
226
+ });
227
+
228
+ if (latestVersion && latestVersion !== currentVersion) {
229
+ const currentParts = currentVersion.split('.').map(Number);
230
+ const latestParts = latestVersion.split('.').map(Number);
231
+
232
+ let isOutdated = false;
233
+ for(let i=0; i<3; i++) {
234
+ if (latestParts[i] > currentParts[i]) {
235
+ isOutdated = true;
236
+ break;
237
+ } else if (latestParts[i] < currentParts[i]) {
238
+ break;
239
+ }
240
+ }
241
+
242
+ if (isOutdated) {
243
+ console.log('\n');
244
+ console.log('\x1b[33m┌────────────────────────────────────────────────────────────┐\x1b[0m');
245
+ console.log(`\x1b[33m│\x1b[0m \x1b[1mUpdate available!\x1b[0m \x1b[31m${currentVersion}\x1b[0m → \x1b[32m${latestVersion}\x1b[0m \x1b[33m│\x1b[0m`);
246
+ console.log(`\x1b[33m│\x1b[0m Run \x1b[36mnpm install @lapeeh/lapeeh@latest\x1b[0m to update \x1b[33m│\x1b[0m`);
247
+ console.log(`\x1b[33m│\x1b[0m Then run \x1b[36mnpx lapeeh upgrade\x1b[0m to sync files \x1b[33m│\x1b[0m`);
248
+ console.log('\x1b[33m└────────────────────────────────────────────────────────────┘\x1b[0m');
249
+ console.log('\n');
250
+ }
251
+ }
252
+ } catch (e) {
253
+ // Ignore errors during update check
254
+ }
255
+ }
256
+
257
+ async function runDev() {
258
+ console.log('🚀 Starting lapeeh in development mode...');
259
+ await checkUpdate();
260
+ try {
261
+ const tsNodePath = require.resolve('ts-node/register');
262
+ const tsConfigPathsPath = require.resolve('tsconfig-paths/register');
263
+
264
+ // Resolve bootstrap file
265
+ // 1. Try to find it in the current project's node_modules (preferred)
266
+ const localBootstrapPath = path.join(process.cwd(), 'node_modules/lapeeh/lib/bootstrap.ts');
267
+
268
+ // 2. Fallback to relative to this script (if running from source or global cache without local install)
269
+ const fallbackBootstrapPath = path.resolve(__dirname, '../lib/bootstrap.ts');
270
+
271
+ const bootstrapPath = fs.existsSync(localBootstrapPath) ? localBootstrapPath : fallbackBootstrapPath;
272
+
273
+ // We execute a script that requires ts-node to run lib/bootstrap.ts
274
+ // Use JSON.stringify to properly escape paths for the shell command
275
+ const nodeArgs = `-r ${JSON.stringify(tsNodePath)} -r ${JSON.stringify(tsConfigPathsPath)} ${JSON.stringify(bootstrapPath)}`;
276
+ const isWin = process.platform === 'win32';
277
+
278
+ let cmd;
279
+ if (isWin) {
280
+ // On Windows, escape inner quotes
281
+ const escapedArgs = nodeArgs.replace(/"/g, '\\"');
282
+ cmd = `npx nodemon --watch src --watch lib --ext ts,json --exec "node ${escapedArgs}"`;
283
+ } else {
284
+ // On Linux/Mac, use single quotes for the outer wrapper
285
+ cmd = `npx nodemon --watch src --watch lib --ext ts,json --exec 'node ${nodeArgs}'`;
286
+ }
287
+
288
+ execSync(cmd, { stdio: 'inherit' });
289
+ } catch (error) {
290
+ // Ignore error
291
+ }
292
+ }
293
+
294
+ async function runStart() {
295
+ await spin('Starting lapeeh production server...', async () => {
296
+ await new Promise(r => setTimeout(r, 1500)); // Simulate startup checks animation
297
+ });
298
+
299
+ let bootstrapPath;
300
+ try {
301
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
302
+ const lapeehDist = path.join(projectNodeModules, 'lapeeh', 'dist', 'lib', 'bootstrap.js');
303
+ const lapeehLib = path.join(projectNodeModules, 'lapeeh', 'lib', 'bootstrap.js');
304
+
305
+ if (fs.existsSync(lapeehDist)) {
306
+ bootstrapPath = lapeehDist;
307
+ } else if (fs.existsSync(lapeehLib)) {
308
+ bootstrapPath = path.resolve(__dirname, '../lib/bootstrap.js');
309
+ if (!fs.existsSync(bootstrapPath)) {
310
+ bootstrapPath = path.resolve(__dirname, '../dist/lib/bootstrap.js');
311
+ }
312
+ }
313
+
314
+ const frameworkBootstrap = require('../lib/bootstrap');
315
+ frameworkBootstrap.bootstrap();
316
+ return;
317
+
318
+ } catch (e) {
319
+ }
320
+
321
+ const possiblePaths = [
322
+ path.join(__dirname, '../lib/bootstrap.js'),
323
+ path.join(__dirname, '../dist/lib/bootstrap.js'),
324
+ path.join(process.cwd(), 'node_modules/@lapeeh/lapeeh/lib/bootstrap.js')
325
+ ];
326
+
327
+ bootstrapPath = possiblePaths.find(p => fs.existsSync(p));
328
+
329
+ if (!bootstrapPath) {
330
+ console.error('❌ Could not find lapeeh bootstrap file.');
331
+ console.error(' Searched in:', possiblePaths);
332
+ process.exit(1);
333
+ }
334
+
335
+ let cmd;
336
+ if (bootstrapPath.endsWith('.ts')) {
337
+ let tsNodePath;
338
+ let tsConfigPathsPath;
339
+
340
+ try {
341
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
342
+ tsNodePath = require.resolve('ts-node/register', { paths: [projectNodeModules, __dirname] });
343
+ tsConfigPathsPath = require.resolve('tsconfig-paths/register', { paths: [projectNodeModules, __dirname] });
344
+ } catch (e) {
345
+ try {
346
+ tsNodePath = require.resolve('ts-node/register');
347
+ tsConfigPathsPath = require.resolve('tsconfig-paths/register');
348
+ } catch (e2) {
349
+ console.warn('⚠️ Could not resolve ts-node/register. Trying npx...');
350
+ }
351
+ }
352
+
353
+ if (tsNodePath && tsConfigPathsPath) {
354
+ const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
355
+ cmd = `node -r ${JSON.stringify(tsNodePath)} -r ${JSON.stringify(tsConfigPathsPath)} -e ${JSON.stringify(script)}`;
356
+ } else {
357
+ const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
358
+ cmd = `npx ts-node -r tsconfig-paths/register -e ${JSON.stringify(script)}`;
359
+ }
360
+ } else {
361
+ const script = `require(${JSON.stringify(bootstrapPath)}).bootstrap()`;
362
+ cmd = `node -e ${JSON.stringify(script)}`;
363
+ }
364
+
365
+ execSync(cmd, {
366
+ stdio: 'inherit',
367
+ env: { ...process.env, NODE_ENV: 'production' }
368
+ });
369
+ }
370
+
371
+ function runBuild() {
372
+ console.log('🛠️ Building lapeeh project...');
373
+
374
+ try {
375
+ execSync('npx tsc -p tsconfig.build.json && npx tsc-alias -p tsconfig.build.json', { stdio: 'inherit' });
376
+ } catch (e) {
377
+ console.error('❌ Build failed.');
378
+ process.exit(1);
379
+ }
380
+
381
+ console.log('✅ Build complete.');
382
+ }
383
+
384
+ async function upgradeProject() {
385
+ const currentDir = process.cwd();
386
+ const templateDir = path.join(__dirname, '..');
387
+
388
+ console.log(`🚀 Upgrading lapeeh project in ${currentDir}...`);
389
+
390
+ const packageJsonPath = path.join(currentDir, 'package.json');
391
+ if (!fs.existsSync(packageJsonPath)) {
392
+ console.error('❌ No package.json found. Are you in the root of a lapeeh project?');
393
+ process.exit(1);
394
+ }
395
+
396
+ const filesToSync = [
397
+ 'lib',
398
+ 'docker-compose.yml',
399
+ '.env.example',
400
+ '.vscode',
401
+ 'tsconfig.json',
402
+ 'README.md',
403
+ 'ecosystem.config.js',
404
+ 'src/redis.ts'
405
+ ];
406
+
407
+ const scriptsDir = path.join(currentDir, 'scripts');
408
+ if (fs.existsSync(scriptsDir)) {
409
+ console.log(`🗑️ Removing obsolete directory: ${scriptsDir}`);
410
+ fs.rmSync(scriptsDir, { recursive: true, force: true });
411
+ }
412
+
413
+ const updateStats = {
414
+ updated: [],
415
+ created: [],
416
+ removed: []
417
+ };
418
+
419
+ function syncDirectory(src, dest, clean = false) {
420
+ if (!fs.existsSync(src)) return;
421
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
422
+
423
+ const srcEntries = fs.readdirSync(src, { withFileTypes: true });
424
+ const srcEntryNames = new Set();
425
+
426
+ for (const entry of srcEntries) {
427
+ srcEntryNames.add(entry.name);
428
+ const srcPath = path.join(src, entry.name);
429
+ const destPath = path.join(dest, entry.name);
430
+ const relativePath = path.relative(currentDir, destPath);
431
+
432
+ if (entry.isDirectory()) {
433
+ syncDirectory(srcPath, destPath, clean);
434
+ } else {
435
+ let shouldCopy = true;
436
+
437
+ if (fs.existsSync(destPath)) {
438
+ const srcContent = fs.readFileSync(srcPath);
439
+ const destContent = fs.readFileSync(destPath);
440
+ if (srcContent.equals(destContent)) {
441
+ shouldCopy = false;
442
+ } else {
443
+ updateStats.updated.push(relativePath);
444
+ }
445
+ } else {
446
+ updateStats.created.push(relativePath);
447
+ }
448
+
449
+ if (shouldCopy) {
450
+ fs.copyFileSync(srcPath, destPath);
451
+ }
452
+ }
453
+ }
454
+
455
+ if (clean) {
456
+ const destEntries = fs.readdirSync(dest, { withFileTypes: true });
457
+ for (const entry of destEntries) {
458
+ if (!srcEntryNames.has(entry.name)) {
459
+ const destPath = path.join(dest, entry.name);
460
+ const relativePath = path.relative(currentDir, destPath);
461
+
462
+ console.log(`🗑️ Removing obsolete file/directory: ${destPath}`);
463
+ updateStats.removed.push(relativePath);
464
+
465
+ if (entry.isDirectory()) {
466
+ fs.rmSync(destPath, { recursive: true, force: true });
467
+ } else {
468
+ fs.unlinkSync(destPath);
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ for (const item of filesToSync) {
476
+ const srcPath = path.join(templateDir, item);
477
+ const destPath = path.join(currentDir, item);
478
+ const relativePath = item; // Since item is relative to templateDir/currentDir
479
+
480
+ if (fs.existsSync(srcPath)) {
481
+ const stats = fs.statSync(srcPath);
482
+ if (stats.isDirectory()) {
483
+ console.log(`🔄 Syncing directory ${item}...`);
484
+ syncDirectory(srcPath, destPath, item === 'lib');
485
+ } else {
486
+ console.log(`🔄 Checking file ${item}...`);
487
+ const destDir = path.dirname(destPath);
488
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
489
+
490
+ let shouldCopy = true;
491
+ if (fs.existsSync(destPath)) {
492
+ const srcContent = fs.readFileSync(srcPath);
493
+ const destContent = fs.readFileSync(destPath);
494
+ if (srcContent.equals(destContent)) {
495
+ shouldCopy = false;
496
+ } else {
497
+ updateStats.updated.push(relativePath);
498
+ }
499
+ } else {
500
+ updateStats.created.push(relativePath);
501
+ }
502
+
503
+ if (shouldCopy) {
504
+ fs.copyFileSync(srcPath, destPath);
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ console.log('📝 Updating package.json...');
511
+ const currentPackageJson = require(packageJsonPath);
512
+
513
+ // Capture original dependency before merging
514
+ const originallapeehDep = currentPackageJson.dependencies && currentPackageJson.dependencies["lapeeh"];
515
+
516
+ const templatePackageJson = require(path.join(templateDir, 'package.json'));
517
+
518
+ // Define scripts to remove (those that depend on the scripts folder)
519
+ const scriptsToRemove = ['first', 'generate:jwt', 'make:module', 'make:modul', 'config:clear', 'release'];
520
+
521
+ // Filter template scripts
522
+ const filteredTemplateScripts = Object.keys(templatePackageJson.scripts)
523
+ .filter(key => !scriptsToRemove.includes(key))
524
+ .reduce((obj, key) => {
525
+ obj[key] = templatePackageJson.scripts[key];
526
+ return obj;
527
+ }, {});
528
+
529
+ currentPackageJson.scripts = {
530
+ ...currentPackageJson.scripts,
531
+ ...filteredTemplateScripts,
532
+ "dev": "lapeeh dev",
533
+ "start": "lapeeh start",
534
+ "build": "lapeeh build",
535
+ "start:prod": "lapeeh start"
536
+ };
537
+
538
+ // Clean up existing scripts that we want to remove
539
+ scriptsToRemove.forEach(script => {
540
+ if (currentPackageJson.scripts[script]) {
541
+ delete currentPackageJson.scripts[script];
542
+ }
543
+ });
544
+
545
+ currentPackageJson.dependencies = {
546
+ ...currentPackageJson.dependencies,
547
+ ...templatePackageJson.dependencies
548
+ };
549
+
550
+ currentPackageJson.devDependencies = {
551
+ ...currentPackageJson.devDependencies,
552
+ ...templatePackageJson.devDependencies
553
+ };
554
+
555
+ const frameworkPackageJson = require(path.join(templateDir, 'package.json'));
556
+
557
+ if (originallapeehDep && originallapeehDep.startsWith('file:')) {
558
+ console.log(`ℹ️ Preserving local 'lapeeh' dependency: ${originallapeehDep}`);
559
+ currentPackageJson.dependencies["lapeeh"] = originallapeehDep;
560
+ } else {
561
+ if (__dirname.includes('node_modules')) {
562
+ currentPackageJson.dependencies["lapeeh"] = `^${frameworkPackageJson.version}`;
563
+ } else {
564
+ const lapeehPath = path.resolve(__dirname, '..').replace(/\\/g, '/');
565
+ currentPackageJson.dependencies["lapeeh"] = `file:${lapeehPath}`;
566
+ }
567
+ }
568
+
569
+ // Ensure prisma config exists for seed
570
+ if (!currentPackageJson.prisma) {
571
+ currentPackageJson.prisma = {
572
+ seed: "npx ts-node -r tsconfig-paths/register prisma/seed.ts"
573
+ };
574
+ }
575
+
576
+ fs.writeFileSync(packageJsonPath, JSON.stringify(currentPackageJson, null, 2));
577
+
578
+ console.log('🔧 Configuring tsconfig.json...');
579
+ const tsconfigPath = path.join(currentDir, 'tsconfig.json');
580
+ if (fs.existsSync(tsconfigPath)) {
581
+ const tsconfig = require(tsconfigPath);
582
+ if (tsconfig.compilerOptions && tsconfig.compilerOptions.paths) {
583
+ tsconfig.compilerOptions.paths["@lapeeh/*"] = ["./node_modules/@lapeeh/lapeeh/dist/lib/*"];
584
+ }
585
+ tsconfig["ts-node"] = {
586
+ "ignore": ["node_modules/(?!@lapeeh)"]
587
+ };
588
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
589
+ }
590
+
591
+ console.log('📦 Installing updated dependencies...');
592
+ try {
593
+ execSync('npm install', { cwd: currentDir, stdio: 'inherit' });
594
+ } catch (error) {
595
+ console.error('❌ Error installing dependencies.');
596
+ process.exit(1);
597
+ }
598
+
599
+ console.log('\n✅ Upgrade completed successfully!');
600
+
601
+ if (updateStats.created.length > 0) {
602
+ console.log('\n✨ Created files:');
603
+ updateStats.created.forEach(f => console.log(` \x1b[32m+ ${f}\x1b[0m`));
604
+ }
605
+
606
+ if (updateStats.updated.length > 0) {
607
+ console.log('\n📝 Updated files:');
608
+ updateStats.updated.forEach(f => console.log(` \x1b[33m~ ${f}\x1b[0m`));
609
+ }
610
+
611
+ if (updateStats.removed.length > 0) {
612
+ console.log('\n🗑️ Removed files:');
613
+ updateStats.removed.forEach(f => console.log(` \x1b[31m- ${f}\x1b[0m`));
614
+ }
615
+
616
+ if (updateStats.created.length === 0 && updateStats.updated.length === 0 && updateStats.removed.length === 0) {
617
+ console.log(' No files were changed.');
618
+ }
619
+
620
+ console.log('\n Please check your .env file against .env.example for any new required variables.');
621
+ }
622
+
623
+ function createModule(moduleName) {
624
+ // Capitalize first letter
625
+ const name = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
626
+ const lowerName = moduleName.toLowerCase();
627
+
628
+ const currentDir = process.cwd();
629
+ // Support both src/modules (default) and just modules if user changed structure
630
+ const srcModulesDir = path.join(currentDir, 'src', 'modules');
631
+ const modulesDir = fs.existsSync(srcModulesDir) ? srcModulesDir : path.join(currentDir, 'modules');
632
+
633
+ if (!fs.existsSync(path.join(currentDir, 'src')) && !fs.existsSync(modulesDir)) {
634
+ console.error('❌ Could not find src directory. Are you in a lapeeh project root?');
635
+ process.exit(1);
636
+ }
637
+
638
+ const targetDir = path.join(modulesDir, name);
639
+
640
+ if (fs.existsSync(targetDir)) {
641
+ console.error(`❌ Module ${name} already exists at ${targetDir}`);
642
+ process.exit(1);
643
+ }
644
+
645
+ fs.mkdirSync(targetDir, { recursive: true });
646
+
647
+ // Controller
648
+ const controllerContent = `import { Request, Response } from "express";
649
+ import { sendSuccess } from "@lapeeh/utils/response";
650
+ // import * as ${name}Service from "./${lowerName}.service";
651
+
652
+ export async function index(_req: Request, res: Response) {
653
+ sendSuccess(res, 200, "Index ${name}");
654
+ }
655
+
656
+ export async function show(req: Request, res: Response) {
657
+ const { id } = req.params;
658
+ sendSuccess(res, 200, "Show ${name} " + id);
659
+ }
660
+
661
+ export async function create(_req: Request, res: Response) {
662
+ sendSuccess(res, 201, "Create ${name}");
663
+ }
664
+
665
+ export async function update(req: Request, res: Response) {
666
+ const { id } = req.params;
667
+ sendSuccess(res, 200, "Update ${name} " + id);
668
+ }
669
+
670
+ export async function destroy(req: Request, res: Response) {
671
+ const { id } = req.params;
672
+ sendSuccess(res, 200, "Delete ${name} " + id);
673
+ }
674
+ `;
675
+
676
+ fs.writeFileSync(path.join(targetDir, `${lowerName}.controller.ts`), controllerContent);
677
+
678
+ // Service
679
+ const serviceContent = `
680
+ export async function findAll() {
681
+ return [];
682
+ }
683
+
684
+ export async function findOne(_id: number) {
685
+ return null;
686
+ }
687
+ `;
688
+ fs.writeFileSync(path.join(targetDir, `${lowerName}.service.ts`), serviceContent);
689
+
690
+ // Route Stub
691
+ const routeContent = `import { Router } from "express";
692
+ import * as ${name}Controller from "./${lowerName}.controller";
693
+
694
+ const router = Router();
695
+
696
+ router.get("/", ${name}Controller.index);
697
+ router.get("/:id", ${name}Controller.show);
698
+ router.post("/", ${name}Controller.create);
699
+ router.put("/:id", ${name}Controller.update);
700
+ router.delete("/:id", ${name}Controller.destroy);
701
+
702
+ export default router;
703
+ `;
704
+ fs.writeFileSync(path.join(targetDir, `${lowerName}.routes.ts`), routeContent);
705
+
706
+ console.log(`✅ Module ${name} created successfully at src/modules/${name}`);
707
+ console.log(` - ${lowerName}.controller.ts`);
708
+ console.log(` - ${lowerName}.service.ts`);
709
+ console.log(` - ${lowerName}.routes.ts`);
710
+ console.log(`\n👉 Don't forget to register the route in src/routes/index.ts!`);
711
+ }
712
+
713
+ function createProject(skipFirstArg = false) {
714
+ const searchArgs = skipFirstArg ? args.slice(1) : args;
715
+ const projectName = searchArgs.find(arg => !arg.startsWith('-'));
716
+ const isFull = args.includes('--full');
717
+ const useDefaults = args.includes('--defaults') || args.includes('--default') || args.includes('-y');
718
+
719
+ if (!projectName) {
720
+ console.error('❌ Please specify the project name:');
721
+ console.error(' npx lapeeh-cli <project-name> [--full] [--defaults|-y]');
722
+ process.exit(1);
723
+ }
724
+
725
+ const currentDir = process.cwd();
726
+ const projectDir = path.join(currentDir, projectName);
727
+ const templateDir = path.join(__dirname, '..');
728
+
729
+ if (fs.existsSync(projectDir)) {
730
+ console.error(`❌ Directory ${projectName} already exists.`);
731
+ process.exit(1);
732
+ }
733
+
734
+ const rl = readline.createInterface({
735
+ input: process.stdin,
736
+ output: process.stdout,
737
+ });
738
+
739
+ const ask = (query, defaultVal) => {
740
+ return new Promise((resolve) => {
741
+ rl.question(`${query} ${defaultVal ? `[${defaultVal}]` : ""}: `, (answer) => {
742
+ resolve(answer.trim() || defaultVal);
743
+ });
744
+ });
745
+ };
746
+
747
+ const selectOption = async (query, options) => {
748
+ console.log(query);
749
+ options.forEach((opt, idx) => {
750
+ console.log(` [${opt.key}] ${opt.label}`);
751
+ });
752
+
753
+ while (true) {
754
+ const answer = await ask(">", options[0].key);
755
+ const selected = options.find(o => o.key.toLowerCase() === answer.toLowerCase());
756
+ if (selected) return selected;
757
+
758
+ const byLabel = options.find(o => o.label.toLowerCase().includes(answer.toLowerCase()));
759
+ if (byLabel) return byLabel;
760
+
761
+ console.log("Pilihan tidak valid. Silakan coba lagi.");
762
+ }
763
+ };
764
+
765
+ (async () => {
766
+ // Animation lapeeh "L A P E H"
767
+ const frames = [
768
+ "██╗ █████╗ ██████╗ ███████╗██╗ ██╗",
769
+ "██║ ██╔══██╗██╔══██╗██╔════╝██║ ██║",
770
+ "██║ ███████║██████╔╝█████╗ ███████║",
771
+ "██║ ██╔══██║██╔═══╝ ██╔══╝ ██╔══██║",
772
+ "███████╗██║ ██║██║ ███████╗██║ ██║",
773
+ "╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝"
774
+ ];
775
+
776
+ console.clear();
777
+ console.log('\n');
778
+ for (let i = 0; i < frames.length; i++) {
779
+ await new Promise(r => setTimeout(r, 100));
780
+ console.log(`\x1b[36m ${frames[i]}\x1b[0m`);
781
+ }
782
+ console.log('\n\x1b[36m L A P E H F R A M E W O R K\x1b[0m\n');
783
+ await new Promise(r => setTimeout(r, 800));
784
+
785
+ console.log(`🚀 Creating a new API lapeeh project in ${projectDir}...`);
786
+ fs.mkdirSync(projectDir);
787
+
788
+ const ignoreList = [
789
+ 'node_modules', 'dist', '.git', '.env', 'bin', 'scripts',
790
+ 'package-lock.json', '.DS_Store', 'prisma', 'website',
791
+ 'init', 'test-local-run', 'coverage', 'doc', projectName
792
+ ];
793
+
794
+ function copyDir(src, dest) {
795
+ const entries = fs.readdirSync(src, { withFileTypes: true });
796
+ for (const entry of entries) {
797
+ if (ignoreList.includes(entry.name)) continue;
798
+ const srcPath = path.join(src, entry.name);
799
+ const destPath = path.join(dest, entry.name);
800
+
801
+ // Clean storage/logs: skip everything except .gitkeep
802
+ // Check if we are inside storage/logs
803
+ const relPath = path.relative(templateDir, srcPath);
804
+ const isInLogs = relPath.includes(path.join('storage', 'logs')) || relPath.includes('storage/logs') || relPath.includes('storage\\logs');
805
+
806
+ if (isInLogs && !entry.isDirectory() && entry.name !== '.gitkeep') {
807
+ continue;
808
+ }
809
+
810
+ if (entry.isDirectory()) {
811
+ fs.mkdirSync(destPath);
812
+ copyDir(srcPath, destPath);
813
+ } else {
814
+ fs.copyFileSync(srcPath, destPath);
815
+ }
816
+ }
817
+ }
818
+
819
+ console.log('\n📂 Copying template files...');
820
+ copyDir(templateDir, projectDir);
821
+
822
+ const gitignoreTemplate = path.join(projectDir, 'gitignore.template');
823
+ if (fs.existsSync(gitignoreTemplate)) {
824
+ fs.renameSync(gitignoreTemplate, path.join(projectDir, '.gitignore'));
825
+ }
826
+
827
+ console.log('⚙️ Configuring environment...');
828
+ const envExamplePath = path.join(projectDir, '.env.example');
829
+ const envPath = path.join(projectDir, '.env');
830
+
831
+ if (fs.existsSync(envExamplePath)) {
832
+ let envContent = fs.readFileSync(envExamplePath, 'utf8');
833
+ fs.writeFileSync(envPath, envContent);
834
+ }
835
+
836
+ console.log('📝 Updating package.json...');
837
+ const packageJsonPath = path.join(projectDir, 'package.json');
838
+ const packageJson = require(packageJsonPath);
839
+ packageJson.name = projectName;
840
+
841
+ const frameworkPackageJson = require(path.join(__dirname, '../package.json'));
842
+ if (__dirname.includes('node_modules')) {
843
+ packageJson.dependencies["lapeeh"] = `^${frameworkPackageJson.version}`;
844
+ } else {
845
+ const lapeehPath = path.resolve(__dirname, '..').replace(/\\/g, '/');
846
+ packageJson.dependencies["lapeeh"] = `file:${lapeehPath}`;
847
+ }
848
+
849
+
850
+ packageJson.version = '1.0.0';
851
+ delete packageJson.bin;
852
+ delete packageJson.peerDependencies;
853
+
854
+ packageJson.scripts = {
855
+ ...packageJson.scripts,
856
+ "dev": "lapeeh dev",
857
+ "start": "lapeeh start",
858
+ "build": "lapeeh build",
859
+ "start:prod": "lapeeh start"
860
+ };
861
+
862
+ // Remove scripts that depend on the scripts folder
863
+ const scriptsToRemove = ['first', 'generate:jwt', 'make:module', 'make:modul', 'config:clear', 'release'];
864
+ scriptsToRemove.forEach(script => {
865
+ delete packageJson.scripts[script];
866
+ });
867
+
868
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
869
+
870
+ // Update tsconfig.json for aliases
871
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
872
+ if (fs.existsSync(tsconfigPath)) {
873
+ try {
874
+ const tsconfig = require(tsconfigPath);
875
+ if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
876
+ if (!tsconfig.compilerOptions.paths) tsconfig.compilerOptions.paths = {};
877
+
878
+ // Ensure @lapeeh/* points to the installed package
879
+ tsconfig.compilerOptions.paths["@lapeeh/*"] = ["./node_modules/@lapeeh/lapeeh/dist/lib/*"];
880
+
881
+ // Add ts-node configuration to allow compiling lapeeh in node_modules
882
+ tsconfig["ts-node"] = {
883
+ "ignore": ["node_modules/(?!@lapeeh)"]
884
+ };
885
+
886
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
887
+ } catch (e) {
888
+ console.warn('⚠️ Failed to update tsconfig.json aliases.');
889
+ }
890
+ }
891
+
892
+ // Removed Prisma base file handling
893
+
894
+ try {
895
+ await spin('Installing dependencies...', async () => {
896
+ await runCommand('npm install', projectDir);
897
+ });
898
+ } catch (e) {
899
+ console.error('❌ Error installing dependencies.');
900
+ console.error(e.message);
901
+ process.exit(1);
902
+ }
903
+
904
+ try {
905
+ // Inline JWT Generation Logic
906
+ const crypto = require('crypto');
907
+ const secret = crypto.randomBytes(64).toString('hex');
908
+
909
+ let envContent = '';
910
+ if (fs.existsSync(envPath)) {
911
+ envContent = fs.readFileSync(envPath, 'utf8');
912
+ }
913
+
914
+ if (envContent.match(/^JWT_SECRET=/m)) {
915
+ envContent = envContent.replace(/^JWT_SECRET=.*/m, `JWT_SECRET="${secret}"`);
916
+ } else {
917
+ if (envContent && !envContent.endsWith('\n')) {
918
+ envContent += '\n';
919
+ }
920
+ envContent += `JWT_SECRET="${secret}"\n`;
921
+ }
922
+
923
+ fs.writeFileSync(envPath, envContent);
924
+ console.log('✅ JWT Secret generated.');
925
+ } catch (e) {
926
+ console.warn('⚠️ Failed to generate JWT secret automatically.');
927
+ }
928
+
929
+ // Removed Prisma setup steps
930
+
931
+ console.log(`\n✅ Project ${projectName} created successfully!`);
932
+ rl.close();
933
+ })();
934
+ }