@seip/blue-bird 0.7.6 → 0.8.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.
- package/.env_example +34 -34
- package/AGENTS.md +174 -249
- package/LICENSE +21 -21
- package/README.md +312 -367
- package/{index.js → backend/index.js} +22 -30
- package/backend/routes/api.js +57 -57
- package/core/app.js +338 -402
- package/core/auth.js +262 -256
- package/core/cache.js +174 -174
- package/core/cli/docker.js +488 -457
- package/core/cli/init.js +332 -336
- package/core/cli/route.js +42 -42
- package/core/config.js +52 -52
- package/core/database.js +263 -263
- package/core/debug.js +248 -248
- package/core/logger.js +115 -115
- package/core/middleware.js +27 -27
- package/core/router.js +144 -144
- package/core/swagger.js +40 -40
- package/core/upload.js +77 -77
- package/core/validate.js +380 -380
- package/docker/Dockerfile +16 -16
- package/docker/docker-compose.dev.yml +6 -0
- package/docker/docker-compose.mysql.yml +92 -92
- package/docker/docker-compose.none.yml +68 -68
- package/docker/docker-compose.postgres.yml +93 -93
- package/docker/nginx.conf +98 -106
- package/docker-compose.yml +92 -92
- package/frontend/about.html +98 -0
- package/frontend/css/app.css +0 -0
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +141 -0
- package/frontend/js/bundle.js +8 -0
- package/package.json +64 -71
- package/frontend/astro.config.mjs +0 -35
- package/frontend/public/css/app.css +0 -319
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/http/api.js +0 -29
- package/frontend/src/layouts/Layout.astro +0 -20
- package/frontend/src/pages/about.astro +0 -54
- package/frontend/src/pages/index.astro +0 -110
package/core/cli/docker.js
CHANGED
|
@@ -1,457 +1,488 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Parses the .env file to retrieve project configurations.
|
|
8
|
-
* @returns {Object} Local environment variables dictionary.
|
|
9
|
-
*/
|
|
10
|
-
function getEnvVars() {
|
|
11
|
-
const env = { ...process.env };
|
|
12
|
-
const envPath = path.join(process.cwd(), ".env");
|
|
13
|
-
if (fs.existsSync(envPath)) {
|
|
14
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
15
|
-
content.split(/\r?\n/).forEach(line => {
|
|
16
|
-
line = line.trim();
|
|
17
|
-
if (line && !line.startsWith("#") && line.includes("=")) {
|
|
18
|
-
const idx = line.indexOf("=");
|
|
19
|
-
const key = line.substring(0, idx).trim();
|
|
20
|
-
const value = line.substring(idx + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
21
|
-
env[key] = value;
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
return env;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Determines the target database type (mysql or postgres) from environment variables.
|
|
30
|
-
* @param {Object} [env] - Environment dictionary.
|
|
31
|
-
* @returns {string} 'postgres' or 'mysql'.
|
|
32
|
-
*/
|
|
33
|
-
function getDbType(env = getEnvVars()) {
|
|
34
|
-
if (env.DB_TYPE && (env.DB_TYPE.toLowerCase() === "postgres" || env.DB_TYPE.toLowerCase() === "postgresql" || env.DB_TYPE.toLowerCase() === "pg")) {
|
|
35
|
-
return "postgres";
|
|
36
|
-
}
|
|
37
|
-
if (env.DB_TYPE && env.DB_TYPE.toLowerCase() === "mysql") {
|
|
38
|
-
return "mysql";
|
|
39
|
-
}
|
|
40
|
-
if (env.DB_TYPE && env.DB_TYPE.toLowerCase() === "none") {
|
|
41
|
-
return "none";
|
|
42
|
-
}
|
|
43
|
-
if (env.DATABASE_URL && !env.DATABASE_URL.startsWith("#")) {
|
|
44
|
-
if (env.DATABASE_URL.startsWith("postgres://") || env.DATABASE_URL.startsWith("postgresql://")) {
|
|
45
|
-
return "postgres";
|
|
46
|
-
}
|
|
47
|
-
if (env.DATABASE_URL.startsWith("mysql://")) {
|
|
48
|
-
return "mysql";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return "mysql";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Spawns a child process and inherits standard IO for interactive terminal sessions.
|
|
56
|
-
* @param {string} command - The binary to execute.
|
|
57
|
-
* @param {string[]} args - Argument list.
|
|
58
|
-
* @returns {Promise<number>} Exit code of the process.
|
|
59
|
-
*/
|
|
60
|
-
function runCmd(command, args) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
const proc = spawn(command, args, { stdio: "inherit", env: process.env });
|
|
63
|
-
proc.on("close", (code) => {
|
|
64
|
-
resolve(code || 0);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Confirms that a docker-compose.yml file is present in the workspace directory.
|
|
71
|
-
*/
|
|
72
|
-
function checkComposeFile() {
|
|
73
|
-
if (!fs.existsSync("docker-compose.yml")) {
|
|
74
|
-
console.error(chalk.red("Error: docker-compose.yml not found in the current directory."));
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Handles the 'start' CLI command.
|
|
81
|
-
* @param {string} service - Service to boot up.
|
|
82
|
-
*/
|
|
83
|
-
async function startCommand(service) {
|
|
84
|
-
checkComposeFile();
|
|
85
|
-
const dbType = getDbType();
|
|
86
|
-
|
|
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);
|
|
89
|
-
if (targetContainer === "none") {
|
|
90
|
-
console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', skipping database container startup."));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
console.log(chalk.cyan(`Starting ${targetContainer.toUpperCase()} container...`));
|
|
94
|
-
const code = await runCmd("docker", ["compose", "up", "-d", targetContainer]);
|
|
95
|
-
if (code === 0) {
|
|
96
|
-
console.log(chalk.green(`${targetContainer.toUpperCase()} started.`));
|
|
97
|
-
} else {
|
|
98
|
-
console.error(chalk.red(`Error starting ${targetContainer}. Make sure '${targetContainer}' service is defined in docker-compose.yml.`));
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
} else if (service === "redis" || service === "--redis") {
|
|
102
|
-
console.log(chalk.cyan("Starting Redis container..."));
|
|
103
|
-
const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
|
|
104
|
-
if (code === 0) {
|
|
105
|
-
console.log(chalk.green("Redis started."));
|
|
106
|
-
} else {
|
|
107
|
-
console.error(chalk.red("Error starting Redis."));
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
} else if (service === "dbs" || service === "databases") {
|
|
111
|
-
if (dbType === "none") {
|
|
112
|
-
console.log(chalk.cyan("DB_TYPE is 'none', starting Redis container only..."));
|
|
113
|
-
const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
|
|
114
|
-
if (code === 0) {
|
|
115
|
-
console.log(chalk.green("Redis started."));
|
|
116
|
-
} else {
|
|
117
|
-
console.error(chalk.red("Error starting Redis."));
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
console.log(chalk.cyan(`Starting Database containers (${dbType.toUpperCase()} + Redis)...`));
|
|
123
|
-
const code = await runCmd("docker", ["compose", "up", "-d", dbType, "redis"]);
|
|
124
|
-
if (code === 0) {
|
|
125
|
-
console.log(chalk.green("Database containers started."));
|
|
126
|
-
} else {
|
|
127
|
-
console.error(chalk.red("Error starting databases."));
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
} else if (service === "prod" || service === "app" || service === "--app" || !service) {
|
|
131
|
-
console.log(chalk.cyan("Starting production stack (DB + Redis + App + Nginx)..."));
|
|
132
|
-
const code = await runCmd("docker", ["compose", "--profile", "prod", "up", "-d"]);
|
|
133
|
-
if (code === 0) {
|
|
134
|
-
console.log(chalk.green("Production stack started."));
|
|
135
|
-
} else {
|
|
136
|
-
console.error(chalk.red("Error starting production stack."));
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
console.error(chalk.red(`Unknown service '${service}'. Use: mysql, postgres, db, redis, dbs, prod.`));
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Handles the '
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} else
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(chalk.
|
|
167
|
-
} else
|
|
168
|
-
console.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
*
|
|
346
|
-
* @param {
|
|
347
|
-
*/
|
|
348
|
-
async function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
case "
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
case "
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
await
|
|
449
|
-
break;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses the .env file to retrieve project configurations.
|
|
8
|
+
* @returns {Object} Local environment variables dictionary.
|
|
9
|
+
*/
|
|
10
|
+
function getEnvVars() {
|
|
11
|
+
const env = { ...process.env };
|
|
12
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
13
|
+
if (fs.existsSync(envPath)) {
|
|
14
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
15
|
+
content.split(/\r?\n/).forEach(line => {
|
|
16
|
+
line = line.trim();
|
|
17
|
+
if (line && !line.startsWith("#") && line.includes("=")) {
|
|
18
|
+
const idx = line.indexOf("=");
|
|
19
|
+
const key = line.substring(0, idx).trim();
|
|
20
|
+
const value = line.substring(idx + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
21
|
+
env[key] = value;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return env;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determines the target database type (mysql or postgres) from environment variables.
|
|
30
|
+
* @param {Object} [env] - Environment dictionary.
|
|
31
|
+
* @returns {string} 'postgres' or 'mysql'.
|
|
32
|
+
*/
|
|
33
|
+
function getDbType(env = getEnvVars()) {
|
|
34
|
+
if (env.DB_TYPE && (env.DB_TYPE.toLowerCase() === "postgres" || env.DB_TYPE.toLowerCase() === "postgresql" || env.DB_TYPE.toLowerCase() === "pg")) {
|
|
35
|
+
return "postgres";
|
|
36
|
+
}
|
|
37
|
+
if (env.DB_TYPE && env.DB_TYPE.toLowerCase() === "mysql") {
|
|
38
|
+
return "mysql";
|
|
39
|
+
}
|
|
40
|
+
if (env.DB_TYPE && env.DB_TYPE.toLowerCase() === "none") {
|
|
41
|
+
return "none";
|
|
42
|
+
}
|
|
43
|
+
if (env.DATABASE_URL && !env.DATABASE_URL.startsWith("#")) {
|
|
44
|
+
if (env.DATABASE_URL.startsWith("postgres://") || env.DATABASE_URL.startsWith("postgresql://")) {
|
|
45
|
+
return "postgres";
|
|
46
|
+
}
|
|
47
|
+
if (env.DATABASE_URL.startsWith("mysql://")) {
|
|
48
|
+
return "mysql";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "mysql";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Spawns a child process and inherits standard IO for interactive terminal sessions.
|
|
56
|
+
* @param {string} command - The binary to execute.
|
|
57
|
+
* @param {string[]} args - Argument list.
|
|
58
|
+
* @returns {Promise<number>} Exit code of the process.
|
|
59
|
+
*/
|
|
60
|
+
function runCmd(command, args) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const proc = spawn(command, args, { stdio: "inherit", env: process.env });
|
|
63
|
+
proc.on("close", (code) => {
|
|
64
|
+
resolve(code || 0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Confirms that a docker-compose.yml file is present in the workspace directory.
|
|
71
|
+
*/
|
|
72
|
+
function checkComposeFile() {
|
|
73
|
+
if (!fs.existsSync("docker-compose.yml")) {
|
|
74
|
+
console.error(chalk.red("Error: docker-compose.yml not found in the current directory."));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handles the 'start' CLI command.
|
|
81
|
+
* @param {string} service - Service to boot up.
|
|
82
|
+
*/
|
|
83
|
+
async function startCommand(service) {
|
|
84
|
+
checkComposeFile();
|
|
85
|
+
const dbType = getDbType();
|
|
86
|
+
|
|
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);
|
|
89
|
+
if (targetContainer === "none") {
|
|
90
|
+
console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', skipping database container startup."));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log(chalk.cyan(`Starting ${targetContainer.toUpperCase()} container...`));
|
|
94
|
+
const code = await runCmd("docker", ["compose", "up", "-d", targetContainer]);
|
|
95
|
+
if (code === 0) {
|
|
96
|
+
console.log(chalk.green(`${targetContainer.toUpperCase()} started.`));
|
|
97
|
+
} else {
|
|
98
|
+
console.error(chalk.red(`Error starting ${targetContainer}. Make sure '${targetContainer}' service is defined in docker-compose.yml.`));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
} else if (service === "redis" || service === "--redis") {
|
|
102
|
+
console.log(chalk.cyan("Starting Redis container..."));
|
|
103
|
+
const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
|
|
104
|
+
if (code === 0) {
|
|
105
|
+
console.log(chalk.green("Redis started."));
|
|
106
|
+
} else {
|
|
107
|
+
console.error(chalk.red("Error starting Redis."));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
} else if (service === "dbs" || service === "databases") {
|
|
111
|
+
if (dbType === "none") {
|
|
112
|
+
console.log(chalk.cyan("DB_TYPE is 'none', starting Redis container only..."));
|
|
113
|
+
const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
|
|
114
|
+
if (code === 0) {
|
|
115
|
+
console.log(chalk.green("Redis started."));
|
|
116
|
+
} else {
|
|
117
|
+
console.error(chalk.red("Error starting Redis."));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log(chalk.cyan(`Starting Database containers (${dbType.toUpperCase()} + Redis)...`));
|
|
123
|
+
const code = await runCmd("docker", ["compose", "up", "-d", dbType, "redis"]);
|
|
124
|
+
if (code === 0) {
|
|
125
|
+
console.log(chalk.green("Database containers started."));
|
|
126
|
+
} else {
|
|
127
|
+
console.error(chalk.red("Error starting databases."));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
} else if (service === "prod" || service === "app" || service === "--app" || !service) {
|
|
131
|
+
console.log(chalk.cyan("Starting production stack (DB + Redis + App + Nginx)..."));
|
|
132
|
+
const code = await runCmd("docker", ["compose", "--profile", "prod", "up", "-d"]);
|
|
133
|
+
if (code === 0) {
|
|
134
|
+
console.log(chalk.green("Production stack started."));
|
|
135
|
+
} else {
|
|
136
|
+
console.error(chalk.red("Error starting production stack."));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.error(chalk.red(`Unknown service '${service}'. Use: mysql, postgres, db, redis, dbs, prod.`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handles the 'dev' CLI command.
|
|
147
|
+
*/
|
|
148
|
+
async function devCommand() {
|
|
149
|
+
checkComposeFile();
|
|
150
|
+
console.log(chalk.cyan("Starting development stack (DB + Redis + App (npm run dev) + Nginx)..."));
|
|
151
|
+
|
|
152
|
+
const devComposeFile = path.join(process.cwd(), "docker", "docker-compose.dev.yml");
|
|
153
|
+
const cmdArgs = ["compose", "-f", "docker-compose.yml"];
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(devComposeFile)) {
|
|
156
|
+
cmdArgs.push("-f", "docker/docker-compose.dev.yml");
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.yellow("[WARN] docker/docker-compose.dev.yml not found, running standard stack."));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
cmdArgs.push("--profile", "prod", "up", "-d");
|
|
162
|
+
|
|
163
|
+
const code = await runCmd("docker", cmdArgs);
|
|
164
|
+
if (code === 0) {
|
|
165
|
+
console.log(chalk.green("Development stack started."));
|
|
166
|
+
console.log(chalk.cyan("View live logs with: npx blue-bird docker logs"));
|
|
167
|
+
} else {
|
|
168
|
+
console.error(chalk.red("Error starting development stack."));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handles the 'stop' CLI command.
|
|
175
|
+
* @param {string} service - Service to stop.
|
|
176
|
+
*/
|
|
177
|
+
async function stopCommand(service) {
|
|
178
|
+
checkComposeFile();
|
|
179
|
+
const dbType = getDbType();
|
|
180
|
+
|
|
181
|
+
if (!service || service === "all") {
|
|
182
|
+
console.log(chalk.cyan("Stopping all Blue Bird containers..."));
|
|
183
|
+
await runCmd("docker", ["compose", "--profile", "prod", "down"]);
|
|
184
|
+
console.log(chalk.green("All containers stopped."));
|
|
185
|
+
} else if (service === "mysql" || service === "--mysql" || service === "postgres" || service === "--postgres" || service === "db" || service === "--db") {
|
|
186
|
+
const targetContainer = (service === "postgres" || service === "--postgres") ? "postgres" : ((service === "mysql" || service === "--mysql") ? "mysql" : dbType);
|
|
187
|
+
if (targetContainer === "none") {
|
|
188
|
+
console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', no database container to stop."));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.cyan(`Stopping ${targetContainer.toUpperCase()}...`));
|
|
192
|
+
await runCmd("docker", ["compose", "stop", targetContainer]);
|
|
193
|
+
await runCmd("docker", ["compose", "rm", "-f", targetContainer]);
|
|
194
|
+
console.log(chalk.green(`${targetContainer.toUpperCase()} stopped.`));
|
|
195
|
+
} else if (service === "redis" || service === "--redis") {
|
|
196
|
+
console.log(chalk.cyan("Stopping Redis..."));
|
|
197
|
+
await runCmd("docker", ["compose", "stop", "redis"]);
|
|
198
|
+
await runCmd("docker", ["compose", "rm", "-f", "redis"]);
|
|
199
|
+
console.log(chalk.green("Redis stopped."));
|
|
200
|
+
} else if (service === "app" || service === "--app") {
|
|
201
|
+
console.log(chalk.cyan("Stopping Node.js app container..."));
|
|
202
|
+
await runCmd("docker", ["compose", "--profile", "prod", "stop", "app"]);
|
|
203
|
+
await runCmd("docker", ["compose", "--profile", "prod", "rm", "-f", "app"]);
|
|
204
|
+
console.log(chalk.green("App container stopped."));
|
|
205
|
+
} else {
|
|
206
|
+
console.error(chalk.red(`Unknown service '${service}'. Use: all, mysql, postgres, db, redis, app.`));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Handles the 'build' CLI command.
|
|
213
|
+
* @param {string[]} options - Build configuration options.
|
|
214
|
+
*/
|
|
215
|
+
async function buildCommand(options = []) {
|
|
216
|
+
checkComposeFile();
|
|
217
|
+
console.log(chalk.cyan("Building Blue Bird app Docker image..."));
|
|
218
|
+
const cmdArgs = ["compose", "--profile", "prod", "build"];
|
|
219
|
+
if (options.includes("--no-cache") || options.includes("-n")) {
|
|
220
|
+
cmdArgs.push("--no-cache");
|
|
221
|
+
}
|
|
222
|
+
cmdArgs.push("app");
|
|
223
|
+
|
|
224
|
+
const code = await runCmd("docker", cmdArgs);
|
|
225
|
+
if (code === 0) {
|
|
226
|
+
console.log(chalk.green("Image built successfully. Start with: npx blue-bird docker start prod"));
|
|
227
|
+
} else {
|
|
228
|
+
console.error(chalk.red("Build error."));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handles the 'ps' CLI command.
|
|
235
|
+
*/
|
|
236
|
+
async function psCommand() {
|
|
237
|
+
checkComposeFile();
|
|
238
|
+
console.log(chalk.cyan("Active Blue Bird Containers:"));
|
|
239
|
+
await runCmd("docker", ["compose", "--profile", "prod", "ps"]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Handles the 'logs' CLI command.
|
|
244
|
+
* @param {string} service - Service to pull logs from.
|
|
245
|
+
* @param {string} followOpt - Flag for active log tailing.
|
|
246
|
+
*/
|
|
247
|
+
async function logsCommand(service, followOpt) {
|
|
248
|
+
checkComposeFile();
|
|
249
|
+
const follow = followOpt !== "--no-follow";
|
|
250
|
+
const dbType = getDbType();
|
|
251
|
+
let targetService = "app";
|
|
252
|
+
if (service === "mysql" || service === "postgres" || service === "db") {
|
|
253
|
+
targetService = service === "db" ? dbType : service;
|
|
254
|
+
}
|
|
255
|
+
if (targetService === "none") {
|
|
256
|
+
console.log(chalk.yellow("[INFO] DB_TYPE is set to 'none', no database container logs to display."));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const cmdArgs = ["compose"];
|
|
261
|
+
if (targetService === "app") {
|
|
262
|
+
cmdArgs.push("--profile", "prod");
|
|
263
|
+
}
|
|
264
|
+
cmdArgs.push("logs", "--tail=100");
|
|
265
|
+
if (follow) {
|
|
266
|
+
cmdArgs.push("-f");
|
|
267
|
+
}
|
|
268
|
+
cmdArgs.push(targetService);
|
|
269
|
+
|
|
270
|
+
await runCmd("docker", cmdArgs);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handles interactive shell connections into the MySQL or PostgreSQL container.
|
|
275
|
+
* @param {string} userOpt - DB username.
|
|
276
|
+
* @param {string} passOpt - DB password.
|
|
277
|
+
* @param {string} dbOpt - DB database name.
|
|
278
|
+
* @param {boolean} rootOpt - Flag for overriding database credentials to connect as root/postgres.
|
|
279
|
+
* @param {string} explicitService - Explicit target service if specified ('mysql' or 'postgres').
|
|
280
|
+
*/
|
|
281
|
+
async function dbClientCommand(userOpt, passOpt, dbOpt, rootOpt, explicitService) {
|
|
282
|
+
checkComposeFile();
|
|
283
|
+
const env = getEnvVars();
|
|
284
|
+
const dbType = explicitService === "postgres" || explicitService === "psql" ? "postgres" : (explicitService === "mysql" ? "mysql" : getDbType(env));
|
|
285
|
+
if (dbType === "none") {
|
|
286
|
+
console.error(chalk.yellow("[INFO] DB_TYPE is set to 'none'. No database container available to connect to."));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let dbUser = userOpt;
|
|
291
|
+
let dbPass = passOpt;
|
|
292
|
+
let dbName = dbOpt;
|
|
293
|
+
|
|
294
|
+
if (dbType === "postgres") {
|
|
295
|
+
if (rootOpt) {
|
|
296
|
+
dbUser = "postgres";
|
|
297
|
+
} else {
|
|
298
|
+
dbUser = dbUser || env.DB_USER || "postgres";
|
|
299
|
+
}
|
|
300
|
+
dbName = dbName || env.DB_NAME || "blue_bird";
|
|
301
|
+
|
|
302
|
+
const targetDb = dbName ? ` (database: ${dbName})` : "";
|
|
303
|
+
console.log(chalk.cyan(`Connecting to PostgreSQL shell (psql) in container as '${dbUser}'${targetDb}...`));
|
|
304
|
+
|
|
305
|
+
const cmdArgs = ["compose", "exec", "postgres", "psql", `-U${dbUser}`];
|
|
306
|
+
if (dbName) {
|
|
307
|
+
cmdArgs.push("-d", dbName);
|
|
308
|
+
}
|
|
309
|
+
const code = await runCmd("docker", cmdArgs);
|
|
310
|
+
if (code !== 0) {
|
|
311
|
+
console.error(chalk.yellow("Make sure the PostgreSQL container is running: npx blue-bird docker start postgres"));
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
if (rootOpt) {
|
|
316
|
+
dbUser = "root";
|
|
317
|
+
dbPass = env.DB_PASSWORD || "root";
|
|
318
|
+
} else {
|
|
319
|
+
dbUser = dbUser || env.DB_USER || "root";
|
|
320
|
+
dbPass = dbPass || env.DB_PASSWORD || "root";
|
|
321
|
+
}
|
|
322
|
+
dbName = dbName || env.DB_NAME || "blue_bird";
|
|
323
|
+
|
|
324
|
+
const cmdArgs = ["compose", "exec", "mysql", "mysql", `-u${dbUser}`];
|
|
325
|
+
if (dbPass) {
|
|
326
|
+
cmdArgs.push(`-p${dbPass}`);
|
|
327
|
+
}
|
|
328
|
+
if (dbName) {
|
|
329
|
+
cmdArgs.push(dbName);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const targetDb = dbName ? ` (database: ${dbName})` : "";
|
|
333
|
+
console.log(chalk.cyan(`Connecting to MySQL shell in container as '${dbUser}'${targetDb}...`));
|
|
334
|
+
|
|
335
|
+
const code = await runCmd("docker", cmdArgs);
|
|
336
|
+
if (code !== 0) {
|
|
337
|
+
console.error(chalk.yellow("Make sure the MySQL container is running: npx blue-bird docker start mysql"));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Handles cleaning up and pruning unused Docker resources.
|
|
345
|
+
* @param {boolean} forceOpt - Force cleaning without user confirmation.
|
|
346
|
+
* @param {boolean} allOpt - Clean all unused resources.
|
|
347
|
+
*/
|
|
348
|
+
async function pruneCommand(forceOpt, allOpt) {
|
|
349
|
+
if (!forceOpt) {
|
|
350
|
+
console.log(chalk.yellow("Clean unused volumes, dangling images, and BuildKit cache? (Press enter to confirm, Ctrl+C to cancel)"));
|
|
351
|
+
await new Promise((resolve) => process.stdin.once("data", resolve));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log(chalk.cyan("1/3 Cleaning orphaned Docker volumes..."));
|
|
355
|
+
await runCmd("docker", ["volume", "prune", "-f"]);
|
|
356
|
+
|
|
357
|
+
console.log(chalk.cyan("2/3 Cleaning unused Docker images..."));
|
|
358
|
+
const imgArgs = ["image", "prune"];
|
|
359
|
+
if (allOpt) {
|
|
360
|
+
imgArgs.push("-a");
|
|
361
|
+
}
|
|
362
|
+
imgArgs.push("-f");
|
|
363
|
+
await runCmd("docker", imgArgs);
|
|
364
|
+
|
|
365
|
+
console.log(chalk.cyan("3/3 Cleaning BuildKit build cache..."));
|
|
366
|
+
await runCmd("docker", ["builder", "prune", "-a", "-f"]);
|
|
367
|
+
|
|
368
|
+
console.log(chalk.green("\nDocker cleanup completed successfully! Current disk usage:"));
|
|
369
|
+
await runCmd("docker", ["system", "df"]);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Executes PM2 commands inside the Node.js application container.
|
|
374
|
+
* @param {string[]} pm2Args - Arguments to pass to PM2.
|
|
375
|
+
*/
|
|
376
|
+
async function pm2Command(pm2Args = []) {
|
|
377
|
+
checkComposeFile();
|
|
378
|
+
const subCommand = pm2Args[0] || "status";
|
|
379
|
+
const cmdArgs = ["compose", "exec", "app", "pm2", subCommand, ...pm2Args.slice(1)];
|
|
380
|
+
const code = await runCmd("docker", cmdArgs);
|
|
381
|
+
if (code !== 0) {
|
|
382
|
+
console.error(chalk.red("Error running PM2 command. Make sure the production stack is started."));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Handles interactive shell connections into the Redis container.
|
|
389
|
+
*/
|
|
390
|
+
async function redisCommand() {
|
|
391
|
+
checkComposeFile();
|
|
392
|
+
const cmdArgs = ["compose", "exec", "redis", "redis-cli"];
|
|
393
|
+
await runCmd("docker", cmdArgs);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Entry point for Blue Bird CLI Docker subcommands.
|
|
398
|
+
*/
|
|
399
|
+
async function main() {
|
|
400
|
+
const env = getEnvVars();
|
|
401
|
+
let projectName = env.BLUEBIRD_PROJECT_NAME;
|
|
402
|
+
if (!projectName && env.TITLE) {
|
|
403
|
+
projectName = env.TITLE.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
|
|
404
|
+
}
|
|
405
|
+
if (!projectName) {
|
|
406
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
407
|
+
if (fs.existsSync(pkgPath)) {
|
|
408
|
+
try {
|
|
409
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
410
|
+
if (pkg.name) {
|
|
411
|
+
projectName = pkg.name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
|
|
412
|
+
}
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!projectName) {
|
|
417
|
+
projectName = "bluebird";
|
|
418
|
+
}
|
|
419
|
+
process.env.BLUEBIRD_PROJECT_NAME = projectName;
|
|
420
|
+
process.env.TITLE = projectName;
|
|
421
|
+
|
|
422
|
+
const args = process.argv.slice(3);
|
|
423
|
+
const command = args[0];
|
|
424
|
+
|
|
425
|
+
if (!command) {
|
|
426
|
+
await psCommand();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
switch (command) {
|
|
431
|
+
case "start":
|
|
432
|
+
await startCommand(args[1]);
|
|
433
|
+
break;
|
|
434
|
+
case "dev":
|
|
435
|
+
await devCommand();
|
|
436
|
+
break;
|
|
437
|
+
case "stop":
|
|
438
|
+
await stopCommand(args[1]);
|
|
439
|
+
break;
|
|
440
|
+
case "build":
|
|
441
|
+
await buildCommand(args.slice(1));
|
|
442
|
+
break;
|
|
443
|
+
case "ps":
|
|
444
|
+
case "status":
|
|
445
|
+
await psCommand();
|
|
446
|
+
break;
|
|
447
|
+
case "logs":
|
|
448
|
+
await logsCommand(args[1], args[2]);
|
|
449
|
+
break;
|
|
450
|
+
case "pm2":
|
|
451
|
+
await pm2Command(args.slice(1));
|
|
452
|
+
break;
|
|
453
|
+
case "redis":
|
|
454
|
+
await redisCommand();
|
|
455
|
+
break;
|
|
456
|
+
case "mysql":
|
|
457
|
+
case "postgres":
|
|
458
|
+
case "psql":
|
|
459
|
+
case "db": {
|
|
460
|
+
let user, password, db, root = false;
|
|
461
|
+
for (let i = 1; i < args.length; i++) {
|
|
462
|
+
if (args[i] === "-u" || args[i] === "--user") user = args[++i];
|
|
463
|
+
else if (args[i] === "-p" || args[i] === "--password") password = args[++i];
|
|
464
|
+
else if (args[i] === "-d" || args[i] === "--db") db = args[++i];
|
|
465
|
+
else if (args[i] === "--root") root = true;
|
|
466
|
+
}
|
|
467
|
+
await dbClientCommand(user, password, db, root, command);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case "df":
|
|
471
|
+
case "disk":
|
|
472
|
+
console.log(chalk.cyan("📊 Docker Disk Usage:"));
|
|
473
|
+
await runCmd("docker", ["system", "df"]);
|
|
474
|
+
break;
|
|
475
|
+
case "prune":
|
|
476
|
+
case "clean": {
|
|
477
|
+
const force = args.includes("-f") || args.includes("--force");
|
|
478
|
+
const all = args.includes("-a") || args.includes("--all");
|
|
479
|
+
await pruneCommand(force, all);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
default:
|
|
483
|
+
console.log(chalk.yellow(`Unknown docker command: ${command}`));
|
|
484
|
+
console.log("Available commands: dev, start, stop, build, ps, logs, pm2, mysql/postgres/db, redis, df/disk, prune/clean");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
main();
|