@seip/blue-bird 0.6.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 +38 -0
- package/AGENTS.md +276 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/backend/index.js +21 -0
- package/backend/routes/api.js +53 -0
- package/backend/routes/frontend.js +29 -0
- package/core/app.js +397 -0
- package/core/auth.js +180 -0
- package/core/cache.js +72 -0
- package/core/cli/docker.js +319 -0
- package/core/cli/init.js +130 -0
- package/core/cli/route.js +43 -0
- package/core/cli/swagger.js +40 -0
- package/core/config.js +52 -0
- package/core/debug.js +249 -0
- package/core/logger.js +100 -0
- package/core/middleware.js +27 -0
- package/core/router.js +148 -0
- package/core/seo.js +113 -0
- package/core/swagger.js +40 -0
- package/core/template.js +254 -0
- package/core/upload.js +77 -0
- package/core/validate.js +380 -0
- package/docker/Dockerfile +25 -0
- package/docker-compose.yml +70 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/public/js/spa.js +183 -0
- package/frontend/public/js/tailwind.js +8 -0
- package/frontend/templates/about.html +101 -0
- package/frontend/templates/index.html +140 -0
- package/package.json +59 -0
package/core/cache.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const CACHE = {};
|
|
2
|
+
|
|
3
|
+
setInterval(() => {
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
for (const key in CACHE) {
|
|
6
|
+
if (CACHE[key].expiry <= now) {
|
|
7
|
+
delete CACHE[key];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}, 300000).unref();
|
|
11
|
+
/**
|
|
12
|
+
* Simple in-memory Cache class to provide middleware for Express routes.
|
|
13
|
+
* Caches JSON responses based on the request URL.
|
|
14
|
+
*/
|
|
15
|
+
class Cache {
|
|
16
|
+
/**
|
|
17
|
+
* Middleware to cache responses.
|
|
18
|
+
* @param {number} [seconds=60] - Number of seconds to cache the response.
|
|
19
|
+
* @returns {Function} Express middleware function.
|
|
20
|
+
* @example
|
|
21
|
+
* router.get("/stats", Cache.middleware(120), (req, res) => {
|
|
22
|
+
* res.json({ ok: true });
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
static middleware(seconds = 60) {
|
|
26
|
+
return (req, res, next) => {
|
|
27
|
+
const key = req.originalUrl;
|
|
28
|
+
|
|
29
|
+
if (CACHE[key] && CACHE[key].expiry > Date.now()) {
|
|
30
|
+
const cached = CACHE[key];
|
|
31
|
+
if (cached.type === "json") {
|
|
32
|
+
return res.json(cached.data);
|
|
33
|
+
} else {
|
|
34
|
+
res.type("text/html");
|
|
35
|
+
return res.send(cached.data);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const originalJson = res.json.bind(res);
|
|
40
|
+
const originalSend = res.send.bind(res);
|
|
41
|
+
let cachedInRequest = false;
|
|
42
|
+
|
|
43
|
+
res.json = (body) => {
|
|
44
|
+
if (!cachedInRequest) {
|
|
45
|
+
CACHE[key] = {
|
|
46
|
+
type: "json",
|
|
47
|
+
data: body,
|
|
48
|
+
expiry: Date.now() + seconds * 1000,
|
|
49
|
+
};
|
|
50
|
+
cachedInRequest = true;
|
|
51
|
+
}
|
|
52
|
+
return originalJson(body);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
res.send = (body) => {
|
|
56
|
+
if (!cachedInRequest && typeof body === "string") {
|
|
57
|
+
CACHE[key] = {
|
|
58
|
+
type: "html",
|
|
59
|
+
data: body,
|
|
60
|
+
expiry: Date.now() + seconds * 1000,
|
|
61
|
+
};
|
|
62
|
+
cachedInRequest = true;
|
|
63
|
+
}
|
|
64
|
+
return originalSend(body);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default Cache;
|
|
@@ -0,0 +1,319 @@
|
|
|
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
|
+
* Spawns a child process and inherits standard IO for interactive terminal sessions.
|
|
30
|
+
* @param {string} command - The binary to execute.
|
|
31
|
+
* @param {string[]} args - Argument list.
|
|
32
|
+
* @returns {Promise<number>} Exit code of the process.
|
|
33
|
+
*/
|
|
34
|
+
function runCmd(command, args) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const proc = spawn(command, args, { stdio: "inherit", env: process.env });
|
|
37
|
+
proc.on("close", (code) => {
|
|
38
|
+
resolve(code || 0);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Confirms that a docker-compose.yml file is present in the workspace directory.
|
|
45
|
+
*/
|
|
46
|
+
function checkComposeFile() {
|
|
47
|
+
if (!fs.existsSync("docker-compose.yml")) {
|
|
48
|
+
console.error(chalk.red("Error: docker-compose.yml not found in the current directory."));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handles the 'start' CLI command.
|
|
55
|
+
* @param {string} service - Service to boot up.
|
|
56
|
+
*/
|
|
57
|
+
async function startCommand(service) {
|
|
58
|
+
checkComposeFile();
|
|
59
|
+
|
|
60
|
+
if (service === "mysql" || service === "--mysql" || service === "dev") {
|
|
61
|
+
console.log(chalk.cyan("Starting MySQL container (dev mode)..."));
|
|
62
|
+
const code = await runCmd("docker", ["compose", "up", "-d", "mysql"]);
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
console.log(chalk.green("MySQL started. Run your app locally with: npm run dev"));
|
|
65
|
+
console.log(chalk.blue("To also run the app in Docker (production), use: npx blue-bird docker start prod"));
|
|
66
|
+
} else {
|
|
67
|
+
console.error(chalk.red("Error starting MySQL."));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
} else if (service === "prod" || service === "app" || service === "--app" || !service) {
|
|
71
|
+
console.log(chalk.cyan("Starting production stack (MySQL + Node.js app)..."));
|
|
72
|
+
const code = await runCmd("docker", ["compose", "--profile", "prod", "up", "-d"]);
|
|
73
|
+
if (code === 0) {
|
|
74
|
+
console.log(chalk.green("Production stack started."));
|
|
75
|
+
console.log(chalk.blue("View logs with: npx blue-bird docker logs"));
|
|
76
|
+
} else {
|
|
77
|
+
console.error(chalk.red("Error starting production stack."));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
console.error(chalk.red(`Unknown service '${service}'. Use: mysql (or --mysql), prod (or --app), or no arguments for both.`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handles the 'stop' CLI command.
|
|
88
|
+
* @param {string} service - Service to stop.
|
|
89
|
+
*/
|
|
90
|
+
async function stopCommand(service) {
|
|
91
|
+
checkComposeFile();
|
|
92
|
+
|
|
93
|
+
if (!service || service === "all") {
|
|
94
|
+
console.log(chalk.cyan("Stopping all Blue Bird containers..."));
|
|
95
|
+
await runCmd("docker", ["compose", "--profile", "prod", "down"]);
|
|
96
|
+
console.log(chalk.green("All containers stopped."));
|
|
97
|
+
} else if (service === "mysql" || service === "--mysql") {
|
|
98
|
+
console.log(chalk.cyan("Stopping MySQL..."));
|
|
99
|
+
await runCmd("docker", ["compose", "stop", "mysql"]);
|
|
100
|
+
await runCmd("docker", ["compose", "rm", "-f", "mysql"]);
|
|
101
|
+
console.log(chalk.green("MySQL stopped."));
|
|
102
|
+
} else if (service === "app" || service === "--app") {
|
|
103
|
+
console.log(chalk.cyan("Stopping Node.js app container..."));
|
|
104
|
+
await runCmd("docker", ["compose", "--profile", "prod", "stop", "app"]);
|
|
105
|
+
await runCmd("docker", ["compose", "--profile", "prod", "rm", "-f", "app"]);
|
|
106
|
+
console.log(chalk.green("App container stopped."));
|
|
107
|
+
} else {
|
|
108
|
+
console.error(chalk.red(`Unknown service '${service}'. Use: all (default), mysql, app.`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Handles the 'build' CLI command.
|
|
115
|
+
* @param {string[]} options - Build configuration options.
|
|
116
|
+
*/
|
|
117
|
+
async function buildCommand(options = []) {
|
|
118
|
+
checkComposeFile();
|
|
119
|
+
console.log(chalk.cyan("Building Blue Bird app Docker image..."));
|
|
120
|
+
const cmdArgs = ["compose", "--profile", "prod", "build"];
|
|
121
|
+
if (options.includes("--no-cache") || options.includes("-n")) {
|
|
122
|
+
cmdArgs.push("--no-cache");
|
|
123
|
+
}
|
|
124
|
+
cmdArgs.push("app");
|
|
125
|
+
|
|
126
|
+
const code = await runCmd("docker", cmdArgs);
|
|
127
|
+
if (code === 0) {
|
|
128
|
+
console.log(chalk.green("Image built successfully. Start with: npx blue-bird docker start prod"));
|
|
129
|
+
} else {
|
|
130
|
+
console.error(chalk.red("Build error."));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handles the 'ps' CLI command.
|
|
137
|
+
*/
|
|
138
|
+
async function psCommand() {
|
|
139
|
+
checkComposeFile();
|
|
140
|
+
console.log(chalk.cyan("Active Blue Bird Containers:"));
|
|
141
|
+
await runCmd("docker", ["compose", "--profile", "prod", "ps"]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handles the 'logs' CLI command.
|
|
146
|
+
* @param {string} service - Service to pull logs from.
|
|
147
|
+
* @param {string} followOpt - Flag for active log tailing.
|
|
148
|
+
*/
|
|
149
|
+
async function logsCommand(service, followOpt) {
|
|
150
|
+
checkComposeFile();
|
|
151
|
+
const follow = followOpt !== "--no-follow";
|
|
152
|
+
const targetService = service === "mysql" ? "mysql" : "app";
|
|
153
|
+
|
|
154
|
+
const cmdArgs = ["compose"];
|
|
155
|
+
if (targetService === "app") {
|
|
156
|
+
cmdArgs.push("--profile", "prod");
|
|
157
|
+
}
|
|
158
|
+
cmdArgs.push("logs", "--tail=100");
|
|
159
|
+
if (follow) {
|
|
160
|
+
cmdArgs.push("-f");
|
|
161
|
+
}
|
|
162
|
+
cmdArgs.push(targetService);
|
|
163
|
+
|
|
164
|
+
await runCmd("docker", cmdArgs);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handles interactive shell connections into the MySQL container.
|
|
169
|
+
* @param {string} userOpt - DB username.
|
|
170
|
+
* @param {string} passOpt - DB password.
|
|
171
|
+
* @param {string} dbOpt - DB database name.
|
|
172
|
+
* @param {boolean} rootOpt - Flag for overriding database credentials to connect as root.
|
|
173
|
+
*/
|
|
174
|
+
async function mysqlCommand(userOpt, passOpt, dbOpt, rootOpt) {
|
|
175
|
+
checkComposeFile();
|
|
176
|
+
const env = getEnvVars();
|
|
177
|
+
|
|
178
|
+
let dbUser = userOpt;
|
|
179
|
+
let dbPass = passOpt;
|
|
180
|
+
let dbName = dbOpt;
|
|
181
|
+
|
|
182
|
+
if (rootOpt) {
|
|
183
|
+
dbUser = "root";
|
|
184
|
+
dbPass = env.DB_PASSWORD || "root";
|
|
185
|
+
} else {
|
|
186
|
+
dbUser = dbUser || env.DB_USER || "root";
|
|
187
|
+
dbPass = dbPass || env.DB_PASSWORD || "root";
|
|
188
|
+
}
|
|
189
|
+
dbName = dbName || env.DB_NAME || "blue_bird";
|
|
190
|
+
|
|
191
|
+
const cmdArgs = ["compose", "exec", "mysql", "mysql", `-u${dbUser}`];
|
|
192
|
+
if (dbPass) {
|
|
193
|
+
cmdArgs.push(`-p${dbPass}`);
|
|
194
|
+
}
|
|
195
|
+
if (dbName) {
|
|
196
|
+
cmdArgs.push(dbName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const targetDb = dbName ? ` (database: {dbName})` : "";
|
|
200
|
+
console.log(chalk.cyan(`Connecting to MySQL shell in container as '${dbUser}'${targetDb}...`));
|
|
201
|
+
|
|
202
|
+
const code = await runCmd("docker", cmdArgs);
|
|
203
|
+
if (code !== 0) {
|
|
204
|
+
console.error(chalk.yellow("Make sure the MySQL container is running: npx blue-bird docker start mysql"));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Handles cleaning up and pruning unused Docker resources.
|
|
211
|
+
* @param {boolean} forceOpt - Force cleaning without user confirmation.
|
|
212
|
+
* @param {boolean} allOpt - Clean all unused resources.
|
|
213
|
+
*/
|
|
214
|
+
async function pruneCommand(forceOpt, allOpt) {
|
|
215
|
+
if (!forceOpt) {
|
|
216
|
+
console.log(chalk.yellow("Clean unused volumes, dangling images, and BuildKit cache? (Press enter to confirm, Ctrl+C to cancel)"));
|
|
217
|
+
await new Promise((resolve) => process.stdin.once("data", resolve));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(chalk.cyan("1/3 Cleaning orphaned Docker volumes..."));
|
|
221
|
+
await runCmd("docker", ["volume", "prune", "-f"]);
|
|
222
|
+
|
|
223
|
+
console.log(chalk.cyan("2/3 Cleaning unused Docker images..."));
|
|
224
|
+
const imgArgs = ["image", "prune"];
|
|
225
|
+
if (allOpt) {
|
|
226
|
+
imgArgs.push("-a");
|
|
227
|
+
}
|
|
228
|
+
imgArgs.push("-f");
|
|
229
|
+
await runCmd("docker", imgArgs);
|
|
230
|
+
|
|
231
|
+
console.log(chalk.cyan("3/3 Cleaning BuildKit build cache..."));
|
|
232
|
+
await runCmd("docker", ["builder", "prune", "-a", "-f"]);
|
|
233
|
+
|
|
234
|
+
console.log(chalk.green("\nDocker cleanup completed successfully! Current disk usage:"));
|
|
235
|
+
await runCmd("docker", ["system", "df"]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Entry point for Blue Bird CLI Docker subcommands.
|
|
240
|
+
*/
|
|
241
|
+
async function main() {
|
|
242
|
+
const env = getEnvVars();
|
|
243
|
+
let projectName = env.BLUEBIRD_PROJECT_NAME;
|
|
244
|
+
if (!projectName && env.TITLE) {
|
|
245
|
+
projectName = env.TITLE.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
|
|
246
|
+
}
|
|
247
|
+
if (!projectName) {
|
|
248
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
249
|
+
if (fs.existsSync(pkgPath)) {
|
|
250
|
+
try {
|
|
251
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
252
|
+
if (pkg.name) {
|
|
253
|
+
projectName = pkg.name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
|
|
254
|
+
}
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!projectName) {
|
|
259
|
+
projectName = "bluebird";
|
|
260
|
+
}
|
|
261
|
+
process.env.BLUEBIRD_PROJECT_NAME = projectName;
|
|
262
|
+
process.env.TITLE = projectName;
|
|
263
|
+
|
|
264
|
+
const args = process.argv.slice(3);
|
|
265
|
+
const command = args[0];
|
|
266
|
+
|
|
267
|
+
if (!command) {
|
|
268
|
+
await psCommand();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
switch (command) {
|
|
273
|
+
case "start":
|
|
274
|
+
await startCommand(args[1]);
|
|
275
|
+
break;
|
|
276
|
+
case "stop":
|
|
277
|
+
await stopCommand(args[1]);
|
|
278
|
+
break;
|
|
279
|
+
case "build":
|
|
280
|
+
await buildCommand(args.slice(1));
|
|
281
|
+
break;
|
|
282
|
+
case "ps":
|
|
283
|
+
case "status":
|
|
284
|
+
await psCommand();
|
|
285
|
+
break;
|
|
286
|
+
case "logs":
|
|
287
|
+
await logsCommand(args[1], args[2]);
|
|
288
|
+
break;
|
|
289
|
+
case "mysql":
|
|
290
|
+
case "db": {
|
|
291
|
+
let user, password, db, root = false;
|
|
292
|
+
for (let i = 1; i < args.length; i++) {
|
|
293
|
+
if (args[i] === "-u" || args[i] === "--user") user = args[++i];
|
|
294
|
+
else if (args[i] === "-p" || args[i] === "--password") password = args[++i];
|
|
295
|
+
else if (args[i] === "-d" || args[i] === "--db") db = args[++i];
|
|
296
|
+
else if (args[i] === "--root") root = true;
|
|
297
|
+
}
|
|
298
|
+
await mysqlCommand(user, password, db, root);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "df":
|
|
302
|
+
case "disk":
|
|
303
|
+
console.log(chalk.cyan("📊 Docker Disk Usage:"));
|
|
304
|
+
await runCmd("docker", ["system", "df"]);
|
|
305
|
+
break;
|
|
306
|
+
case "prune":
|
|
307
|
+
case "clean": {
|
|
308
|
+
const force = args.includes("-f") || args.includes("--force");
|
|
309
|
+
const all = args.includes("-a") || args.includes("--all");
|
|
310
|
+
await pruneCommand(force, all);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
default:
|
|
314
|
+
console.log(chalk.yellow(`Unknown docker command: ${command}`));
|
|
315
|
+
console.log("Available commands: start, stop, build, ps, logs, mysql/db, df/disk, prune/clean");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main();
|
package/core/cli/init.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
* Initializes a new Blue Bird project by copying the base structure.
|
|
9
|
+
*/
|
|
10
|
+
class ProjectInit {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.appDir = process.cwd();
|
|
13
|
+
this.sourceDir = path.resolve(import.meta.dirname, "../../");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs the project initialization process.
|
|
18
|
+
*/
|
|
19
|
+
async run() {
|
|
20
|
+
console.log(chalk.cyan("Starting Blue Bird project initialization..."));
|
|
21
|
+
|
|
22
|
+
const itemsToCopy = [
|
|
23
|
+
"backend",
|
|
24
|
+
"frontend",
|
|
25
|
+
"docker",
|
|
26
|
+
"docker-compose.yml",
|
|
27
|
+
".env_example",
|
|
28
|
+
"AGENTS.md"
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
itemsToCopy.forEach(item => {
|
|
33
|
+
const src = path.join(this.sourceDir, item);
|
|
34
|
+
const dest = path.join(this.appDir, item);
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(src)) {
|
|
37
|
+
if (!fs.existsSync(dest)) {
|
|
38
|
+
this.copyRecursive(src, dest);
|
|
39
|
+
console.log(chalk.green(`✓ Copied ${item} to root.`));
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.yellow(`! ${item} already exists, skipping.`));
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.warn(chalk.red(`✗ Source ${item} not found in ${this.sourceDir}`));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const envPath = path.join(this.appDir, ".env");
|
|
49
|
+
const envExamplePath = path.join(this.appDir, ".env_example");
|
|
50
|
+
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
51
|
+
fs.copyFileSync(envExamplePath, envPath);
|
|
52
|
+
console.log(chalk.green("✓ Created .env from .env_example."));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.updatePackageJson();
|
|
56
|
+
|
|
57
|
+
console.log(chalk.blue("\nBlue Bird initialization completed!"));
|
|
58
|
+
console.log(chalk.white("Next steps:"));
|
|
59
|
+
console.log(chalk.bold(" npm install"));
|
|
60
|
+
console.log(chalk.bold(" npm run dev"));
|
|
61
|
+
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(chalk.red("Error during initialization:"), error.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Updates the user's package.json with Blue Bird scripts.
|
|
69
|
+
*/
|
|
70
|
+
updatePackageJson() {
|
|
71
|
+
const pkgPath = path.join(this.appDir, "package.json");
|
|
72
|
+
if (fs.existsSync(pkgPath)) {
|
|
73
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
74
|
+
pkg.scripts = pkg.scripts || {};
|
|
75
|
+
|
|
76
|
+
const scriptsToAdd = {
|
|
77
|
+
"dev": "node --watch --env-file=.env backend/index.js",
|
|
78
|
+
"start": "node --env-file=.env backend/index.js",
|
|
79
|
+
"init": "blue-bird",
|
|
80
|
+
"route": "blue-bird route",
|
|
81
|
+
"swagger-install": "blue-bird swagger-install",
|
|
82
|
+
"docker": "blue-bird docker"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let updated = false;
|
|
86
|
+
for (const [key, value] of Object.entries(scriptsToAdd)) {
|
|
87
|
+
if (!pkg.scripts[key]) {
|
|
88
|
+
pkg.scripts[key] = value;
|
|
89
|
+
updated = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (updated) {
|
|
94
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
95
|
+
console.log(chalk.green("✓ Updated package.json scripts."));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Copies a file or directory recursively.
|
|
102
|
+
* @param {string} src - Source path.
|
|
103
|
+
* @param {string} dest - Destination path.
|
|
104
|
+
*/
|
|
105
|
+
copyRecursive(src, dest) {
|
|
106
|
+
const stats = fs.statSync(src);
|
|
107
|
+
const isDirectory = stats.isDirectory();
|
|
108
|
+
|
|
109
|
+
if (isDirectory) {
|
|
110
|
+
if (!fs.existsSync(dest)) {
|
|
111
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
fs.readdirSync(src).forEach(childItemName => {
|
|
114
|
+
this.copyRecursive(path.join(src, childItemName), path.join(dest, childItemName));
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
fs.copyFileSync(src, dest);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const initializer = new ProjectInit();
|
|
123
|
+
|
|
124
|
+
const args = process.argv.slice(2);
|
|
125
|
+
const command = args[0];
|
|
126
|
+
|
|
127
|
+
if (command === "route") import("./route.js");
|
|
128
|
+
else if (command === "swagger-install") import("./swagger.js");
|
|
129
|
+
else if (command === "docker") import("./docker.js");
|
|
130
|
+
else initializer.run();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import Config from "../config.js";
|
|
4
|
+
|
|
5
|
+
const __dirname = Config.dirname();
|
|
6
|
+
|
|
7
|
+
class RouteCLI {
|
|
8
|
+
/**
|
|
9
|
+
* Create route
|
|
10
|
+
*/
|
|
11
|
+
create() {
|
|
12
|
+
let nameRoute = process.argv[2];
|
|
13
|
+
if (!nameRoute) {
|
|
14
|
+
console.log("Please provide a route name. Usage: npm run route <route-name>");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
nameRoute =nameRoute.charAt(0).toUpperCase() + nameRoute.slice(1);
|
|
18
|
+
const folder= path.join(__dirname, 'backend/routes');
|
|
19
|
+
if (!fs.existsSync(folder)){
|
|
20
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
const filePath = path.join(folder, `${nameRoute}.js`);
|
|
23
|
+
if (fs.existsSync(filePath)) {
|
|
24
|
+
console.log(`Route ${nameRoute} already exists.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const content =`import Router from "@seip/blue-bird/core/router.js"
|
|
28
|
+
|
|
29
|
+
const router${nameRoute} = new Router("/${nameRoute.toLowerCase()}");
|
|
30
|
+
|
|
31
|
+
router${nameRoute}.get("/", (req, res) => {
|
|
32
|
+
res.json({ message: "Hello from ${nameRoute} route!" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export default router${nameRoute};
|
|
36
|
+
`;
|
|
37
|
+
fs.writeFileSync(filePath, content);
|
|
38
|
+
console.log(`Route ${nameRoute} created successfully at ${filePath}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const routeCLI = new RouteCLI();
|
|
43
|
+
routeCLI.create()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
class SwaggerCli {
|
|
4
|
+
install() {
|
|
5
|
+
const dependencies = this.checkDependencies();
|
|
6
|
+
if (dependencies.missingDependencies.length > 0) {
|
|
7
|
+
console.log("Installing dependencies...");
|
|
8
|
+
console.log(`Installing swagger-jsdoc...`);
|
|
9
|
+
execSync(`npm install swagger-jsdoc@6.2.8`, { stdio: "inherit" });
|
|
10
|
+
console.log(`Installing swagger-ui-express...`);
|
|
11
|
+
execSync(`npm install swagger-ui-express@5.0.1`, { stdio: "inherit" });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
checkDependencies() {
|
|
15
|
+
const dependencies = [
|
|
16
|
+
"swagger-jsdoc",
|
|
17
|
+
"swagger-ui-express"
|
|
18
|
+
];
|
|
19
|
+
const missingDependencies = [];
|
|
20
|
+
dependencies.forEach(dependency => {
|
|
21
|
+
if (!this.checkDependency(dependency)) {
|
|
22
|
+
missingDependencies.push(dependency);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
missingDependencies
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
checkDependency(dependency) {
|
|
30
|
+
try {
|
|
31
|
+
require.resolve(dependency);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const swaggerExecutor = new SwaggerCli();
|
|
40
|
+
swaggerExecutor.install();
|
package/core/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
let _cachedProps = null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration class to manage application-wide settings and environment variables.
|
|
7
|
+
*/
|
|
8
|
+
class Config {
|
|
9
|
+
/**
|
|
10
|
+
* Returns the base directory of the application.
|
|
11
|
+
* @returns {string} The current working directory.
|
|
12
|
+
*/
|
|
13
|
+
static dirname() {
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves application properties from environment variables or default values.
|
|
19
|
+
* Results are cached after first call for performance.
|
|
20
|
+
* @returns {{debug: boolean, descriptionMeta: string, keywordsMeta: string, titleMeta: string, authorMeta: string, description: string, title: string, version: string, langMeta: string, host: string, appUrl: string, port: number, static: {path: string, options: Object}}} The configuration properties object.
|
|
21
|
+
* @example
|
|
22
|
+
* const props = Config.props();
|
|
23
|
+
* console.log(props);
|
|
24
|
+
*/
|
|
25
|
+
static props() {
|
|
26
|
+
if (_cachedProps) return _cachedProps;
|
|
27
|
+
|
|
28
|
+
const portRaw = parseInt(process.env.PORT);
|
|
29
|
+
|
|
30
|
+
_cachedProps = {
|
|
31
|
+
debug: process.env.DEBUG === "true",
|
|
32
|
+
descriptionMeta: process.env.DESCRIPTION_META || "",
|
|
33
|
+
keywordsMeta: process.env.KEYWORDS_META || "",
|
|
34
|
+
titleMeta: process.env.TITLE_META || "",
|
|
35
|
+
authorMeta: process.env.AUTHOR_META || "",
|
|
36
|
+
description: process.env.DESCRIPTION || "",
|
|
37
|
+
title: process.env.TITLE || "",
|
|
38
|
+
version: process.env.VERSION || "1.0.0",
|
|
39
|
+
langMeta: process.env.LANGMETA || "en",
|
|
40
|
+
host: process.env.HOST || "http://localhost",
|
|
41
|
+
appUrl: process.env.APP_URL || process.env.HOST || "http://localhost",
|
|
42
|
+
port: Number.isNaN(portRaw) ? 3000 : portRaw,
|
|
43
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
44
|
+
static: {
|
|
45
|
+
path: process.env.STATIC_PATH || "frontend/public",
|
|
46
|
+
options: {},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return _cachedProps;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export default Config;
|