create-lumina-project 1.0.1 → 1.2.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 (33) hide show
  1. package/bin/cli.js +106 -28
  2. package/package.json +3 -2
  3. package/template/.env.example +1 -0
  4. package/template/package.json +61 -55
  5. package/template/public/js/status.js +53 -0
  6. package/template/scripts/create-migration.ts +457 -16
  7. package/template/server.ts +50 -8
  8. package/template/src/config/database.ts +11 -13
  9. package/template/src/config/env.ts +54 -0
  10. package/template/src/controllers/AuthController.ts +30 -0
  11. package/template/src/database/migrations/20260222151300-create_refresh_tokens.js +63 -0
  12. package/template/src/database/migrations/20260420050854-create_users.js +68 -0
  13. package/template/src/database/migrations/20260420050855-create_refresh_tokens.js +60 -0
  14. package/template/src/exceptions/Handler.ts +10 -3
  15. package/template/src/middlewares/Authentication.ts +2 -5
  16. package/template/src/middlewares/Csrf.ts +52 -0
  17. package/template/src/middlewares/Maintenance.ts +2 -4
  18. package/template/src/middlewares/RequestLogger.ts +31 -0
  19. package/template/src/models/RefreshToken.ts +69 -0
  20. package/template/src/models/index.ts +4 -6
  21. package/template/src/routes/api.ts +5 -0
  22. package/template/src/routes/web.ts +34 -5
  23. package/template/src/services/AuthService.ts +101 -9
  24. package/template/src/services/StorageService.ts +36 -9
  25. package/template/src/tests/apiResponse.test.ts +66 -0
  26. package/template/src/tests/env.test.ts +99 -0
  27. package/template/src/tests/hash.test.ts +30 -0
  28. package/template/src/utils/Logger.ts +16 -6
  29. package/template/views/404.html +233 -0
  30. package/template/views/maintenance.html +445 -23
  31. package/template/views/status.html +545 -0
  32. package/template/views/welcome.html +472 -39
  33. package/template/vitest.config.ts +10 -0
package/bin/cli.js CHANGED
@@ -3,70 +3,148 @@
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
5
5
  import prompts from 'prompts';
6
- import { bold, green, cyan, red } from 'kleur/colors';
6
+ import { bold, green, cyan, red, yellow } from 'kleur/colors';
7
7
  import { fileURLToPath } from 'url';
8
+ import { exec } from 'child_process';
9
+ import util from 'util';
10
+ import ora from 'ora';
11
+
12
+ const execAsync = util.promisify(exec);
8
13
 
9
14
  // Get current directory of the script
10
15
  const __filename = fileURLToPath(import.meta.url);
11
16
  const __dirname = path.dirname(__filename);
12
17
 
