@seip/blue-bird 1.1.0 → 1.1.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.
@@ -26,11 +26,14 @@ function getEnvVars() {
26
26
  }
27
27
 
28
28
  /**
29
- * Determines the target database type (mysql or postgres) from environment variables.
29
+ * Determines the target database type (sqlite, mysql, or postgres) from environment variables.
30
30
  * @param {Object} [env] - Environment dictionary.
31
- * @returns {string} 'postgres' or 'mysql'.
31
+ * @returns {string} 'sqlite', 'postgres', 'mysql', or 'none'.
32
32
  */
33
33
  function getDbType(env = getEnvVars()) {
34
+ if (env.DB_TYPE && (env.DB_TYPE.toLowerCase() === "sqlite" || env.DB_TYPE.toLowerCase() === "sqlite3")) {
35
+ return "sqlite";
36
+ }
34
37
  if (env.DB_TYPE && (env.DB_TYPE.toLowerCase() === "postgres" || env.DB_TYPE.toLowerCase() === "postgresql" || env.DB_TYPE.toLowerCase() === "pg")) {
35
38
  return "postgres";
36
39
  }
@@ -41,6 +44,9 @@ function getDbType(env = getEnvVars()) {
41
44
  return "none";
42
45
  }
43
46
  if (env.DATABASE_URL && !env.DATABASE_URL.startsWith("#")) {
47
+ if (env.DATABASE_URL.startsWith("sqlite://") || env.DATABASE_URL.startsWith("sqlite:")) {
48
+ return "sqlite";
49
+ }
44
50
  if (env.DATABASE_URL.startsWith("postgres://") || env.DATABASE_URL.startsWith("postgresql://")) {
45
51
  return "postgres";
46
52
  }
@@ -48,7 +54,7 @@ function getDbType(env = getEnvVars()) {
48
54
  return "mysql";
49
55
  }
50
56
  }
51
- return "mysql";
57
+ return "sqlite";
52
58
  }
53
59
 
