@seip/blue-bird 0.6.4 → 0.7.1
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 +9 -10
- package/AGENTS.md +54 -158
- package/README.md +63 -135
- package/backend/routes/api.js +21 -17
- package/core/app.js +86 -81
- package/core/cache.js +117 -29
- package/core/cli/docker.js +47 -8
- package/core/cli/init.js +120 -11
- package/core/logger.js +77 -78
- package/core/router.js +2 -6
- package/docker/Dockerfile +3 -12
- package/docker/nginx.conf +83 -0
- package/docker-compose.yml +42 -19
- package/frontend/astro.config.mjs +35 -0
- package/frontend/public/css/app.css +319 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/http/api.js +24 -0
- package/frontend/src/layouts/Layout.astro +20 -0
- package/frontend/src/pages/about.astro +54 -0
- package/frontend/src/pages/index.astro +110 -0
- package/{backend/index.js → index.js} +11 -4
- package/package.json +16 -4
- package/backend/routes/frontend.js +0 -39
- package/core/seo.js +0 -113
- package/core/template.js +0 -319
- package/frontend/public/js/blue-bird.js +0 -1465
- package/frontend/public/js/tailwind.js +0 -8
- package/frontend/templates/about.html +0 -105
- package/frontend/templates/index.html +0 -146
- package/frontend/templates/preact_example.html +0 -80
package/backend/routes/api.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import Router from "@seip/blue-bird/core/router.js";
|
|
2
2
|
import Validator from "@seip/blue-bird/core/validate.js";
|
|
3
3
|
import Cache from "@seip/blue-bird/core/cache.js";
|
|
4
|
-
import Auth from "@seip/blue-bird/core/auth.js"
|
|
4
|
+
import Auth from "@seip/blue-bird/core/auth.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const routerApi = new Router("/api");
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
routerApi.get("//", (req, res) => {
|
|
9
|
+
res.json({ api: true, message: "Bluebird API", time: Date.now() });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
routerApi.get("/users", (req, res) => {
|
|
9
13
|
const users = [
|
|
10
14
|
{
|
|
11
15
|
name: "John Doe",
|
|
@@ -26,28 +30,28 @@ const loginSchema = {
|
|
|
26
30
|
|
|
27
31
|
const loginValidator = new Validator(loginSchema);
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
routerApi.post("/login", loginValidator.middleware(), (req, res) => {
|
|
30
34
|
res.json({ message: "Login successful", body: req.body });
|
|
31
35
|
});
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
37
|
+
routerApi.get("/cache", Cache.middleware(), async (req, res) => {
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
35
39
|
res.json({ message: "Cache successful" });
|
|
36
|
-
})
|
|
40
|
+
});
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
const token = await Auth.login(res, { id: 1, name: "John Doe" })
|
|
42
|
+
routerApi.get("/auth_generate", async (req, res) => {
|
|
43
|
+
const token = await Auth.login(res, { id: 1, name: "John Doe" });
|
|
40
44
|
res.json({ message: "Auth successful", token });
|
|
41
|
-
})
|
|
45
|
+
});
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
await Auth.logout(res)
|
|
47
|
+
routerApi.get("/auth_logout", async (req, res) => {
|
|
48
|
+
await Auth.logout(res);
|
|
45
49
|
res.json({ message: "Auth successful" });
|
|
46
|
-
})
|
|
50
|
+
});
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
const userInfo = req.user
|
|
52
|
+
routerApi.get("/auth_verify", Auth.protect(), (req, res) => {
|
|
53
|
+
const userInfo = req.user;
|
|
50
54
|
res.json({ message: "Auth successful", user: userInfo });
|
|
51
|
-
})
|
|
55
|
+
});
|
|
52
56
|
|
|
53
|
-
export default
|
|
57
|
+
export default routerApi;
|
package/core/app.js
CHANGED
|
@@ -10,8 +10,6 @@ import compression from "compression";
|
|
|
10
10
|
import Config from "./config.js";
|
|
11
11
|
import Logger from "./logger.js";
|
|
12
12
|
import Debug from "./debug.js";
|
|
13
|
-
import Template from "./template.js";
|
|
14
|
-
import SEO from "./seo.js";
|
|
15
13
|
|
|
16
14
|
const __dirname = Config.dirname();
|
|
17
15
|
const props = Config.props();
|
|
@@ -36,7 +34,8 @@ class App {
|
|
|
36
34
|
* @param {boolean} [options.cookieParser=true] - Whether to enable cookie parsing.
|
|
37
35
|
* @param {boolean|Object} [options.rateLimit=false] - Enable global rate limiting.
|
|
38
36
|
* @param {boolean|Object} [options.swagger=false] - Enable swagger.
|
|
39
|
-
* @param {boolean} [options.compression=true] - Enable
|
|
37
|
+
* @param {boolean} [options.compression=true] - Enable compression.
|
|
38
|
+
* @param {boolean} [options.astro=true] - Astro handler.
|
|
40
39
|
* @example
|
|
41
40
|
* const app = new App({
|
|
42
41
|
* routes: [],
|
|
@@ -55,7 +54,14 @@ class App {
|
|
|
55
54
|
* info: { title: "Blue Bird API", version: "1.0.0", description: "API Documentation" },
|
|
56
55
|
* url: "http://localhost:8000"
|
|
57
56
|
* },
|
|
58
|
-
* compression:
|
|
57
|
+
* compression:true,
|
|
58
|
+
* astro: {
|
|
59
|
+
* server: true,
|
|
60
|
+
* serverEntry: "./frontend/dist/server/entry.mjs",
|
|
61
|
+
* client: false,
|
|
62
|
+
* clientDir: "./frontend/dist/client",
|
|
63
|
+
* base: "/"
|
|
64
|
+
* }
|
|
59
65
|
* });
|
|
60
66
|
*/
|
|
61
67
|
constructor(options = {}) {
|
|
@@ -75,6 +81,7 @@ class App {
|
|
|
75
81
|
this.rateLimit = options.rateLimit ?? false;
|
|
76
82
|
this.swagger = options.swagger ?? false;
|
|
77
83
|
this.compression = options.compression ?? true;
|
|
84
|
+
this.astro = options.astro || false;
|
|
78
85
|
this.loggerInstance = new Logger();
|
|
79
86
|
/** @type {Set<import('http').ServerResponse>} */
|
|
80
87
|
this._hotReloadClients = new Set();
|
|
@@ -121,13 +128,16 @@ class App {
|
|
|
121
128
|
|
|
122
129
|
if (this.static.path)
|
|
123
130
|
this.app.use(
|
|
124
|
-
express.static(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
res.setHeader("X-Powered-By", "Blue Bird");
|
|
128
|
-
res.setHeader(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
express.static(path.join(__dirname, this.static.path), {
|
|
132
|
+
...this.static.options,
|
|
133
|
+
setHeaders: (res) => {
|
|
134
|
+
res.setHeader("X-Powered-By", "Blue Bird");
|
|
135
|
+
res.setHeader(
|
|
136
|
+
"Cache-Control",
|
|
137
|
+
"public, max-age=31536000, immutable",
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
131
141
|
);
|
|
132
142
|
|
|
133
143
|
this.app.use(cors(this.cors));
|
|
@@ -165,13 +175,12 @@ class App {
|
|
|
165
175
|
if (this.logger || props.debug) this._middlewareLogger(this.logger);
|
|
166
176
|
|
|
167
177
|
this.app.use((req, res, next) => {
|
|
168
|
-
res.setHeader("X-Powered-By", "Blue Bird");
|
|
169
|
-
|
|
178
|
+
res.setHeader("X-Powered-By", "Blue Bird");
|
|
179
|
+
next();
|
|
170
180
|
});
|
|
171
181
|
|
|
172
182
|
if (props.debug) {
|
|
173
183
|
Debug.middlewareMetrics(this.app);
|
|
174
|
-
this._setupHotReload();
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
if (this.swagger) {
|
|
@@ -196,70 +205,65 @@ class App {
|
|
|
196
205
|
|
|
197
206
|
this._dispatchRoutes();
|
|
198
207
|
|
|
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
|
-
|
|
208
|
+
if (this.astro) {
|
|
209
|
+
const defaultAstro = {
|
|
210
|
+
server: true,
|
|
211
|
+
serverEntry: "./frontend/dist/server/entry.mjs",
|
|
212
|
+
client: false,
|
|
213
|
+
clientDir: "./frontend/dist/client",
|
|
214
|
+
base: "/",
|
|
215
|
+
};
|
|
216
|
+
const astroConfig =
|
|
217
|
+
typeof this.astro === "object"
|
|
218
|
+
? { ...defaultAstro, ...this.astro }
|
|
219
|
+
: { ...defaultAstro };
|
|
220
|
+
|
|
221
|
+
if (astroConfig.client) {
|
|
222
|
+
const clientPath = path.resolve(astroConfig.clientDir);
|
|
223
|
+
if (fs.existsSync(clientPath)) {
|
|
224
|
+
this.app.use(astroConfig.base, express.static(clientPath));
|
|
225
|
+
console.log(
|
|
226
|
+
chalk.green(
|
|
227
|
+
`[OK] Astro Static Client Assets registered at ${astroConfig.base}`,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
console.warn(
|
|
232
|
+
chalk.yellow(
|
|
233
|
+
`[WARN] Astro client directory not found at: ${clientPath}`,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
225
237
|
}
|
|
226
|
-
this._hotReloadClients.add(res);
|
|
227
|
-
req.on("close", () => {
|
|
228
|
-
this._hotReloadClients.delete(res);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
238
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
if (astroConfig.server) {
|
|
240
|
+
const entryPath = path.resolve(astroConfig.serverEntry);
|
|
241
|
+
if (fs.existsSync(entryPath)) {
|
|
242
|
+
try {
|
|
243
|
+
const { handler: ssrHandler } = await import(entryPath);
|
|
244
|
+
this.app.use(ssrHandler);
|
|
245
|
+
console.log(
|
|
246
|
+
chalk.green("[OK] Astro SSR Handler registered successfully."),
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(
|
|
250
|
+
chalk.red("[ERROR] Failed to load Astro SSR Handler:"),
|
|
251
|
+
error.message,
|
|
252
|
+
);
|
|
241
253
|
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
fs.watch(frontendPath, { recursive: true }, (eventType, filename) => {
|
|
250
|
-
if (!filename) return;
|
|
251
|
-
if (/\.(html|css|js)$/i.test(filename)) {
|
|
252
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
253
|
-
debounceTimer = setTimeout(() => {
|
|
254
|
-
console.log(chalk.magenta(`[Hot Reload] ${filename} changed`));
|
|
255
|
-
Template.clearCache();
|
|
256
|
-
notifyClients();
|
|
257
|
-
}, 200);
|
|
254
|
+
} else {
|
|
255
|
+
console.warn(
|
|
256
|
+
chalk.yellow(
|
|
257
|
+
`[WARN] Astro build entrypoint not found at: ${entryPath}`,
|
|
258
|
+
),
|
|
259
|
+
);
|
|
258
260
|
}
|
|
259
|
-
}
|
|
260
|
-
} catch (_) {
|
|
261
|
-
console.log(chalk.yellow("[Hot Reload] Could not watch frontend/ directory"));
|
|
261
|
+
}
|
|
262
262
|
}
|
|
263
|
+
|
|
264
|
+
if (this.notFound) this._notFoundDefault();
|
|
265
|
+
|
|
266
|
+
this._errorHandler();
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
/**
|
|
@@ -279,6 +283,7 @@ class App {
|
|
|
279
283
|
Object.keys(req.params).length > 0
|
|
280
284
|
? ` ${JSON.stringify(req.params)}`
|
|
281
285
|
: "";
|
|
286
|
+
|
|
282
287
|
const ip = req.ip;
|
|
283
288
|
const now = new Date().toISOString();
|
|
284
289
|
const time = `${now.split("T")[0]} ${now.split("T")[1].split(".")[0]}`;
|
|
@@ -357,14 +362,14 @@ class App {
|
|
|
357
362
|
this.app.listen(this.port, () => {
|
|
358
363
|
console.log(
|
|
359
364
|
chalk.bold.blue("Blue Bird Server Online\n") +
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
chalk.bold.cyan("App URL: ") +
|
|
366
|
+
chalk.green(`${this.appUrl}`) +
|
|
367
|
+
"\n" +
|
|
368
|
+
chalk.bold.cyan("Internal: ") +
|
|
369
|
+
chalk.green(`${this.host}:${this.port}`) +
|
|
370
|
+
"\n" +
|
|
371
|
+
(props.debug ? chalk.bold.magenta("Hot Reload: enabled\n") : "") +
|
|
372
|
+
chalk.gray("────────────────────────────────"),
|
|
368
373
|
);
|
|
369
374
|
});
|
|
370
375
|
})
|
package/core/cache.js
CHANGED
|
@@ -1,38 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
1
3
|
const CACHE = {};
|
|
2
4
|
|
|
5
|
+
let redisClient = null;
|
|
6
|
+
let isRedisConnected = false;
|
|
7
|
+
const redisHost = process.env.REDIS_HOST ?? false;
|
|
8
|
+
const redisPort = process.env.REDIS_PORT ?? 6379;
|
|
9
|
+
const redisPassword = process.env.REDIS_PASSWORD || "";
|
|
10
|
+
const redisUrl = redisPassword
|
|
11
|
+
? `redis://:${redisPassword}@${redisHost}:${redisPort}`
|
|
12
|
+
: `redis://${redisHost}:${redisPort}`;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initializes the Redis client connection if REDIS_HOST env is set.
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
async function initRedis() {
|
|
19
|
+
if (redisClient) return;
|
|
20
|
+
if (!redisHost) return;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { createClient } = await import("redis");
|
|
24
|
+
let host = redisHost;
|
|
25
|
+
if (host === "localhost" && fs.existsSync("/.dockerenv")) {
|
|
26
|
+
host = "redis";
|
|
27
|
+
}
|
|
28
|
+
const url = redisUrl;
|
|
29
|
+
|
|
30
|
+
redisClient = createClient({ url });
|
|
31
|
+
redisClient.on("error", () => {
|
|
32
|
+
isRedisConnected = false;
|
|
33
|
+
});
|
|
34
|
+
await redisClient.connect();
|
|
35
|
+
isRedisConnected = true;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
redisClient = null;
|
|
38
|
+
isRedisConnected = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
initRedis().catch(() => {});
|
|
43
|
+
|
|
3
44
|
setInterval(() => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
for (const key in CACHE) {
|
|
47
|
+
if (CACHE[key].expiry <= now) {
|
|
48
|
+
delete CACHE[key];
|
|
9
49
|
}
|
|
50
|
+
}
|
|
10
51
|
}, 300000).unref();
|
|
52
|
+
|
|
11
53
|
/**
|
|
12
|
-
*
|
|
13
|
-
* Caches JSON responses based on the request URL.
|
|
54
|
+
* High-performance Caching class supporting both local memory and Redis backends.
|
|
14
55
|
*/
|
|
15
56
|
class Cache {
|
|
16
57
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param {number} [seconds=60] -
|
|
19
|
-
* @returns {Function} Express middleware
|
|
20
|
-
* @example
|
|
21
|
-
* router.get("/stats", Cache.middleware(120), (req, res) => {
|
|
22
|
-
* res.json({ ok: true });
|
|
23
|
-
* });
|
|
58
|
+
* Express middleware to cache route JSON and HTML responses.
|
|
59
|
+
* @param {number} [seconds=60] - Expiry time in seconds.
|
|
60
|
+
* @returns {Function} Express middleware.
|
|
24
61
|
*/
|
|
25
62
|
static middleware(seconds = 60) {
|
|
26
|
-
return (req, res, next) => {
|
|
63
|
+
return async (req, res, next) => {
|
|
27
64
|
const key = req.originalUrl;
|
|
28
65
|
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
if (redisHost && !redisClient) {
|
|
67
|
+
await initRedis().catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isRedisConnected && redisClient) {
|
|
71
|
+
try {
|
|
72
|
+
const cachedData = await redisClient.get(key);
|
|
73
|
+
if (cachedData) {
|
|
74
|
+
const cached = JSON.parse(cachedData);
|
|
75
|
+
if (cached.type === "json") {
|
|
76
|
+
return res.json(cached.data);
|
|
77
|
+
} else {
|
|
78
|
+
res.type("text/html");
|
|
79
|
+
res.set("X-Blue-Bird-Cache", "HIT");
|
|
80
|
+
return res.send(cached.data);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
isRedisConnected = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isRedisConnected || !redisClient) {
|
|
89
|
+
if (CACHE[key] && CACHE[key].expiry > Date.now()) {
|
|
90
|
+
const cached = CACHE[key];
|
|
91
|
+
if (cached.type === "json") {
|
|
92
|
+
res.set("X-Blue-Bird-Cache", "HIT");
|
|
93
|
+
return res.json(cached.data);
|
|
94
|
+
} else {
|
|
95
|
+
res.type("text/html");
|
|
96
|
+
res.set("X-Blue-Bird-Cache", "HIT");
|
|
97
|
+
return res.send(cached.data);
|
|
98
|
+
}
|
|
36
99
|
}
|
|
37
100
|
}
|
|
38
101
|
|
|
@@ -40,33 +103,58 @@ class Cache {
|
|
|
40
103
|
const originalSend = res.send.bind(res);
|
|
41
104
|
let cachedInRequest = false;
|
|
42
105
|
|
|
43
|
-
res.json = (body) => {
|
|
106
|
+
res.json = async (body) => {
|
|
44
107
|
if (!cachedInRequest) {
|
|
45
|
-
|
|
108
|
+
cachedInRequest = true;
|
|
109
|
+
const cacheObject = {
|
|
46
110
|
type: "json",
|
|
47
111
|
data: body,
|
|
48
112
|
expiry: Date.now() + seconds * 1000,
|
|
49
113
|
};
|
|
50
|
-
|
|
114
|
+
if (isRedisConnected && redisClient) {
|
|
115
|
+
try {
|
|
116
|
+
await redisClient.set(key, JSON.stringify(cacheObject), {
|
|
117
|
+
EX: seconds,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
CACHE[key] = cacheObject;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
CACHE[key] = cacheObject;
|
|
124
|
+
}
|
|
51
125
|
}
|
|
126
|
+
res.set("X-Blue-Bird-Cache", "MISS");
|
|
52
127
|
return originalJson(body);
|
|
53
128
|
};
|
|
54
129
|
|
|
55
|
-
res.send = (body) => {
|
|
130
|
+
res.send = async (body) => {
|
|
56
131
|
if (!cachedInRequest && typeof body === "string") {
|
|
57
|
-
|
|
132
|
+
cachedInRequest = true;
|
|
133
|
+
const cacheObject = {
|
|
58
134
|
type: "html",
|
|
59
135
|
data: body,
|
|
60
136
|
expiry: Date.now() + seconds * 1000,
|
|
61
137
|
};
|
|
62
|
-
|
|
138
|
+
if (isRedisConnected && redisClient) {
|
|
139
|
+
try {
|
|
140
|
+
await redisClient.set(key, JSON.stringify(cacheObject), {
|
|
141
|
+
EX: seconds,
|
|
142
|
+
});
|
|
143
|
+
} catch (err) {
|
|
144
|
+
CACHE[key] = cacheObject;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
CACHE[key] = cacheObject;
|
|
148
|
+
}
|
|
63
149
|
}
|
|
150
|
+
res.set("X-Blue-Bird-Cache", "MISS");
|
|
64
151
|
return originalSend(body);
|
|
65
152
|
};
|
|
66
153
|
|
|
154
|
+
res.set("X-Blue-Bird-Cache", "MISS");
|
|
67
155
|
next();
|
|
68
156
|
};
|
|
69
157
|
}
|
|
70
158
|
}
|
|
71
159
|
|
|
72
|
-
export default Cache;
|
|
160
|
+
export default Cache;
|
package/core/cli/docker.js
CHANGED
|
@@ -58,27 +58,43 @@ async function startCommand(service) {
|
|
|
58
58
|
checkComposeFile();
|
|
59
59
|
|
|
60
60
|
if (service === "mysql" || service === "--mysql" || service === "dev") {
|
|
61
|
-
console.log(chalk.cyan("Starting MySQL container
|
|
61
|
+
console.log(chalk.cyan("Starting MySQL container..."));
|
|
62
62
|
const code = await runCmd("docker", ["compose", "up", "-d", "mysql"]);
|
|
63
63
|
if (code === 0) {
|
|
64
|
-
console.log(chalk.green("MySQL started.
|
|
65
|
-
console.log(chalk.blue("To also run the app in Docker (production), use: npx blue-bird docker start prod"));
|
|
64
|
+
console.log(chalk.green("MySQL started."));
|
|
66
65
|
} else {
|
|
67
66
|
console.error(chalk.red("Error starting MySQL."));
|
|
68
67
|
process.exit(1);
|
|
69
68
|
}
|
|
69
|
+
} else if (service === "redis" || service === "--redis") {
|
|
70
|
+
console.log(chalk.cyan("Starting Redis container..."));
|
|
71
|
+
const code = await runCmd("docker", ["compose", "up", "-d", "redis"]);
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
console.log(chalk.green("Redis started."));
|
|
74
|
+
} else {
|
|
75
|
+
console.error(chalk.red("Error starting Redis."));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
} else if (service === "dbs" || service === "databases") {
|
|
79
|
+
console.log(chalk.cyan("Starting Database containers (MySQL + Redis)..."));
|
|
80
|
+
const code = await runCmd("docker", ["compose", "up", "-d", "mysql", "redis"]);
|
|
81
|
+
if (code === 0) {
|
|
82
|
+
console.log(chalk.green("Database containers started."));
|
|
83
|
+
} else {
|
|
84
|
+
console.error(chalk.red("Error starting databases."));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
70
87
|
} else if (service === "prod" || service === "app" || service === "--app" || !service) {
|
|
71
|
-
console.log(chalk.cyan("Starting production stack (MySQL +
|
|
88
|
+
console.log(chalk.cyan("Starting production stack (MySQL + Redis + App + Nginx)..."));
|
|
72
89
|
const code = await runCmd("docker", ["compose", "--profile", "prod", "up", "-d"]);
|
|
73
90
|
if (code === 0) {
|
|
74
91
|
console.log(chalk.green("Production stack started."));
|
|
75
|
-
console.log(chalk.blue("View logs with: npx blue-bird docker logs"));
|
|
76
92
|
} else {
|
|
77
93
|
console.error(chalk.red("Error starting production stack."));
|
|
78
94
|
process.exit(1);
|
|
79
95
|
}
|
|
80
96
|
} else {
|
|
81
|
-
console.error(chalk.red(`Unknown service '${service}'. Use: mysql
|
|
97
|
+
console.error(chalk.red(`Unknown service '${service}'. Use: mysql, redis, dbs, prod.`));
|
|
82
98
|
process.exit(1);
|
|
83
99
|
}
|
|
84
100
|
}
|
|
@@ -99,13 +115,18 @@ async function stopCommand(service) {
|
|
|
99
115
|
await runCmd("docker", ["compose", "stop", "mysql"]);
|
|
100
116
|
await runCmd("docker", ["compose", "rm", "-f", "mysql"]);
|
|
101
117
|
console.log(chalk.green("MySQL stopped."));
|
|
118
|
+
} else if (service === "redis" || service === "--redis") {
|
|
119
|
+
console.log(chalk.cyan("Stopping Redis..."));
|
|
120
|
+
await runCmd("docker", ["compose", "stop", "redis"]);
|
|
121
|
+
await runCmd("docker", ["compose", "rm", "-f", "redis"]);
|
|
122
|
+
console.log(chalk.green("Redis stopped."));
|
|
102
123
|
} else if (service === "app" || service === "--app") {
|
|
103
124
|
console.log(chalk.cyan("Stopping Node.js app container..."));
|
|
104
125
|
await runCmd("docker", ["compose", "--profile", "prod", "stop", "app"]);
|
|
105
126
|
await runCmd("docker", ["compose", "--profile", "prod", "rm", "-f", "app"]);
|
|
106
127
|
console.log(chalk.green("App container stopped."));
|
|
107
128
|
} else {
|
|
108
|
-
console.error(chalk.red(`Unknown service '${service}'. Use: all
|
|
129
|
+
console.error(chalk.red(`Unknown service '${service}'. Use: all, mysql, redis, app.`));
|
|
109
130
|
process.exit(1);
|
|
110
131
|
}
|
|
111
132
|
}
|
|
@@ -235,6 +256,21 @@ async function pruneCommand(forceOpt, allOpt) {
|
|
|
235
256
|
await runCmd("docker", ["system", "df"]);
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Executes PM2 commands inside the Node.js application container.
|
|
261
|
+
* @param {string[]} pm2Args - Arguments to pass to PM2.
|
|
262
|
+
*/
|
|
263
|
+
async function pm2Command(pm2Args = []) {
|
|
264
|
+
checkComposeFile();
|
|
265
|
+
const subCommand = pm2Args[0] || "status";
|
|
266
|
+
const cmdArgs = ["compose", "exec", "app", "pm2", subCommand, ...pm2Args.slice(1)];
|
|
267
|
+
const code = await runCmd("docker", cmdArgs);
|
|
268
|
+
if (code !== 0) {
|
|
269
|
+
console.error(chalk.red("Error running PM2 command. Make sure the production stack is started."));
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
238
274
|
/**
|
|
239
275
|
* Entry point for Blue Bird CLI Docker subcommands.
|
|
240
276
|
*/
|
|
@@ -286,6 +322,9 @@ async function main() {
|
|
|
286
322
|
case "logs":
|
|
287
323
|
await logsCommand(args[1], args[2]);
|
|
288
324
|
break;
|
|
325
|
+
case "pm2":
|
|
326
|
+
await pm2Command(args.slice(1));
|
|
327
|
+
break;
|
|
289
328
|
case "mysql":
|
|
290
329
|
case "db": {
|
|
291
330
|
let user, password, db, root = false;
|
|
@@ -312,7 +351,7 @@ async function main() {
|
|
|
312
351
|
}
|
|
313
352
|
default:
|
|
314
353
|
console.log(chalk.yellow(`Unknown docker command: ${command}`));
|
|
315
|
-
console.log("Available commands: start, stop, build, ps, logs, mysql/db, df/disk, prune/clean");
|
|
354
|
+
console.log("Available commands: start, stop, build, ps, logs, pm2, mysql/db, df/disk, prune/clean");
|
|
316
355
|
}
|
|
317
356
|
}
|
|
318
357
|
|