13
18
  async function init() {
14
- console.log(bold(cyan('\nšŸš€ Welcome to the Lumina Installer!')));
19
+ console.log(`\n${bold(cyan('šŸš€ Welcome to the Lumina Installer!'))}\n`);
15
20
 
16
21
  // 1. Ask for Project Name
17
22
  let targetDir = process.argv[2];
18
23
 
19
24
  if (!targetDir) {
20
- const response = await prompts({
21
- type: 'text',
22
- name: 'projectName',
23
- message: 'What is your project name?',
24
- initial: 'my-lumina-app'
25
- });
26
-
27
- if (!response.projectName) {
28
- console.log(red('āœ– Operation cancelled'));
29
- process.exit(1);
30
- }
31
- targetDir = response.projectName;
25
+ const response = await prompts(
26
+ {
27
+ type: 'text',
28
+ name: 'projectName',
29
+ message: 'What is your project name?',
30
+ initial: 'my-lumina-app',
31
+ validate: (name) => {
32
+ if (!name.trim()) return 'Project name cannot be empty.';
33
+ if (!/^[a-z0-9-_]+$/.test(name)) {
34
+ return 'Project name can only contain lowercase letters, numbers, dashes, and underscores.';
35
+ }
36
+ return true;
37
+ }
38
+ },
39
+ {
40
+ onCancel: () => {
41
+ console.log(red('āœ– Operation cancelled'));
42
+ process.exit(1);
43
+ }
44
+ }
45
+ );
46
+
47
+ targetDir = response.projectName.trim();
32
48
  }
33
49
 
34
- const projectPath = path.join(process.cwd(), targetDir);
50
+ const projectPath = path.resolve(process.cwd(), targetDir);
35
51
 
36
52
  // 2. Check if folder exists
37
53
  if (fs.existsSync(projectPath)) {
38
54
  console.log(red(`\nāœ– Error: Directory "${targetDir}" already exists.`));
39
55
  process.exit(1);
40
56
  }
57
+
58
+ // 3. Prompt for extra setups (git and npm install)
59
+ const setupPreferences = await prompts(
60
+ [
61
+ {
62
+ type: 'confirm',
63
+ name: 'initGit',
64
+ message: 'Initialize a new git repository?',
65
+ initial: true
66
+ },
67
+ {
68
+ type: 'confirm',
69
+ name: 'installDeps',
70
+ message: 'Install dependencies automatically?',
71
+ initial: true
72
+ }
73
+ ],
74
+ {
75
+ onCancel: () => {
76
+ console.log(red('āœ– Operation cancelled'));
77
+ process.exit(1);
78
+ }
79
+ }
80
+ );
41
81
 
42
- console.log(`\nšŸ“‚ Creating project in ${green(projectPath)}...`);
82
+ console.log(''); // Blank space for readability
43
83
 
44
- // 3. Copy Template
84
+ const copySpinner = ora(`šŸ“‚ Creating project in ${green(projectPath)}...`).start();
85
+
86
+ // 4. Copy Template
45
87
  const templateDir = path.resolve(__dirname, '../template');
46
88
 
47
89
  try {
48
90
  await fs.copy(templateDir, projectPath);
49
91
 
50
- // 4. Rename _gitignore to .gitignore (npm usually strips .gitignore from published packages)
92
+ // 5. Rename _gitignore to .gitignore
51
93
  const gitignorePath = path.join(projectPath, '_gitignore');
52
94
  if (fs.existsSync(gitignorePath)) {
53
95
  fs.renameSync(gitignorePath, path.join(projectPath, '.gitignore'));
54
96
  }
55
97
 
56
- // 5. Update package.json name
98
+ // 6. Update package.json name
57
99
  const pkgPath = path.join(projectPath, 'package.json');
58
- const pkg = await fs.readJson(pkgPath);
59
- pkg.name = targetDir;
60
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
100
+ if (fs.existsSync(pkgPath)) {
101
+ const pkg = await fs.readJson(pkgPath);
102
+ pkg.name = path.basename(projectPath);
103
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
104
+ }
61
105
 
62
- console.log(green('\nāœ… Project setup complete!'));
63
- console.log('\nTo get started run:');
64
- console.log(cyan(` cd ${targetDir}`));
65
- console.log(cyan(' npm install'));
66
- console.log(cyan(' npm run dev'));
106
+ copySpinner.succeed(green('Project template copied successfully!'));
107
+
108
+ // 7. Initialize Git
109
+ if (setupPreferences.initGit) {
110
+ const gitSpinner = ora('Initializing git repository...').start();
111
+ try {
112
+ await execAsync('git init', { cwd: projectPath });
113
+ // Optional: add initial commit but usually people prefer to do it themselves or standard tooling just inits
114
+ gitSpinner.succeed(green('Git repository initialized.'));
115
+ } catch (error) {
116
+ gitSpinner.fail(yellow('Failed to initialize git repository (is git installed?)'));
117
+ }
118
+ }
119
+
120
+ // 8. Install Dependencies
121
+ if (setupPreferences.installDeps) {
122
+ const depsSpinner = ora('Installing dependencies (this might take a few minutes)...').start();
123
+ try {
124
+ await execAsync('npm install', { cwd: projectPath });
125
+ depsSpinner.succeed(green('Dependencies installed successfully!'));
126
+ } catch (error) {
127
+ depsSpinner.fail(red('Failed to install dependencies. You can run "npm install" manually later.'));
128
+ }
129
+ }
130
+
131
+ // 9. Success message & Next steps
132
+ console.log(`\n${bold(green('āœ… All done! Project setup complete.'))}`);
133
+ console.log('\nTo get started run:\n');
134
+
135
+ const cdCmd = projectPath === process.cwd() ? '' : `cd ${targetDir}`;
136
+ if (cdCmd) console.log(cyan(` ${cdCmd}`));
137
+
138
+ if (!setupPreferences.installDeps) {
139
+ console.log(cyan(' npm install'));
140
+ }
141
+
142
+ console.log(cyan(' npm run dev\n'));
67
143
 
68
144
  } catch (error) {
69
- console.error(red('Error copying files:'), error);
145
+ copySpinner.fail(red('Error copying files:'));
146
+ console.error(error);
147
+ process.exit(1);
70
148
  }
71
149
  }
