@seip/blue-bird 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/cli/init.js CHANGED
@@ -118,6 +118,8 @@ class ProjectInit {
118
118
  "docker",
119
119
  ".env_example",
120
120
  "AGENTS.md",
121
+ "jsconfig.json",
122
+ ".vscode",
121
123
  ];
122
124
 
123
125
  try {
@@ -333,7 +335,10 @@ class ProjectInit {
333
335
  dev: "node --watch --env-file=.env backend/index.js",
334
336
  start: "node --env-file=.env backend/index.js",
335
337
  init: "blue-bird",
338
+ doctor: "blue-bird doctor",
336
339
  route: "blue-bird route",
340
+ migrate: "blue-bird migrate",
341
+ seed: "blue-bird seed",
337
342
  "swagger-install": "blue-bird swagger-install",
338
343
  docker: "blue-bird docker",
339
344
  };
@@ -452,9 +457,33 @@ const initializer = new ProjectInit();
452
457
  const args = process.argv.slice(2);
453
458
  const command = args[0];
454
459
 
455
- if (command === "route") import("./route.js");
456
- else if (command === "swagger-install") import("./swagger.js");
457
- else if (command === "docker") import("./docker.js");
458
- else if (command === "add") addCommand(args[1]);
459
- else initializer.run();
460
+ if (command === "route" || command === "make:route") {
461
+ import("./route.js");
462
+ } else if (command === "doctor") {
463
+ import("./doctor.js");
464
+ } else if (
465
+ command === "nginx:conf" ||
466
+ command === "nginx:host" ||
467
+ command === "nginx"
468
+ ) {
469
+ import("./nginx.js");
470
+ } else if (
471
+ command === "migrate" ||
472
+ command === "migrate:status" ||
473
+ command === "migrate:rollback" ||
474
+ command === "seed" ||
475
+ command === "make:migration" ||
476
+ command === "make:seed"
477
+ ) {
478
+ import("./migrate.js");
479
+ } else if (command === "swagger-install") {
480
+ import("./swagger.js");
481
+ } else if (command === "docker") {
482
+ import("./docker.js");
483
+ } else if (command === "add") {
484
+ addCommand(args[1]);
485
+ } else {
486
+ initializer.run();
487
+ }
488
+
460
489
 
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import chalk from "chalk";
6
+ import { Database, DB_TYPE } from "../database.js";
7
+
8
+ /**
9
+ * Generates current timestamp string in YYYYMMDD_HHMMSS format.
10
+ * @returns {string}
11
+ */
12
+ function getTimestamp() {
13
+ const now = new Date();
14
+ const pad = (n) => String(n).padStart(2, "0");
15
+ const year = now.getFullYear();
16
+ const month = pad(now.getMonth() + 1);
17
+ const day = pad(now.getDate());
18
+ const hours = pad(now.getHours());
19
+ const mins = pad(now.getMinutes());
20
+ const secs = pad(now.getSeconds());
21
+ return `${year}${month}${day}_${hours}${mins}${secs}`;
22
+ }
23
+
24
+ /**
25
+ * Creates a new migration file in database/migrations/.
26
+ * @param {string} name
27
+ */
28
+ function makeMigration(name) {
29
+ if (!name) {
30
+ console.log(chalk.red("[ERROR] Missing migration name."));
31
+ console.log("Usage: npx blue-bird make:migration <name>");
32
+ process.exit(1);
33
+ }
34
+
35
+ const cleanName = name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
36
+ const filename = `${getTimestamp()}_${cleanName}.sql`;
37
+ const migrationsDir = path.resolve(process.cwd(), "database/migrations");
38
+
39
+ if (!fs.existsSync(migrationsDir)) {
40
+ fs.mkdirSync(migrationsDir, { recursive: true });
41
+ }
42
+
43
+ const filePath = path.join(migrationsDir, filename);
44
+
45
+ const template = `-- =============================================================
46
+ -- Migration: ${cleanName}
47
+ -- Created At: ${new Date().toISOString()}
48
+ -- Driver Compatibility: SQLite / MySQL / PostgreSQL
49
+ -- =============================================================
50
+
51
+ CREATE TABLE IF NOT EXISTS ${cleanName} (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ name VARCHAR(255) NOT NULL,
54
+ description TEXT,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
56
+ );
57
+ `;
58
+
59
+ fs.writeFileSync(filePath, template, "utf-8");
60
+ console.log(chalk.green(`[OK] Migration created: database/migrations/${filename}`));
61
+ }
62
+
63
+ /**
64
+ * Creates a new seed file in database/seeds/.
65
+ * @param {string} name
66
+ */
67
+ function makeSeed(name) {
68
+ if (!name) {
69
+ console.log(chalk.red("[ERROR] Missing seed name."));
70
+ console.log("Usage: npx blue-bird make:seed <name>");
71
+ process.exit(1);
72
+ }
73
+
74
+ const cleanName = name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
75
+ const filename = `${cleanName}.sql`;
76
+ const seedsDir = path.resolve(process.cwd(), "database/seeds");
77
+
78
+ if (!fs.existsSync(seedsDir)) {
79
+ fs.mkdirSync(seedsDir, { recursive: true });
80
+ }
81
+
82
+ const filePath = path.join(seedsDir, filename);
83
+
84
+ const template = `-- =============================================================
85
+ -- Seed: ${cleanName}
86
+ -- Created At: ${new Date().toISOString()}
87
+ -- =============================================================
88
+
89
+ -- INSERT INTO table_name (name) VALUES ('Sample Item 1');
90
+ `;
91
+
92
+ fs.writeFileSync(filePath, template, "utf-8");
93
+ console.log(chalk.green(`[OK] Seed file created: database/seeds/${filename}`));
94
+ }
95
+
96
+ /**
97
+ * Ensures migrations tracking table exists.
98
+ * @param {Database} db
99
+ */
100
+ async function ensureMigrationsTable(db) {
101
+ let ddl = "";
102
+ if (DB_TYPE === "postgres") {
103
+ ddl = `CREATE TABLE IF NOT EXISTS _bluebird_migrations (
104
+ id SERIAL PRIMARY KEY,
105
+ name VARCHAR(255) NOT NULL UNIQUE,
106
+ batch INT NOT NULL,
107
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
108
+ );`;
109
+ } else if (DB_TYPE === "mysql") {
110
+ ddl = `CREATE TABLE IF NOT EXISTS _bluebird_migrations (
111
+ id INT AUTO_INCREMENT PRIMARY KEY,
112
+ name VARCHAR(255) NOT NULL UNIQUE,
113
+ batch INT NOT NULL,
114
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
115
+ );`;
116
+ } else {
117
+ // sqlite default
118
+ ddl = `CREATE TABLE IF NOT EXISTS _bluebird_migrations (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ name TEXT NOT NULL UNIQUE,
121
+ batch INTEGER NOT NULL,
122
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
123
+ );`;
124
+ }
125
+
126
+ await db.query(ddl);
127
+ }
128
+
129
+ /**
130
+ * Executes pending database migrations.
131
+ */
132
+ async function runMigrations() {
133
+ const migrationsDir = path.resolve(process.cwd(), "database/migrations");
134
+ if (!fs.existsSync(migrationsDir)) {
135
+ console.log(chalk.yellow("[INFO] No 'database/migrations' directory found. Nothing to migrate."));
136
+ return;
137
+ }
138
+
139
+ const files = fs
140
+ .readdirSync(migrationsDir)
141
+ .filter((f) => f.endsWith(".sql") || f.endsWith(".js"))
142
+ .sort();
143
+
144
+ if (files.length === 0) {
145
+ console.log(chalk.yellow("[INFO] No migration files found in 'database/migrations'."));
146
+ return;
147
+ }
148
+
149
+ let db;
150
+ try {
151
+ db = new Database(5);
152
+ await ensureMigrationsTable(db);
153
+ } catch (err) {
154
+ console.error(chalk.red("[ERROR] Could not connect to database to run migrations:"), err.message);
155
+ process.exit(1);
156
+ }
157
+
158
+ const appliedRows = (await db.query("SELECT name, batch FROM _bluebird_migrations ORDER BY id ASC")) || [];
159
+ const appliedSet = new Set(appliedRows.map((r) => r.name));
160
+
161
+ const maxBatchRow = await db.query("SELECT MAX(batch) as max_batch FROM _bluebird_migrations", [], "return_row");
162
+ const currentBatch = ((maxBatchRow && maxBatchRow.max_batch) || 0) + 1;
163
+
164
+ const pending = files.filter((f) => !appliedSet.has(f));
165
+
166
+ if (pending.length === 0) {
167
+ console.log(chalk.green("[INFO] Database is up to date. No pending migrations."));
168
+ process.exit(0);
169
+ }
170
+
171
+ console.log(chalk.cyan(`[INFO] Running ${pending.length} pending migration(s) (Batch #${currentBatch})...\n`));
172
+
173
+ for (const file of pending) {
174
+ const filePath = path.join(migrationsDir, file);
175
+ try {
176
+ if (file.endsWith(".sql")) {
177
+ const sql = fs.readFileSync(filePath, "utf-8");
178
+ // Split statements by semicolon where appropriate
179
+ const statements = sql
180
+ .split(/;\s*$/m)
181
+ .map((s) => s.trim())
182
+ .filter((s) => s.length > 0);
183
+
184
+ await db.transaction(async (tx) => {
185
+ for (const stmt of statements) {
186
+ await tx.query(stmt);
187
+ }
188
+ await tx.query(
189
+ "INSERT INTO _bluebird_migrations (name, batch) VALUES (?, ?)",
190
+ [file, currentBatch]
191
+ );
192
+ });
193
+ } else if (file.endsWith(".js")) {
194
+ const modulePath = `file://${filePath}`;
195
+ const migrationModule = await import(modulePath);
196
+ if (typeof migrationModule.up === "function") {
197
+ await db.transaction(async (tx) => {
198
+ await migrationModule.up(tx);
199
+ await tx.query(
200
+ "INSERT INTO _bluebird_migrations (name, batch) VALUES (?, ?)",
201
+ [file, currentBatch]
202
+ );
203
+ });
204
+ }
205
+ }
206
+
207
+ console.log(chalk.green(` [MIGRATED] ${file}`));
208
+ } catch (err) {
209
+ console.error(chalk.red(` [FAILED] ${file}: ${err.message}`));
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ console.log(chalk.bold.green("\n[OK] All pending migrations executed successfully."));
215
+ process.exit(0);
216
+ }
217
+
218
+ /**
219
+ * Shows migration status list.
220
+ */
221
+ async function showMigrationStatus() {
222
+ const migrationsDir = path.resolve(process.cwd(), "database/migrations");
223
+ if (!fs.existsSync(migrationsDir)) {
224
+ console.log(chalk.yellow("[INFO] No 'database/migrations' directory found."));
225
+ return;
226
+ }
227
+
228
+ const files = fs
229
+ .readdirSync(migrationsDir)
230
+ .filter((f) => f.endsWith(".sql") || f.endsWith(".js"))
231
+ .sort();
232
+
233
+ let db;
234
+ try {
235
+ db = new Database(5);
236
+ await ensureMigrationsTable(db);
237
+ } catch (err) {
238
+ console.error(chalk.red("[ERROR] Could not connect to database:"), err.message);
239
+ process.exit(1);
240
+ }
241
+
242
+ const appliedRows = (await db.query("SELECT name, batch, executed_at FROM _bluebird_migrations ORDER BY id ASC")) || [];
243
+ const appliedMap = new Map(appliedRows.map((r) => [r.name, r]));
244
+
245
+ console.log(chalk.bold.cyan("\n============================================================="));
246
+ console.log(chalk.bold.cyan(" Database Migrations Status"));
247
+ console.log(chalk.bold.cyan("=============================================================\n"));
248
+
249
+ for (const file of files) {
250
+ if (appliedMap.has(file)) {
251
+ const record = appliedMap.get(file);
252
+ console.log(` ${chalk.green("[APPLIED]")} ${file.padEnd(40)} (Batch: ${record.batch}, At: ${record.executed_at})`);
253
+ } else {
254
+ console.log(` ${chalk.yellow("[PENDING]")} ${file}`);
255
+ }
256
+ }
257
+
258
+ console.log("");
259
+ process.exit(0);
260
+ }
261
+
262
+ /**
263
+ * Runs seed scripts from database/seeds/.
264
+ */
265
+ async function runSeeds() {
266
+ const seedsDir = path.resolve(process.cwd(), "database/seeds");
267
+ if (!fs.existsSync(seedsDir)) {
268
+ console.log(chalk.yellow("[INFO] No 'database/seeds' directory found. Nothing to seed."));
269
+ return;
270
+ }
271
+
272
+ const files = fs
273
+ .readdirSync(seedsDir)
274
+ .filter((f) => f.endsWith(".sql") || f.endsWith(".js"))
275
+ .sort();
276
+
277
+ if (files.length === 0) {
278
+ console.log(chalk.yellow("[INFO] No seed files found in 'database/seeds'."));
279
+ return;
280
+ }
281
+
282
+ let db;
283
+ try {
284
+ db = new Database(5);
285
+ } catch (err) {
286
+ console.error(chalk.red("[ERROR] Could not connect to database to run seeds:"), err.message);
287
+ process.exit(1);
288
+ }
289
+
290
+ console.log(chalk.cyan(`[INFO] Running ${files.length} seed file(s)...\n`));
291
+
292
+ for (const file of files) {
293
+ const filePath = path.join(seedsDir, file);
294
+ try {
295
+ if (file.endsWith(".sql")) {
296
+ const sql = fs.readFileSync(filePath, "utf-8");
297
+ const statements = sql
298
+ .split(/;\s*$/m)
299
+ .map((s) => s.trim())
300
+ .filter((s) => s.length > 0);
301
+
302
+ await db.transaction(async (tx) => {
303
+ for (const stmt of statements) {
304
+ await tx.query(stmt);
305
+ }
306
+ });
307
+ } else if (file.endsWith(".js")) {
308
+ const modulePath = `file://${filePath}`;
309
+ const seedModule = await import(modulePath);
310
+ if (typeof seedModule.seed === "function") {
311
+ await db.transaction(async (tx) => {
312
+ await seedModule.seed(tx);
313
+ });
314
+ }
315
+ }
316
+
317
+ console.log(chalk.green(` [SEEDED] ${file}`));
318
+ } catch (err) {
319
+ console.error(chalk.red(` [FAILED] ${file}: ${err.message}`));
320
+ process.exit(1);
321
+ }
322
+ }
323
+
324
+ console.log(chalk.bold.green("\n[OK] Database seeding completed."));
325
+ process.exit(0);
326
+ }
327
+
328
+ // CLI Dispatcher
329
+ const rawArgs = process.argv.slice(2);
330
+ const cmd = rawArgs[0];
331
+
332
+ if (cmd === "make:migration") {
333
+ makeMigration(rawArgs[1]);
334
+ } else if (cmd === "make:seed") {
335
+ makeSeed(rawArgs[1]);
336
+ } else if (cmd === "migrate:status") {
337
+ showMigrationStatus();
338
+ } else if (cmd === "seed") {
339
+ runSeeds();
340
+ } else {
341
+ runMigrations();
342
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import chalk from "chalk";
6
+
7
+ /**
8
+ * Parses .env file to extract APP_URL and PORT.
9
+ * @returns {{ domain: string, port: number, appUrl: string }}
10
+ */
11
+ function getEnvConfig() {
12
+ const env = { ...process.env };
13
+ const envPath = path.resolve(process.cwd(), ".env");
14
+ if (fs.existsSync(envPath)) {
15
+ const content = fs.readFileSync(envPath, "utf-8");
16
+ content.split(/\r?\n/).forEach((line) => {
17
+ line = line.trim();
18
+ if (line && !line.startsWith("#") && line.includes("=")) {
19
+ const idx = line.indexOf("=");
20
+ const key = line.substring(0, idx).trim();
21
+ const value = line.substring(idx + 1).trim().replace(/^['"]|['"]$/g, "");
22
+ env[key] = value;
23
+ }
24
+ });
25
+ }
26
+
27
+ let domain = "";
28
+ if (env.APP_URL) {
29
+ try {
30
+ const parsedUrl = new URL(env.APP_URL.includes("://") ? env.APP_URL : `http://${env.APP_URL}`);
31
+ domain = parsedUrl.hostname;
32
+ } catch {
33
+ domain = env.APP_URL.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "").trim();
34
+ }
35
+ }
36
+
37
+ const port = parseInt(env.PORT || "3000", 10);
38
+ return { domain, port, appUrl: env.APP_URL || "" };
39
+ }
40
+
41
+ /**
42
+ * Generates Host Nginx reverse proxy configuration snippet.
43
+ */
44
+ function generateNginxConfig() {
45
+ const rawArgs = process.argv.slice(2);
46
+ const filteredArgs = rawArgs.filter(
47
+ (a) => a !== "nginx:conf" && a !== "nginx:host" && a !== "nginx" && !a.endsWith("nginx.js") && !a.endsWith("init.js")
48
+ );
49
+
50
+ const envConfig = getEnvConfig();
51
+
52
+ let domainArg = filteredArgs.find((a) => !a.startsWith("-") && isNaN(Number(a)));
53
+ let portArg = filteredArgs.find((a) => !isNaN(Number(a)));
54
+
55
+ let domain = domainArg || envConfig.domain;
56
+ let port = portArg ? parseInt(portArg, 10) : envConfig.port;
57
+
58
+ if (!domain) {
59
+ domain = "example.com";
60
+ console.log(chalk.yellow("[INFO] No domain provided and APP_URL is not configured in .env. Defaulting to 'example.com'."));
61
+ } else if (!domainArg && envConfig.domain) {
62
+ console.log(chalk.cyan(`[INFO] Using domain '${domain}' and port ${port} resolved from .env (APP_URL / PORT).`));
63
+ console.log(chalk.gray(` (You can override via: npx blue-bird nginx:conf <domain> [port])\n`));
64
+ }
65
+
66
+ // Clean domain name (strip protocol, path, port)
67
+ domain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "").trim();
68
+
69
+ const nginxSnippet = `# =============================================================
70
+ # Blue Bird Host Nginx Reverse Proxy Configuration
71
+ # File: /etc/nginx/sites-available/${domain}
72
+ # =============================================================
73
+
74
+ server {
75
+ listen 80;
76
+ listen [::]:80;
77
+ server_name ${domain};
78
+
79
+ # Maximum request body size for file uploads
80
+ client_max_body_size 50M;
81
+
82
+ # Security: Block hidden files and scan attempts
83
+ location ~* /\\.(env|git) {
84
+ deny all;
85
+ return 404;
86
+ }
87
+
88
+ # Reverse proxy to Blue Bird container stack
89
+ location / {
90
+ proxy_pass http://127.0.0.1:${port};
91
+ proxy_http_version 1.1;
92
+
93
+ # WebSocket upgrade support
94
+ proxy_set_header Upgrade $http_upgrade;
95
+ proxy_set_header Connection "upgrade";
96
+
97
+ # Standard proxy headers
98
+ proxy_set_header Host $host;
99
+ proxy_set_header X-Real-IP $remote_addr;
100
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
101
+ proxy_set_header X-Forwarded-Proto $scheme;
102
+ proxy_set_header X-Forwarded-Host $host;
103
+ proxy_set_header X-Forwarded-Port $server_port;
104
+
105
+ # Proxy timeouts
106
+ proxy_connect_timeout 60s;
107
+ proxy_send_timeout 60s;
108
+ proxy_read_timeout 60s;
109
+ }
110
+ }`;
111
+
112
+ console.log(chalk.bold.cyan("============================================================="));
113
+ console.log(chalk.bold.cyan(` Host Nginx Configuration & Let's Encrypt Setup for: ${domain}`));
114
+ console.log(chalk.bold.cyan("============================================================="));
115
+ console.log("");
116
+ console.log(chalk.yellow("1. Create the site configuration file on your VPS:"));
117
+ console.log(chalk.green(` sudo nano /etc/nginx/sites-available/${domain}`));
118
+ console.log("");
119
+ console.log(chalk.yellow("2. Paste the following configuration block:"));
120
+ console.log("");
121
+ console.log(chalk.white(nginxSnippet));
122
+ console.log("");
123
+ console.log(chalk.yellow("3. Enable the site configuration:"));
124
+ console.log(chalk.green(` sudo ln -s /etc/nginx/sites-available/${domain} /etc/nginx/sites-enabled/`));
125
+ console.log("");
126
+ console.log(chalk.yellow("4. Test Nginx syntax:"));
127
+ console.log(chalk.green(" sudo nginx -t"));
128
+ console.log("");
129
+ console.log(chalk.yellow("5. Reload Nginx to apply changes:"));
130
+ console.log(chalk.green(" sudo systemctl reload nginx"));
131
+ console.log("");
132
+ console.log(chalk.yellow("6. Provision free SSL certificate with Certbot (Let's Encrypt):"));
133
+ console.log(chalk.green(` sudo certbot --nginx -d ${domain}`));
134
+ console.log("");
135
+ console.log(chalk.bold.cyan("============================================================="));
136
+ }
137
+
138
+ generateNginxConfig();