54
60
  /**
@@ -84,12 +90,23 @@ async function startCommand(service) {
84
90
  checkComposeFile();
85
91
  const dbType = getDbType();
86
92
 
87
- if (service === "mysql" || service === "--mysql" || service === "postgres" || service === "--postgres" || service === "db" || service === "--db" || service === "dev") {
88
- const targetContainer = (service === "postgres" || service === "--postgres") ? "postgres" : ((service === "mysql" || service === "--mysql") ? "mysql" : dbType);
93
+ if (service === "sqlite" || service === "--sqlite" || service === "mysql" || service === "--mysql" || service === "postgres" || service === "--postgres" || service === "db" || service === "--db" || service === "dev") {
94
+ const targetContainer = (service === "sqlite" || service === "--sqlite") ? "sqlite" : ((service === "postgres" || service === "--postgres") ? "postgres" : ((service === "mysql" || service === "--mysql") ? "mysql" : dbType));
89
95
  if (targetContainer === "none") {
90
96
  console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', skipping database container startup."));
91
97
  return;
92
98
  }
99
+ if (targetContainer === "sqlite") {
100
+ console.log(chalk.cyan("[INFO] SQLite is file-based (embedded in app). Starting Redis container..."));
101
+ const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
102
+ if (code === 0) {
103
+ console.log(chalk.green("Redis started."));
104
+ } else {
105
+ console.error(chalk.red("Error starting Redis."));
106
+ process.exit(1);
107
+ }
108
+ return;
109
+ }
93
110
  console.log(chalk.cyan(`Starting ${targetContainer.toUpperCase()} container...`));
94
111
  const code = await runCmd("docker", ["compose", "up", "-d", targetContainer]);
95
112
  if (code === 0) {
@@ -108,8 +125,8 @@ async function startCommand(service) {
108
125
  process.exit(1);
109
126
  }
110
127
  } else if (service === "dbs" || service === "databases") {
111
- if (dbType === "none") {
112
- console.log(chalk.cyan("DB_TYPE is 'none', starting Redis container only..."));
128
+ if (dbType === "none" || dbType === "sqlite") {
129
+ console.log(chalk.cyan(`DB_TYPE is '${dbType}' (embedded/none), starting Redis container only...`));
113
130
  const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
114
131
  if (code === 0) {
115
132
  console.log(chalk.green("Redis started."));
@@ -137,7 +154,7 @@ async function startCommand(service) {
137
154
  process.exit(1);
138
155
  }
139
156
  } else {
140
- console.error(chalk.red(`Unknown service '${service}'. Use: mysql, postgres, db, redis, dbs, prod.`));
157
+ console.error(chalk.red(`Unknown service '${service}'. Use: sqlite, mysql, postgres, db, redis, dbs, prod.`));
141
158
  process.exit(1);
142
159
  }
143
160
  }
@@ -151,14 +168,16 @@ async function devCommand() {
151
168
  console.log(chalk.cyan("Starting development environment (Database + Redis)..."));
152
169
 
153
170
  const containers = ["redis"];
154
- if (dbType !== "none") {
171
+ if (dbType !== "none" && dbType !== "sqlite") {
155
172
  containers.unshift(dbType);
156
173
  }
157
174
 
158
175
  const code = await runCmd("docker", ["compose", "up", "-d", ...containers]);
159
176
  if (code === 0) {
160
177
  console.log(chalk.green("Development containers started."));
161
- console.log(chalk.cyan("View live logs with: npx blue-bird docker logs db"));
178
+ if (dbType !== "none" && dbType !== "sqlite") {
179
+ console.log(chalk.cyan("View live logs with: npx blue-bird docker logs db"));
180
+ }
162
181
  console.log(chalk.yellow("Now execute: npm run dev"));
163
182
  } else {
164
183
  console.error(chalk.red("Error starting development environment."));
@@ -178,10 +197,10 @@ async function stopCommand(service) {
178
197
  console.log(chalk.cyan("Stopping all Blue Bird containers..."));
179
198
  await runCmd("docker", ["compose", "--profile", "prod", "down"]);
180
199
  console.log(chalk.green("All containers stopped."));
181
- } else if (service === "mysql" || service === "--mysql" || service === "postgres" || service === "--postgres" || service === "db" || service === "--db") {
182
- const targetContainer = (service === "postgres" || service === "--postgres") ? "postgres" : ((service === "mysql" || service === "--mysql") ? "mysql" : dbType);
183
- if (targetContainer === "none") {
184
- console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', no database container to stop."));
200
+ } else if (service === "sqlite" || service === "--sqlite" || service === "mysql" || service === "--mysql" || service === "postgres" || service === "--postgres" || service === "db" || service === "--db") {
201
+ const targetContainer = (service === "sqlite" || service === "--sqlite") ? "sqlite" : ((service === "postgres" || service === "--postgres") ? "postgres" : ((service === "mysql" || service === "--mysql") ? "mysql" : dbType));
202
+ if (targetContainer === "none" || targetContainer === "sqlite") {
203
+ console.log(chalk.yellow(`[INFO] DB_TYPE is set to '${targetContainer}', no database container to stop.`));
185
204
  return;
186
205
  }
187
206
  console.log(chalk.cyan(`Stopping ${targetContainer.toUpperCase()}...`));
@@ -199,7 +218,7 @@ async function stopCommand(service) {
199
218
  await runCmd("docker", ["compose", "--profile", "prod", "rm", "-f", "app"]);
200
219
  console.log(chalk.green("App container stopped."));
201
220
  } else {
202
- console.error(chalk.red(`Unknown service '${service}'. Use: all, mysql, postgres, db, redis, app.`));
221
+ console.error(chalk.red(`Unknown service '${service}'. Use: all, sqlite, mysql, postgres, db, redis, app.`));
203
222
  process.exit(1);
204
223
  }
205
224
  }
@@ -245,13 +264,17 @@ async function logsCommand(service, followOpt) {
245
264
  const follow = followOpt !== "--no-follow";
246
265
  const dbType = getDbType();
247
266
  let targetService = "app";
248
- if (service === "mysql" || service === "postgres" || service === "db") {
267
+ if (service === "mysql" || service === "postgres" || service === "sqlite" || service === "db") {
249
268
  targetService = service === "db" ? dbType : service;
250
269
  }
251
270
  if (targetService === "none") {
252
271
  console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', no database container logs to display."));
253
272
  return;
254
273
  }
274
+ if (targetService === "sqlite") {
275
+ console.log(chalk.yellow("[INFO] SQLite is embedded in the Node.js application container. Displaying app logs:"));
276
+ targetService = "app";
277
+ }
255
278
 
256
279
  const cmdArgs = ["compose"];
257
280
  if (targetService === "app") {
@@ -267,22 +290,14 @@ async function logsCommand(service, followOpt) {
267
290
  }
268
291
 
269
292
  /**
270
- * Handles interactive shell connections into the MySQL or PostgreSQL container.
271
- * @param {string} userOpt - DB username.
272
- * @param {string} passOpt - DB password.
273
- * @param {string} dbOpt - DB database name.
274
- * @param {boolean} rootOpt - Flag for overriding database credentials to connect as root/postgres.
275
- * @param {string} explicitService - Explicit target service if specified ('mysql' or 'postgres').
276
- */
277
- /**
278
- * Handles interactive shell connections or smart queries into the MySQL or PostgreSQL container.
293
+ * Handles interactive shell connections or smart queries into the SQLite, MySQL, or PostgreSQL database.
279
294
  * @param {string[]} clientArgs - CLI arguments.
280
- * @param {string} explicitService - Explicit target service if specified ('mysql', 'postgres', 'psql', 'db').
295
+ * @param {string} explicitService - Explicit target service if specified ('sqlite', 'mysql', 'postgres', 'psql', 'db').
281
296
  */