72
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumina-project",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "author": "Glenson Ansin",
5
5
  "description": "Scaffold a modern Node.js API with Lumina architecture.",
6
6
  "type": "module",
@@ -12,8 +12,9 @@
12
12
  "template"
13
13
  ],
14
14
  "dependencies": {
15
- "fs-extra": "^11.3.3",
15
+ "fs-extra": "^11.3.4",
16
16
  "kleur": "^4.1.5",
17
+ "ora": "^9.3.0",
17
18
  "prompts": "^2.4.2"
18
19
  },
19
20
  "license": "MIT",
@@ -17,6 +17,7 @@ JWT_EXPIRES_IN=1d
17
17
  # Application
18
18
  NODE_ENV=development
19
19
  APP_PORT=3000
20
+ CORS_ORIGIN=http://localhost:3000
20
21
 
21
22
  # Logging
22
23
  LOG_LEVEL=info
@@ -1,55 +1,61 @@
1
- {
2
- "name": "lumina",
3
- "version": "1.0.0",
4
- "description": "",
5
- "main": "server.ts",
6
- "scripts": {
7
- "dev": "nodemon --exec tsx server.ts",
8
- "build": "tsc",
9
- "start": "node dist/server.js",
10
- "test": "echo \"Error: no test specified\" && exit 1",
11
- "migrate": "tsx scripts/migrate.ts up",
12
- "migrate:undo": "tsx scripts/migrate.ts down",
13
- "migrate:reset": "tsx scripts/migrate.ts reset",
14
- "db:seed": "tsx scripts/seed.ts",
15
- "down": "tsx scripts/maintenance.ts down",
16
- "up": "tsx scripts/maintenance.ts up",
17
- "create:model": "tsx scripts/create-model.ts",
18
- "create:migration": "tsx scripts/create-migration.ts",
19
- "create:controller": "tsx scripts/create-controller.ts",
20
- "create:factory": "tsx scripts/create-factory.ts",
21
- "key:generate": "tsx scripts/generate-keys.ts"
22
- },
23
- "private": true,
24
- "type": "module",
25
- "dependencies": {
26
- "@faker-js/faker": "^10.2.0",
27
- "bcrypt": "^6.0.0",
28
- "bcryptjs": "^3.0.3",
29
- "cors": "^2.8.6",
30
- "dotenv": "^17.2.3",
31
- "express": "^5.2.1",
32
- "express-rate-limit": "^8.2.1",
33
- "helmet": "^8.1.0",
34
- "jsonwebtoken": "^9.0.3",
35
- "multer": "^2.0.2",
36
- "mysql2": "^3.16.2",
37
- "nodemon": "^3.1.11",
38
- "pg": "^8.18.0",
39
- "pg-hstore": "^2.3.4",
40
- "sequelize": "^6.37.7",
41
- "sequelize-cli": "^6.6.5",
42
- "umzug": "^3.8.2",
43
- "winston": "^3.19.0",
44
- "zod": "^4.3.6"
45
- },
46
- "devDependencies": {
47
- "@types/bcrypt": "^6.0.0",
48
- "@types/cors": "^2.8.19",
49
- "@types/express": "^5.0.6",
50
- "@types/jsonwebtoken": "^9.0.10",
51
- "@types/multer": "^2.0.0",
52
- "tsx": "^4.21.0",
53
- "typescript": "^5.9.3"
54
- }
55
- }
1
+ {
2
+ "name": "lumina",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "server.ts",
6
+ "scripts": {
7
+ "dev": "nodemon --exec tsx server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "test": "vitest run",
11
+ "migrate": "tsx scripts/migrate.ts up",
12
+ "migrate:seed": "tsx scripts/migrate.ts up && tsx scripts/seed.ts",
13
+ "migrate:undo": "tsx scripts/migrate.ts down",
14
+ "migrate:reset": "tsx scripts/migrate.ts reset",
15
+ "db:seed": "tsx scripts/seed.ts",
16
+ "down": "tsx scripts/maintenance.ts down",
17
+ "up": "tsx scripts/maintenance.ts up",
18
+ "create:model": "tsx scripts/create-model.ts",
19
+ "create:migration": "tsx scripts/create-migration.ts",
20
+ "create:controller": "tsx scripts/create-controller.ts",
21
+ "create:factory": "tsx scripts/create-factory.ts",
22
+ "key:generate": "tsx scripts/generate-keys.ts"
23
+ },
24
+ "private": true,
25
+ "type": "module",
26
+ "dependencies": {
27
+ "bcrypt": "^6.0.0",
28
+ "bcryptjs": "^3.0.3",
29
+ "compression": "^1.8.1",
30
+ "cookie-parser": "^1.4.7",
31
+ "cors": "^2.8.6",
32
+ "dotenv": "^17.4.2",
33
+ "express": "^5.2.1",
34
+ "express-rate-limit": "^8.3.2",
35
+ "helmet": "^8.1.0",
36
+ "jsonwebtoken": "^9.0.3",
37
+ "multer": "^2.1.1",
38
+ "mysql2": "^3.22.1",
39
+ "nodemon": "^3.1.14",
40
+ "pg": "^8.20.0",
41
+ "pg-hstore": "^2.3.4",
42
+ "sequelize": "^6.37.8",
43
+ "sequelize-cli": "^6.6.5",
44
+ "umzug": "^3.8.2",
45
+ "winston": "^3.19.0",
46
+ "zod": "^4.3.6"
47
+ },
48
+ "devDependencies": {
49
+ "@faker-js/faker": "^10.4.0",
50
+ "@types/bcrypt": "^6.0.0",
51
+ "@types/compression": "^1.8.1",
52
+ "@types/cookie-parser": "^1.4.10",
53
+ "@types/cors": "^2.8.19",
54
+ "@types/express": "^5.0.6",
55
+ "@types/jsonwebtoken": "^9.0.10",
56
+ "@types/multer": "^2.1.0",
57
+ "tsx": "^4.21.0",
58
+ "typescript": "^6.0.3",
59
+ "vitest": "^4.1.4"
60
+ }
61
+ }
@@ -0,0 +1,53 @@
1
+ async function fetchStatus() {
2
+ try {
3
+ const res = await fetch('/status/json');
4
+ const data = await res.json();
5
+
6
+ // Status
7
+ const heroEl = document.getElementById('statusHero');
8
+ const heroText = document.getElementById('statusHeroText');
9
+ const appStatus = document.getElementById('appStatus');
10
+ const isUp = data.status === 'UP';
11
+
12
+ heroEl.className = 'status-hero ' + (isUp ? 'up' : 'down');
13
+ heroText.textContent = isUp ? 'All Systems Operational' : 'System Degraded';
14
+ appStatus.textContent = data.status;
15
+ appStatus.className = 'info-value ' + (isUp ? 'status-up' : 'status-down');
16
+
17
+ // Environment
18
+ document.getElementById('appEnv').textContent = data.environment || 'unknown';
19
+
20
+ // Uptime
21
+ if (data.uptime !== undefined) {
22
+ document.getElementById('appUptime').textContent = formatUptime(data.uptime);
23
+ }
24
+
25
+ // Memory
26
+ if (data.memoryMB !== undefined) {
27
+ document.getElementById('appMemory').textContent = data.memoryMB + ' MB';
28
+ }
29
+
30
+ // Timestamp
31
+ document.getElementById('lastChecked').textContent = 'Checked ' + new Date().toLocaleTimeString();
32
+ } catch (err) {
33
+ document.getElementById('statusHero').className = 'status-hero down';
34
+ document.getElementById('statusHeroText').textContent = 'Unable to Reach Server';
35
+ }
36
+ }
37
+
38
+ function formatUptime(seconds) {
39
+ const d = Math.floor(seconds / 86400);
40
+ const h = Math.floor((seconds % 86400) / 3600);
41
+ const m = Math.floor((seconds % 3600) / 60);
42
+ const s = Math.floor(seconds % 60);
43
+ const parts = [];
44
+ if (d > 0) parts.push(d + 'd');
45
+ if (h > 0) parts.push(h + 'h');
46
+ if (m > 0) parts.push(m + 'm');
47
+ parts.push(s + 's');
48
+ return parts.join(' ');
49
+ }
50
+
51
+ // Fetch immediately, then refresh every 30s
52
+ fetchStatus();
53
+ setInterval(fetchStatus, 30000);