282
297
  async function dbClientCommand(clientArgs = [], explicitService) {
283
298
  checkComposeFile();
284
299
  const env = getEnvVars();
285
- const dbType = explicitService === "postgres" || explicitService === "psql" ? "postgres" : (explicitService === "mysql" ? "mysql" : getDbType(env));
300
+ const dbType = explicitService === "sqlite" ? "sqlite" : (explicitService === "postgres" || explicitService === "psql" ? "postgres" : (explicitService === "mysql" ? "mysql" : getDbType(env)));
286
301
  if (dbType === "none") {
287
302
  console.error(chalk.yellow("[INFO] DB_TYPE is set to 'none'. No database container available to connect to."));
288
303
  return;
@@ -340,23 +355,27 @@ async function dbClientCommand(clientArgs = [], explicitService) {
340
355
  if (firstPos === "tables") {
341
356
  if (dbType === "postgres") {
342
357
  sqlQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';";
358
+ } else if (dbType === "sqlite") {
359
+ sqlQuery = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';";
343
360
  } else {
344
361
  sqlQuery = "SHOW TABLES;";
345
362
  }
346
363
  } else if (firstPos === "columns" || firstPos === "cols" || firstPos === "describe" || firstPos === "desc") {
347
364
  const tableName = positionalArgs[1];
348
365
  if (!tableName) {
349
- console.error(chalk.red("Error: Please specify a table name. Example: npx blue-bird docker mysql columns users"));
366
+ console.error(chalk.red("Error: Please specify a table name. Example: npx blue-bird docker db columns users"));
350
367
  process.exit(1);
351
368
  }
352
369
  if (dbType === "postgres") {
353
370
  sqlQuery = `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '${tableName}';`;
371
+ } else if (dbType === "sqlite") {
372
+ sqlQuery = `PRAGMA table_info(${tableName});`;
354
373
  } else {
355
374
  sqlQuery = `SHOW COLUMNS FROM ${tableName};`;
356
375
  }
357
376
  } else {
358
377
  const rawInput = positionalArgs.join(" ").trim();
359
- const isFullQuery = /^(select|show|desc|describe|explain|insert|update|delete|create|drop|alter|truncate)\b/i.test(rawInput) || rawInput.includes(" ");
378
+ const isFullQuery = /^(select|show|desc|describe|explain|insert|update|delete|create|drop|alter|truncate|pragma)\b/i.test(rawInput) || rawInput.includes(" ");
360
379
  if (isFullQuery) {
361
380
  sqlQuery = rawInput;
362
381
  } else {
@@ -375,7 +394,53 @@ async function dbClientCommand(clientArgs = [], explicitService) {
375
394
  }
376
395
  }
377
396
 
378
- if (dbType === "postgres") {
397
+ if (dbType === "sqlite") {
398
+ const dbFile = env.DB_FILE || "database/blue_bird.db";
399
+ const dbPath = path.isAbsolute(dbFile) ? dbFile : path.resolve(process.cwd(), dbFile);
400
+ if (!fs.existsSync(dbPath)) {
401
+ console.log(chalk.yellow(`[INFO] SQLite database file not found at '${dbFile}'. It will be created upon query execution.`));
402
+ }
403
+
404
+ try {
405
+ let BetterSqlite;
406
+ try {
407
+ BetterSqlite = (await import("better-sqlite3")).default;
408
+ } catch (err) {
409
+ console.error(chalk.red("Error: 'better-sqlite3' is not installed. Run: npm install better-sqlite3"));
410
+ process.exit(1);
411
+ }
412
+
413
+ const dir = path.dirname(dbPath);
414
+ if (!fs.existsSync(dir)) {
415
+ fs.mkdirSync(dir, { recursive: true });
416
+ }
417
+
418
+ const db = new BetterSqlite(dbPath);
419
+ if (sqlQuery) {
420
+ console.log(chalk.cyan(`🔍 Executing SQLite query on '${dbFile}':`));
421
+ console.log(chalk.gray(` ${sqlQuery}\n`));
422
+ const isSelect = /^(select|pragma|explain)/i.test(sqlQuery.trim());
423
+ if (isSelect) {
424
+ const rows = db.prepare(sqlQuery).all();
425
+ console.table(rows);
426
+ } else {
427
+ const info = db.prepare(sqlQuery).run();
428
+ console.log(chalk.green(`✔ Query executed successfully. Changes: ${info.changes}, LastInsertRowId: ${info.lastInsertRowid}`));
429
+ }
430
+ } else {
431
+ console.log(chalk.cyan(`Connected to SQLite database '${dbFile}'.`));
432
+ console.log(chalk.gray("Active Tables:"));
433
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
434
+ console.table(tables);
435
+ console.log(chalk.yellow("\nTip: Run queries with: npx blue-bird docker db \"SELECT * FROM table_name\""));
436
+ }
437
+ db.close();
438
+ return;
439
+ } catch (err) {
440
+ console.error(chalk.red("[DATABASE ERROR] SQLite operation failed:"), err.message);
441
+ process.exit(1);
442
+ }
443
+ } else if (dbType === "postgres") {
379
444
  dbUser = rootOpt ? "postgres" : (dbUser || env.DB_USER || "postgres");
380
445
  dbName = dbName || env.DB_NAME || "blue_bird";
381
446
 
@@ -434,8 +499,8 @@ async function dbClientCommand(clientArgs = [], explicitService) {
434
499
  }
435
500
 
436
501
  /**
437
- * Exports database schema & data into a .sql file inside backups/ folder.
438
- * @param {string} dbType - Target db type ('mysql', 'postgres', 'none').
502
+ * Exports database schema & data into a .sql or .db file inside backups/ folder.
503
+ * @param {string} dbType - Target db type ('sqlite', 'mysql', 'postgres', 'none').
439
504
  * @param {string} [filenameArg] - Custom backup filename.
440
505
  * @param {string} [userOpt] - Custom db user.
441
506
  * @param {string} [passOpt] - Custom db password.
@@ -456,6 +521,29 @@ async function exportDbCommand(dbType, filenameArg, userOpt, passOpt, dbOpt) {
456
521
  fs.mkdirSync(backupsDir, { recursive: true });
457
522
  }
458
523
 
524
+ if (targetDbType === "sqlite") {
525
+ const dbFile = env.DB_FILE || "database/blue_bird.db";
526
+ const srcDbPath = path.isAbsolute(dbFile) ? dbFile : path.resolve(process.cwd(), dbFile);
527
+ if (!fs.existsSync(srcDbPath)) {
528
+ console.error(chalk.red(`Error: SQLite database file '${dbFile}' does not exist.`));
529
+ return;
530
+ }
531
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
532
+ const backupName = filenameArg || `backup_sqlite_${timestamp}.db`;
533
+ const destFile = path.isAbsolute(backupName)
534
+ ? backupName
535
+ : (backupName.includes("/") || backupName.includes("\\")
536
+ ? path.resolve(process.cwd(), backupName)
537
+ : path.join(backupsDir, backupName));
538
+
539
+ fs.copyFileSync(srcDbPath, destFile);
540
+ const stats = fs.statSync(destFile);
541
+ const sizeKb = (stats.size / 1024).toFixed(2);
542
+ console.log(chalk.green(`\n✔ SQLite database exported successfully!`));
543
+ console.log(chalk.cyan(` File: ${path.relative(process.cwd(), destFile)} (${sizeKb} KB)`));
544
+ return;
545
+ }
546
+
459
547
  let outputFile;
460
548
  if (filenameArg) {
461
549
  let name = filenameArg;
@@ -525,8 +613,8 @@ function exportDbToFile(cmdArgs, outputFile) {
525
613
  }
526
614
 
527
615
  /**
528
- * Imports a .sql file from backups/ folder into the container database.
529
- * @param {string} dbType - Target db type ('mysql', 'postgres', 'none').
616
+ * Imports a .sql or .db file from backups/ folder into the database.
617
+ * @param {string} dbType - Target db type ('sqlite', 'mysql', 'postgres', 'none').
530
618
  * @param {string} [filenameArg] - Custom backup filename.
531
619
  * @param {string} [userOpt] - Custom db user.
532
620
  * @param {string} [passOpt] - Custom db password.
@@ -550,24 +638,29 @@ async function importDbCommand(dbType, filenameArg, userOpt, passOpt, dbOpt) {
550
638
  let inputFile;
551
639
  if (filenameArg) {
552
640
  let name = filenameArg;
553
- if (!name.endsWith(".sql") && !fs.existsSync(name)) name += ".sql";
554
- if (fs.existsSync(name)) {
641
+ if (path.isAbsolute(name) && fs.existsSync(name)) {
642
+ inputFile = name;
643
+ } else if (fs.existsSync(path.resolve(process.cwd(), name))) {
555
644
  inputFile = path.resolve(process.cwd(), name);
556
645
  } else if (fs.existsSync(path.join(backupsDir, name))) {
557
646
  inputFile = path.join(backupsDir, name);
647
+ } else if (fs.existsSync(path.join(backupsDir, `${name}.sql`))) {
648
+ inputFile = path.join(backupsDir, `${name}.sql`);
649
+ } else if (fs.existsSync(path.join(backupsDir, `${name}.db`))) {
650
+ inputFile = path.join(backupsDir, `${name}.db`);
558
651
  } else {
559
652
  console.error(chalk.red(`Error: Backup file '${filenameArg}' not found in current directory or 'backups/' folder.`));
560
653
  process.exit(1);
561
654
  }
562
655
  } else {
563
656
  const files = fs.readdirSync(backupsDir)
564
- .filter(f => f.endsWith(".sql"))
657
+ .filter(f => f.endsWith(".sql") || f.endsWith(".db"))
565
658
  .map(f => ({ name: f, time: fs.statSync(path.join(backupsDir, f)).mtimeMs }))
566
659
  .sort((a, b) => b.time - a.time);
567
660
 
568
661
  if (files.length === 0) {
569
- console.error(chalk.red(`Error: No .sql backup files found in 'backups/' directory.`));
570
- console.log(chalk.yellow(`Usage: npx blue-bird docker import <file.sql>`));
662
+ console.error(chalk.red(`Error: No backup files found in 'backups/' directory.`));
663
+ console.log(chalk.yellow(`Usage: npx blue-bird docker import <file.sql | file.db>`));
571
664
  process.exit(1);
572
665
  }
573
666
 
@@ -575,6 +668,36 @@ async function importDbCommand(dbType, filenameArg, userOpt, passOpt, dbOpt) {
575
668
  console.log(chalk.yellow(`[INFO] No file specified. Using most recent backup: '${files[0].name}'`));
576
669
  }
577
670
 
671
+ const relPath = path.relative(process.cwd(), inputFile);
672
+
673
+ if (targetDbType === "sqlite") {
674
+ const dbFile = env.DB_FILE || "database/blue_bird.db";
675
+ const destDbPath = path.isAbsolute(dbFile) ? dbFile : path.resolve(process.cwd(), dbFile);
676
+ const destDir = path.dirname(destDbPath);
677
+ if (!fs.existsSync(destDir)) {
678
+ fs.mkdirSync(destDir, { recursive: true });
679
+ }
680
+
681
+ if (inputFile.endsWith(".db") || inputFile.endsWith(".sqlite")) {
682
+ fs.copyFileSync(inputFile, destDbPath);
683
+ console.log(chalk.green(`\n✔ SQLite database restored successfully from '${relPath}' to '${dbFile}'!`));
684
+ return;
685
+ }
686
+
687
+ try {
688
+ const BetterSqlite = (await import("better-sqlite3")).default;
689
+ const db = new BetterSqlite(destDbPath);
690
+ const sqlContent = fs.readFileSync(inputFile, "utf-8");
691
+ db.exec(sqlContent);
692
+ db.close();
693
+ console.log(chalk.green(`\n✔ SQL statements from '${relPath}' imported successfully into '${dbFile}'!`));
694
+ } catch (err) {
695
+ console.error(chalk.red(`\n✖ SQLite import failed:`), err.message);
696
+ process.exit(1);
697
+ }
698
+ return;
699
+ }
700
+
578
701
  const dbUser = userOpt || env.DB_USER || (targetDbType === "postgres" ? "postgres" : "root");
579
702
  const dbPass = passOpt || env.DB_PASSWORD || (targetDbType === "postgres" ? "postgres" : "root");
580
703
  const dbName = dbOpt || env.DB_NAME || "blue_bird";
@@ -586,7 +709,6 @@ async function importDbCommand(dbType, filenameArg, userOpt, passOpt, dbOpt) {
586
709
  cmdArgs = ["compose", "exec", "-T", "mysql", "mysql", `-u${dbUser}`, `-p${dbPass}`, dbName];
587
710
  }
588
711
 
589
- const relPath = path.relative(process.cwd(), inputFile);
590
712
  console.log(chalk.cyan(`📥 Importing SQL dump '${relPath}' into ${targetDbType.toUpperCase()} database '${dbName}'...`));
591
713
 
592
714
  const success = await importDbFromFile(cmdArgs, inputFile);
@@ -733,7 +855,9 @@ async function main() {
733
855
  process.env.BLUEBIRD_PROJECT_NAME = projectName;
734
856
  process.env.TITLE = projectName;
735
857
 
736
- const args = process.argv.slice(3);
858
+ const rawArgs = process.argv.slice(2);
859
+ const dockerIdx = rawArgs.indexOf("docker");
860
+ const args = dockerIdx !== -1 ? rawArgs.slice(dockerIdx + 1) : rawArgs;
737
861
  const command = args[0];
738
862
 
739
863
  if (!command) {
@@ -741,6 +865,7 @@ async function main() {
741
865
  return;
742
866
  }
743
867
 
868
+
744
869
  switch (command) {
745
870
  case "start":
746
871
  await startCommand(args[1]);
@@ -775,6 +900,7 @@ async function main() {
775
900
  case "redis":
776
901
  await redisCommand(args.slice(1));
777
902
  break;
903
+ case "sqlite":
778
904
  case "mysql":
779
905
  case "postgres":
780
906
  case "psql":
@@ -793,10 +919,61 @@ async function main() {
793
919
  await pruneCommand(force, all);
794
920
  break;
795
921
  }
922
+ case "help":
923
+ case "--help":
924
+ case "-h":
925
+ helpCommand();
926
+ break;
796
927
  default:
797
928
  console.log(chalk.yellow(`Unknown docker command: ${command}`));
798
- console.log("Available commands: dev, start, stop, build, ps, logs, pm2, export/dump, import/restore, mysql/postgres/db, redis, df/disk, prune/clean");
929
+ console.log("Run 'npx blue-bird docker help' to see all available commands.");
799
930
  }
931
+
932
+ }
933
+
934
+ /**
935
+ * Displays the Blue Bird Docker CLI help manual.
936
+ */
937
+ function helpCommand() {
938
+ console.log(chalk.bold.blue("\n🐦 Blue Bird Docker CLI — Command Reference"));
939
+ console.log(chalk.gray("────────────────────────────────────────────────────────────"));
940
+ console.log(chalk.bold("Usage: ") + chalk.cyan("npx blue-bird docker <command> [options]\n"));
941
+
942
+ console.log(chalk.bold.yellow("📦 Container Lifecycle:"));
943
+ console.log(` ${chalk.green("dev")} Starts development containers (Redis / DB). Run npm run dev locally.`);
944
+ console.log(` ${chalk.green("start [service]")} Starts production stack (app, nginx, redis, db) or specific service.`);
945
+ console.log(` ${chalk.green("start dev")} Starts development services only.`);
946
+ console.log(` ${chalk.green("start db")} Starts configured database container only.`);
947
+ console.log(` ${chalk.green("start redis")} Starts Redis container only.`);
948
+ console.log(` ${chalk.green("start dbs")} Starts all database containers (DB + Redis).`);
949
+ console.log(` ${chalk.green("stop [service]")} Stops all running containers (or specific service).`);
950
+ console.log(` ${chalk.green("build [--no-cache]")} Builds or updates the Node.js production image.`);
951
+ console.log(` ${chalk.green("ps | status")} Lists status of active project containers.`);
952
+ console.log(` ${chalk.green("logs [service] [-f]")} Tails logs for container (app, db, redis, nginx).`);
953
+
954
+ console.log(chalk.bold.yellow("\n🗄️ Database & Inspection:"));
955
+ console.log(` ${chalk.green("db | sqlite | mysql | psql")} [query/table]`);
956
+ console.log(` Inspects active database (tables, columns, or runs SQL query).`);
957
+ console.log(` ${chalk.gray("e.g. npx blue-bird docker db tables")}`);
958
+ console.log(` ${chalk.gray("e.g. npx blue-bird docker db columns users")}`);
959
+ console.log(` ${chalk.gray("e.g. npx blue-bird docker db \"SELECT * FROM users\"")}`);
960
+ console.log(` ${chalk.green("export | dump [file]")} Dumps database backup (.db file for SQLite or .sql) into backups/.`);
961
+ console.log(` ${chalk.green("import | restore [file]")} Restores database from backups/ (.db or .sql).`);
962
+
963
+ console.log(chalk.bold.yellow("\n⚡ Redis & Monitoring:"));
964
+ console.log(` ${chalk.green("redis")} [command] Runs interactive Redis CLI or smart subcommands.`);
965
+ console.log(` ${chalk.gray("e.g. npx blue-bird docker redis monitor")}`);
966
+ console.log(` ${chalk.gray("e.g. npx blue-bird docker redis keys")}`);
967
+ console.log(` ${chalk.gray("e.g. npx blue-bird docker redis key <keyname>")}`);
968
+ console.log(` ${chalk.green("pm2 [args]")} Runs PM2 commands inside app container (status, monit, reload).`);
969
+ console.log(` ${chalk.green("df | disk")} Shows Docker disk usage statistics.`);
970
+ console.log(` ${chalk.green("prune | clean [-f] [-a]")} Cleans unused volumes, dangling images, and build caches.`);
971
+
972
+ console.log(chalk.bold.yellow("\nℹ️ Help:"));
973
+ console.log(` ${chalk.green("help | --help | -h")} Displays this help message.`);
974
+ console.log(chalk.gray("────────────────────────────────────────────────────────────\n"));
800
975
  }
801
976
 
802
977
  main();
978
+
979
+
package/core/cli/init.js CHANGED
@@ -28,8 +28,9 @@ class ProjectInit {
28
28
  let title = "Blue-Bird";
29
29
  let port = 3000;
30
30
  let appUrl = "http://localhost:3000";
31
- let dbType = "none";
31
+ let dbType = "sqlite";
32
32
  let dbName = "blue_bird";
33
+ let dbFile = "database/blue_bird.db";
33
34
  let dbUser = "root";
34
35
  let dbPassword = "root";
35
36
  let dbPort = 3306;
@@ -55,11 +56,17 @@ class ProjectInit {
55
56
  appUrl = await ask("Application URL", defaultAppUrl);
56
57
 
57
58
  const dbTypeAns = await ask(
58
- "Which database do you want to configure? (none / mysql / postgres)",
59
- "none",
59
+ "Which database do you want to configure? (sqlite / mysql / postgres / none)",
60
+ "sqlite",
60
61
  );
61
62
  const cleanDbTypeAns = dbTypeAns.toLowerCase().trim();
62
63
  if (
64
+ cleanDbTypeAns === "sqlite" ||
65
+ cleanDbTypeAns === "sql" ||
66
+ cleanDbTypeAns === "sqlite3"
67
+ ) {
68
+ dbType = "sqlite";
69
+ } else if (
63
70
  cleanDbTypeAns === "postgres" ||
64
71
  cleanDbTypeAns === "postgresql" ||
65
72
  cleanDbTypeAns === "pg" ||
@@ -72,7 +79,15 @@ class ProjectInit {
72
79
  dbType = "none";
73
80
  }
74
81
 
75
- if (dbType !== "none") {
82
+ if (dbType === "sqlite") {
83
+ dbName = await ask("Database Name", dbName);
84
+ const defaultDbFile = `database/${dbName}.db`;
85
+ dbFile = await ask("Database File Path", defaultDbFile);
86
+ const dbDir = path.dirname(path.join(this.appDir, dbFile));
87
+ if (!fs.existsSync(dbDir)) {
88
+ fs.mkdirSync(dbDir, { recursive: true });
89
+ }
90
+ } else if (dbType !== "none") {
76
91
  dbName = await ask("Database Name", dbName);
77
92
  dbUser = await ask(
78
93
  "Database User",
@@ -126,7 +141,7 @@ class ProjectInit {
126
141
  }
127
142
  });
128
143
 
129
- const gitignoreContent = `node_modules\nlogs\n.env\npackage-lock.json\n*.db\ntest/\n\nbackups/*.sql\n`;
144
+ const gitignoreContent = `node_modules\nlogs\n.env\npackage-lock.json\n*.db\n*.sqlite\n*.sqlite-wal\n*.sqlite-shm\n*.db-wal\n*.db-shm\ntest/\n\nbackups/*.sql\nbackups/*.db\n`;
130
145
  const gitignoreDest = path.join(this.appDir, ".gitignore");
131
146
  const gitignoreSrc = path.join(this.sourceDir, ".gitignore");
132
147
 
@@ -149,7 +164,9 @@ class ProjectInit {
149
164
  ? "docker-compose.postgres.yml"
150
165
  : dbType === "mysql"
151
166
  ? "docker-compose.mysql.yml"
152
- : "docker-compose.none.yml";
167
+ : dbType === "sqlite"
168
+ ? "docker-compose.sqlite.yml"
169
+ : "docker-compose.none.yml";
153
170
  const composeSrc = path.join(
154
171
  this.sourceDir,
155
172
  "docker",
@@ -198,13 +215,19 @@ class ProjectInit {
198
215
  DB_TYPE: dbType,
199
216
  };
200
217
 
201
- if (dbType === "mysql") {
218
+ if (dbType === "sqlite") {
219
+ updates.DB_TYPE = "sqlite";
220
+ updates.DB_FILE = dbFile;
221
+ updates.DATABASE_URL = `sqlite:${dbFile}`;
222
+ } else if (dbType === "mysql") {
223
+ updates.DB_TYPE = "mysql";
202
224
  updates.DB_NAME = dbName;
203
225
  updates.DB_USER = dbUser;
204
226
  updates.DB_PASSWORD = dbPassword;
205
227
  updates.DB_PORT = dbPort;
206
228
  updates.DATABASE_URL = `mysql://${dbUser}:${dbPassword}@localhost:${dbPort}/${dbName}`;
207
229
  } else if (dbType === "postgres") {
230
+ updates.DB_TYPE = "postgres";
208
231
  updates.DB_NAME = dbName;
209
232
  updates.DB_USER = dbUser;
210
233
  updates.DB_PASSWORD = dbPassword;
@@ -260,7 +283,9 @@ class ProjectInit {
260
283
  );
261
284
  try {
262
285
  let packagesToInstall = ["redis"];
263
- if (dbType === "postgres") {
286
+ if (dbType === "sqlite") {
287
+ packagesToInstall.push("better-sqlite3");
288
+ } else if (dbType === "postgres") {
264
289
  packagesToInstall.push("pg");
265
290
  } else {
266
291
  packagesToInstall.push("mysql2");
@@ -365,6 +390,63 @@ class ProjectInit {
365
390
  }
366
391
  }
367
392
 
393
+ /**
394
+ * Handles adding on-demand modules and dependencies.
395
+ * @param {string} [feature]
396
+ */
397
+ function addCommand(feature) {
398
+ if (!feature) {
399
+ console.log(chalk.yellow("Usage: npx blue-bird add <feature>"));
400
+ console.log("Available features to add:");
401
+ console.log(" upload | multer - Installs multer for file uploads");
402
+ console.log(" ws | websocket - Installs ws for WebSockets");
403
+ console.log(" redis - Installs redis for distributed caching and sessions");
404
+ console.log(" sqlite - Installs better-sqlite3 for local SQLite database");
405
+ console.log(" mysql - Installs mysql2 for MySQL database");
406
+ console.log(" postgres | pg - Installs pg for PostgreSQL database");
407
+ console.log(" bcrypt - Installs bcrypt for password hashing");
408
+ console.log(" swagger - Installs swagger-ui-express");
409
+ return;
410
+ }
411
+
412
+ const cleanFeature = feature.toLowerCase().trim();
413
+ const packageMap = {
414
+ upload: "multer",
415
+ multer: "multer",
416
+ ws: "ws",
417
+ websocket: "ws",
418
+ websockets: "ws",
419
+ redis: "redis",
420
+ sqlite: "better-sqlite3",
421
+ "better-sqlite3": "better-sqlite3",
422
+ mysql: "mysql2",
423
+ mysql2: "mysql2",
424
+ postgres: "pg",
425
+ postgresql: "pg",
426
+ pg: "pg",
427
+ bcrypt: "bcrypt",
428
+ swagger: "swagger-ui-express",
429
+ };
430
+
431
+ const pkgName = packageMap[cleanFeature];
432
+ if (!pkgName) {
433
+ console.error(chalk.red(`[ERROR] Unknown feature '${feature}'.`));
434
+ console.log("Available features: upload, ws, redis, sqlite, mysql, postgres, bcrypt, swagger");
435
+ return;
436
+ }
437
+
438
+ console.log(chalk.cyan(`[INFO] Installing ${pkgName}...`));
439
+ try {
440
+ execSync(`npm install ${pkgName}`, {
441
+ stdio: "inherit",
442
+ cwd: process.cwd(),
443
+ });
444
+ console.log(chalk.green(`[OK] Successfully installed ${pkgName}.`));
445
+ } catch (err) {
446
+ console.error(chalk.red(`[ERROR] Failed to install ${pkgName}:`), err.message);
447
+ }
448
+ }
449
+
368
450
  const initializer = new ProjectInit();
369
451
 
370
452
  const args = process.argv.slice(2);
@@ -373,4 +455,6 @@ const command = args[0];
373
455
  if (command === "route") import("./route.js");
374
456
  else if (command === "swagger-install") import("./swagger.js");
375
457
  else if (command === "docker") import("./docker.js");
458
+ else if (command === "add") addCommand(args[1]);
376
459
  else initializer.run();
460
+