@seip/blue-bird 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +10 -0
- package/AGENTS.md +41 -4
- package/README.md +55 -1
- package/core/cli/docker.js +10 -1
- package/core/cli/doctor.js +294 -0
- package/core/cli/init.js +34 -5
- package/core/cli/migrate.js +342 -0
- package/core/cli/nginx.js +138 -0
- package/core/cli/route.js +144 -43
- package/core/cli/swagger.js +2 -0
- package/core/index.d.ts +7 -0
- package/core/queue.js +121 -0
- package/core/validate.js +3 -2
- package/docker/nginx.conf +23 -0
- package/frontend/index.html +144 -144
- package/frontend/js/bluebird.d.ts +423 -0
- package/frontend/js/bluebird.js +1121 -716
- package/jsconfig.json +15 -0
- package/package.json +7 -2
package/AGENTS.md
CHANGED
|
@@ -206,14 +206,51 @@ npx blue-bird docker prune # Cleans unused volumes, dangling images, an
|
|
|
206
206
|
|
|
207
207
|
The container names and virtual networks are namespaced by the `TITLE` environment variable parsed from `.env` to prevent resource collisions on VPS hosts. Alternatively, PM2 and other services can be run manually in standalone server environments by configuring `DATABASE_URL` inside `.env`.
|
|
208
208
|
|
|
209
|
-
## 11.
|
|
209
|
+
## 11. Productivity & Developer Tooling CLI
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# System Diagnostics & Smoke Test
|
|
213
|
+
npx blue-bird doctor # Audits .env, ports, permissions, and runs live HTTP smoke test
|
|
214
|
+
|
|
215
|
+
# Route Scaffolding
|
|
216
|
+
npx blue-bird make:route <name> # Generates full CRUD route with Validation & Cache invalidation
|
|
217
|
+
npx blue-bird make:route <name> -a # Generates route with Auth.protect() middleware
|
|
218
|
+
|
|
219
|
+
# Database Migrations & Seeds
|
|
220
|
+
npx blue-bird make:migration <name> # Creates timestamped SQL migration in database/migrations/
|
|
221
|
+
npx blue-bird migrate # Executes pending migrations across SQLite, MySQL, or Postgres
|
|
222
|
+
npx blue-bird migrate:status # Displays applied and pending migration batches
|
|
223
|
+
npx blue-bird make:seed <name> # Creates SQL seed file in database/seeds/
|
|
224
|
+
npx blue-bird seed # Executes seed files in database/seeds/
|
|
225
|
+
|
|
226
|
+
# VPS Host Nginx & SSL Automation (Auto-resolves APP_URL & PORT from .env if omitted)
|
|
227
|
+
npx blue-bird nginx:conf [domain] [port]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 12. Background Jobs & Queue (Queue)
|
|
231
|
+
|
|
232
|
+
Blue Bird includes a lightweight queue worker (`core/queue.js`) backed by Redis with an automatic in-memory fallback for local development or non-redis architectures.
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
import Queue from "@seip/blue-bird/core/queue.js";
|
|
236
|
+
|
|
237
|
+
// 1. Register job processor
|
|
238
|
+
Queue.process("sendWelcomeEmail", async (payload) => {
|
|
239
|
+
console.log(`Sending email to ${payload.email}...`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 2. Dispatch job from route or service
|
|
243
|
+
await Queue.dispatch("sendWelcomeEmail", { email: "user@example.com" });
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 13. AI Development Guidelines
|
|
210
247
|
|
|
211
248
|
1. **Frontend**: Static files are stored in `frontend/` (e.g. `frontend/css`, `frontend/js`). HTML files will be served without the `.html` extension (e.g. `login.html` is accessible as `/login`).
|
|
212
249
|
2. **JSON Responses**: API endpoints should return standardized responses formatted as `{ message: "..." }` or `{ data: ... }`.
|
|
213
250
|
3. **Magic Imports**: Stick to pure relative imports or well-configured aliases (imports natively resolve from `@seip/blue-bird/...` or relative directories like `../../`).
|
|
214
251
|
4. **No inline comments**: Only use JSDoc for documentation.
|
|
215
252
|
|
|
216
|
-
##
|
|
253
|
+
## 14. Database Module (database.js)
|
|
217
254
|
|
|
218
255
|
Blue Bird provides a unified wrapper class (`core/database.js`) supporting **SQLite (`better-sqlite3`)**, **MySQL (`mysql2/promise`)**, and **PostgreSQL (`pg`)**. It features connection pooling/reconnection, automatic retries on startup, query formatting, and built-in Redis query caching:
|
|
219
256
|
|
|
@@ -246,12 +283,12 @@ await connection.transaction(async (tx) => {
|
|
|
246
283
|
});
|
|
247
284
|
```
|
|
248
285
|
|
|
249
|
-
##
|
|
286
|
+
## 15. Nginx Static Asset Caching
|
|
250
287
|
|
|
251
288
|
In production, Nginx is configured to explicitly cache static assets (`.js`, `.css`, `.jpg`, `.png`, etc.) in the user's browser with the `Cache-Control` header (valid for 1 month).
|
|
252
289
|
HTML and API endpoints (`/api/*`) are not cached by Nginx to ensure they serve dynamic and up-to-date content, relying instead on the Node.js application and Redis for data-layer caching.
|
|
253
290
|
|
|
254
|
-
##
|
|
291
|
+
## 16. VPS Permissions & Security Hardening
|
|
255
292
|
|
|
256
293
|
When deploying Blue Bird to Linux VPS servers using Docker Compose orchestration:
|
|
257
294
|
|
package/README.md
CHANGED
|
@@ -444,6 +444,60 @@ Nginx is configured to explicitly cache static assets (`.js`, `.css`, `.jpg`, `.
|
|
|
444
444
|
|
|
445
445
|
---
|
|
446
446
|
|
|
447
|
+
## 🛠️ Developer Tooling & CLI Suite
|
|
448
|
+
|
|
449
|
+
Blue Bird includes built-in developer productivity commands:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
# System Diagnostics & Smoke Testing
|
|
453
|
+
npx blue-bird doctor # Audits .env, ports, permissions, and runs live HTTP smoke test
|
|
454
|
+
|
|
455
|
+
# Route Scaffolding
|
|
456
|
+
npx blue-bird make:route <name> # Generates full CRUD route with Validation & Cache invalidation
|
|
457
|
+
npx blue-bird make:route <name> -a # Generates route with Auth.protect() middleware
|
|
458
|
+
|
|
459
|
+
# Database Migrations & Seeds
|
|
460
|
+
npx blue-bird make:migration <name> # Creates timestamped SQL migration in database/migrations/
|
|
461
|
+
npx blue-bird migrate # Executes pending migrations across SQLite, MySQL, or Postgres
|
|
462
|
+
npx blue-bird migrate:status # Displays applied and pending migration batches
|
|
463
|
+
npx blue-bird make:seed <name> # Creates SQL seed file in database/seeds/
|
|
464
|
+
npx blue-bird seed # Executes seed files in database/seeds/
|
|
465
|
+
|
|
466
|
+
# VPS Host Nginx & SSL Automation (Auto-resolves APP_URL & PORT from .env if omitted)
|
|
467
|
+
npx blue-bird nginx:conf [domain] [port]
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## ✨ IDE Autocompletion & Developer Experience (CSS & JS)
|
|
473
|
+
|
|
474
|
+
When initializing a project with `npx blue-bird`, the CLI generates `jsconfig.json`, `.vscode/settings.json`, `.vscode/extensions.json`, and `frontend/js/bluebird.d.ts` for zero-configuration IntelliSense:
|
|
475
|
+
|
|
476
|
+
- **HTML & CSS Class IntelliSense:** Autocompletes all `bluebird.css` utilities and component classes in HTML attributes (`class="..."`). Recommended extensions:
|
|
477
|
+
- [HTML CSS Class Completion (Open VSX)](https://open-vsx.org/vscode/item?itemName=Zignd.html-css-class-completion) / [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Zignd.html-css-class-completion)
|
|
478
|
+
- [CSS Peek (Open VSX)](https://open-vsx.org/vscode/item?itemName=pranaygp.vscode-css-peek) / [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=pranaygp.vscode-css-peek)
|
|
479
|
+
- **JavaScript Tooltips & Autocomplete:** Built-in typed definitions (`bluebird.d.ts`) provide hover tooltips, options autocomplete, and inline code examples for `bluebird('toast', {...})`, `bluebird('snackbar', {...})`, `bluebird('drawer', {...})`, and other UI helpers.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 📬 Background Jobs & Queue (Queue)
|
|
484
|
+
|
|
485
|
+
Blue Bird includes a lightweight queue worker (`core/queue.js`) backed by Redis with an automatic in-memory fallback for local development or non-redis architectures:
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
import Queue from "@seip/blue-bird/core/queue.js";
|
|
489
|
+
|
|
490
|
+
// 1. Register job processor
|
|
491
|
+
Queue.process("sendWelcomeEmail", async (payload) => {
|
|
492
|
+
console.log(`Sending email to ${payload.email}...`);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 2. Dispatch job from route or service
|
|
496
|
+
await Queue.dispatch("sendWelcomeEmail", { email: "user@example.com" });
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
447
501
|
## Docker CLI Workflow
|
|
448
502
|
|
|
449
503
|
Blue Bird comes with a built-in Docker CLI wrapper that handles both local development database bootstrapping and full-stack VPS production deployments across MySQL, PostgreSQL, or no-database architectures.
|
|
@@ -457,7 +511,7 @@ npx blue-bird docker <command> [options]
|
|
|
457
511
|
### Supported Actions:
|
|
458
512
|
|
|
459
513
|
- **`npx blue-bird docker dev`**: Boots the development database and Redis containers. Run `npm run dev` locally on your host machine.
|
|
460
|
-
- **`npx blue-bird docker start`**: Boots the production stack (Node.js App + Nginx + Database + Redis).
|
|
514
|
+
- **`npx blue-bird docker start`**: Boots the production stack (Node.js App + Nginx + Database + Redis) and runs the automatic health smoke test.
|
|
461
515
|
- **`npx blue-bird docker start db`** (or `postgres` / `mysql`): Boots the configured database container only (great for local development outside Docker).
|
|
462
516
|
- **`npx blue-bird docker start redis`**: Boots the Redis container only.
|
|
463
517
|
- **`npx blue-bird docker start dbs`**: Boots both database containers (configured DB + Redis).
|
package/core/cli/docker.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
1
3
|
import fs from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { spawn } from "node:child_process";
|
|
@@ -148,7 +150,14 @@ async function startCommand(service) {
|
|
|
148
150
|
console.log(chalk.cyan("Starting production stack (DB + Redis + App + Nginx)..."));
|
|
149
151
|
const code = await runCmd("docker", ["compose", "--profile", "prod", "up", "-d"]);
|
|
150
152
|
if (code === 0) {
|
|
151
|
-
console.log(chalk.green("Production stack started."));
|
|
153
|
+
console.log(chalk.green("Production stack started successfully."));
|
|
154
|
+
console.log(chalk.cyan("\nRunning post-start health check (Doctor)...\n"));
|
|
155
|
+
try {
|
|
156
|
+
const { runDoctor } = await import("./doctor.js");
|
|
157
|
+
await runDoctor();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.log(chalk.gray(`Health check skipped: ${err.message}`));
|
|
160
|
+
}
|
|
152
161
|
} else {
|
|
153
162
|
console.error(chalk.red("Error starting production stack."));
|
|
154
163
|
process.exit(1);
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses .env file into key-value map.
|
|
10
|
+
* @returns {Record<string, string>}
|
|
11
|
+
*/
|
|
12
|
+
function parseEnv() {
|
|
13
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
14
|
+
const env = {};
|
|
15
|
+
if (fs.existsSync(envPath)) {
|
|
16
|
+
const lines = fs.readFileSync(envPath, "utf-8").split("\n");
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
|
|
20
|
+
const idx = trimmed.indexOf("=");
|
|
21
|
+
const key = trimmed.substring(0, idx).trim();
|
|
22
|
+
let val = trimmed.substring(idx + 1).trim();
|
|
23
|
+
if (
|
|
24
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
25
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
26
|
+
) {
|
|
27
|
+
val = val.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
env[key] = val;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return env;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks if a TCP port is currently open for binding on localhost.
|
|
38
|
+
* @param {number} port
|
|
39
|
+
* @returns {Promise<boolean>} Resolves to true if available, false if in use.
|
|
40
|
+
*/
|
|
41
|
+
function isPortAvailable(port) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const server = net.createServer();
|
|
44
|
+
server.once("error", () => {
|
|
45
|
+
resolve(false);
|
|
46
|
+
});
|
|
47
|
+
server.once("listening", () => {
|
|
48
|
+
server.close(() => resolve(true));
|
|
49
|
+
});
|
|
50
|
+
server.listen(port, "127.0.0.1");
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Finds a representative static file in frontend/ directory.
|
|
56
|
+
* @returns {string|null} Relative path like 'css/bluebird.css' or 'index.html'
|
|
57
|
+
*/
|
|
58
|
+
function findSampleStaticAsset() {
|
|
59
|
+
const frontendDir = path.resolve(process.cwd(), "frontend");
|
|
60
|
+
if (!fs.existsSync(frontendDir)) return null;
|
|
61
|
+
|
|
62
|
+
const candidates = [
|
|
63
|
+
"css/bluebird.css",
|
|
64
|
+
"css/style.css",
|
|
65
|
+
"js/bluebird.js",
|
|
66
|
+
"js/index.js",
|
|
67
|
+
"index.html",
|
|
68
|
+
"favicon.ico",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (fs.existsSync(path.join(frontendDir, candidate))) {
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const files = fs.readdirSync(frontendDir, { recursive: true });
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
const full = path.join(frontendDir, f);
|
|
81
|
+
if (fs.statSync(full).isFile()) {
|
|
82
|
+
return f.replace(/\\/g, "/");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// fallback
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Formats file permissions mode (e.g. 0600, 0755).
|
|
94
|
+
* @param {string} filePath
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function getOctalPermissions(filePath) {
|
|
98
|
+
try {
|
|
99
|
+
const stats = fs.statSync(filePath);
|
|
100
|
+
return "0" + (stats.mode & 0o777).toString(8);
|
|
101
|
+
} catch {
|
|
102
|
+
return "unknown";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Runs the Blue Bird Doctor diagnostic suite.
|
|
108
|
+
*/
|
|
109
|
+
export async function runDoctor() {
|
|
110
|
+
console.log(chalk.bold.cyan("============================================================="));
|
|
111
|
+
console.log(chalk.bold.cyan(" Blue Bird System & Security Health Diagnostic (Doctor)"));
|
|
112
|
+
console.log(chalk.bold.cyan("============================================================="));
|
|
113
|
+
console.log("");
|
|
114
|
+
|
|
115
|
+
const env = parseEnv();
|
|
116
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
117
|
+
const projectDir = process.cwd();
|
|
118
|
+
const isLinux = process.platform === "linux";
|
|
119
|
+
const port = parseInt(env.PORT || "3000", 10);
|
|
120
|
+
let appUrl = (env.APP_URL || `http://localhost:${port}`).replace(/\/$/, "");
|
|
121
|
+
if (
|
|
122
|
+
(appUrl === "http://localhost" || appUrl === "http://127.0.0.1") &&
|
|
123
|
+
port !== 80
|
|
124
|
+
) {
|
|
125
|
+
appUrl = `http://localhost:${port}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let issuesFound = 0;
|
|
129
|
+
let warningsFound = 0;
|
|
130
|
+
|
|
131
|
+
// -------------------------------------------------------------
|
|
132
|
+
// 1. Environment & Secrets Check (.env)
|
|
133
|
+
// -------------------------------------------------------------
|
|
134
|
+
console.log(chalk.bold("[1/5] Environment & Configuration (.env):"));
|
|
135
|
+
if (!fs.existsSync(envPath)) {
|
|
136
|
+
console.log(chalk.red(" [FAIL] .env file is missing in project root."));
|
|
137
|
+
console.log(chalk.gray(" Run 'npx blue-bird' or copy .env_example to .env."));
|
|
138
|
+
issuesFound++;
|
|
139
|
+
} else {
|
|
140
|
+
console.log(chalk.green(" [PASS] .env file exists."));
|
|
141
|
+
|
|
142
|
+
if (isLinux) {
|
|
143
|
+
const envMode = getOctalPermissions(envPath);
|
|
144
|
+
if (envMode !== "0600" && envMode !== "0400") {
|
|
145
|
+
console.log(chalk.yellow(` [WARN] .env permissions are '${envMode}'. Recommended mode is '0600' (chmod 600 .env).`));
|
|
146
|
+
warningsFound++;
|
|
147
|
+
} else {
|
|
148
|
+
console.log(chalk.green(` [PASS] .env file permissions are strictly isolated (${envMode}).`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!env.JWT_SECRET || env.JWT_SECRET.length < 16) {
|
|
153
|
+
console.log(chalk.yellow(" [WARN] JWT_SECRET is missing or shorter than 16 characters."));
|
|
154
|
+
warningsFound++;
|
|
155
|
+
} else {
|
|
156
|
+
console.log(chalk.green(" [PASS] JWT_SECRET is configured."));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log("");
|
|
160
|
+
|
|
161
|
+
// -------------------------------------------------------------
|
|
162
|
+
// 2. Filesystem Hierarchy & Permissions (FHS)
|
|
163
|
+
// -------------------------------------------------------------
|
|
164
|
+
console.log(chalk.bold("[2/5] Filesystem Hierarchy Standard & Static Asset Structure:"));
|
|
165
|
+
if (isLinux) {
|
|
166
|
+
if (projectDir.startsWith("/home/")) {
|
|
167
|
+
console.log(chalk.yellow(" [WARN] Project is deployed inside '/home/user/'."));
|
|
168
|
+
console.log(chalk.gray(" Recommended FHS production standard is '/var/www/<project>' or '/srv/<project>'"));
|
|
169
|
+
console.log(chalk.gray(" to prevent path traversal restrictions for unprivileged containers (nginx UID 101)."));
|
|
170
|
+
warningsFound++;
|
|
171
|
+
} else {
|
|
172
|
+
console.log(chalk.green(" [PASS] Project directory follows recommended FHS location."));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const frontendDir = path.resolve(process.cwd(), "frontend");
|
|
177
|
+
if (!fs.existsSync(frontendDir)) {
|
|
178
|
+
console.log(chalk.red(" [FAIL] 'frontend/' directory is missing."));
|
|
179
|
+
issuesFound++;
|
|
180
|
+
} else {
|
|
181
|
+
console.log(chalk.green(" [PASS] 'frontend/' directory exists."));
|
|
182
|
+
const sampleAsset = findSampleStaticAsset();
|
|
183
|
+
if (sampleAsset) {
|
|
184
|
+
console.log(chalk.green(` [PASS] Detected sample static asset: 'frontend/${sampleAsset}'`));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(chalk.yellow(" [WARN] No static files found inside 'frontend/'."));
|
|
187
|
+
warningsFound++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
console.log("");
|
|
191
|
+
|
|
192
|
+
// -------------------------------------------------------------
|
|
193
|
+
// 3. Port & Local Network Binding Check
|
|
194
|
+
// -------------------------------------------------------------
|
|
195
|
+
console.log(chalk.bold("[3/5] Port Availability & Binding:"));
|
|
196
|
+
const portFree = await isPortAvailable(port);
|
|
197
|
+
if (portFree) {
|
|
198
|
+
console.log(chalk.green(` [INFO] Port ${port} is currently free and available for binding.`));
|
|
199
|
+
} else {
|
|
200
|
+
console.log(chalk.cyan(` [INFO] Port ${port} is currently in use (application server or container is active).`));
|
|
201
|
+
}
|
|
202
|
+
console.log("");
|
|
203
|
+
|
|
204
|
+
// -------------------------------------------------------------
|
|
205
|
+
// 4. Live Smoke Test (API & Nginx Static Asset Delivery)
|
|
206
|
+
// -------------------------------------------------------------
|
|
207
|
+
console.log(chalk.bold("[4/5] Live HTTP Smoke Test (Health & Static Delivery):"));
|
|
208
|
+
const sampleAsset = findSampleStaticAsset();
|
|
209
|
+
const testUrls = [
|
|
210
|
+
{ name: "API Health Endpoint", url: `${appUrl}/api/health`, isApi: true },
|
|
211
|
+
];
|
|
212
|
+
if (sampleAsset) {
|
|
213
|
+
testUrls.push({
|
|
214
|
+
name: `Static Asset (/${sampleAsset})`,
|
|
215
|
+
url: `${appUrl}/${sampleAsset}`,
|
|
216
|
+
isApi: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let serverReachable = false;
|
|
221
|
+
|
|
222
|
+
for (const item of testUrls) {
|
|
223
|
+
try {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
226
|
+
const res = await fetch(item.url, { signal: controller.signal });
|
|
227
|
+
clearTimeout(timeoutId);
|
|
228
|
+
|
|
229
|
+
serverReachable = true;
|
|
230
|
+
|
|
231
|
+
if (res.status === 200) {
|
|
232
|
+
console.log(chalk.green(` [PASS] ${item.name} returned HTTP 200 OK.`));
|
|
233
|
+
} else if (res.status === 403) {
|
|
234
|
+
console.log(chalk.red(` [FAIL] ${item.name} returned HTTP 403 Forbidden.`));
|
|
235
|
+
console.log(chalk.yellow(" Cause: Unprivileged Nginx worker (UID 101) lacks path traversal (+x) or read (+r) permissions."));
|
|
236
|
+
console.log(chalk.yellow(" Remediation: Run the following commands on your host:"));
|
|
237
|
+
console.log(chalk.white(` chmod 755 ${projectDir}`));
|
|
238
|
+
console.log(chalk.white(" find frontend -type d -exec chmod 755 {} +"));
|
|
239
|
+
console.log(chalk.white(" find frontend -type f -exec chmod 644 {} +"));
|
|
240
|
+
issuesFound++;
|
|
241
|
+
} else if (res.status === 404) {
|
|
242
|
+
console.log(chalk.red(` [FAIL] ${item.name} returned HTTP 404 Not Found.`));
|
|
243
|
+
console.log(chalk.yellow(" Cause: File not found or Docker volume mount inode desynchronization."));
|
|
244
|
+
console.log(chalk.yellow(" Remediation: Recreate container volume mounts:"));
|
|
245
|
+
console.log(chalk.white(" npx blue-bird docker stop && npx blue-bird docker start prod"));
|
|
246
|
+
issuesFound++;
|
|
247
|
+
} else {
|
|
248
|
+
console.log(chalk.yellow(` [WARN] ${item.name} returned HTTP ${res.status}.`));
|
|
249
|
+
warningsFound++;
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (err.name === "AbortError") {
|
|
253
|
+
console.log(chalk.yellow(` [WARN] Request to ${item.name} (${item.url}) timed out after 3s.`));
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk.gray(` [INFO] Cannot connect to ${item.name} at ${item.url} (${err.code || err.message}).`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!serverReachable) {
|
|
261
|
+
console.log(chalk.gray(" [INFO] Application server is not running locally. Start it with:"));
|
|
262
|
+
console.log(chalk.gray(" Development: npm run dev"));
|
|
263
|
+
console.log(chalk.gray(" Production: npx blue-bird docker start prod"));
|
|
264
|
+
}
|
|
265
|
+
console.log("");
|
|
266
|
+
|
|
267
|
+
// -------------------------------------------------------------
|
|
268
|
+
// 5. Database & Cache Driver Status
|
|
269
|
+
// -------------------------------------------------------------
|
|
270
|
+
console.log(chalk.bold("[5/5] Database & Cache Architecture:"));
|
|
271
|
+
const dbType = (env.DB_TYPE || "sqlite").toLowerCase();
|
|
272
|
+
console.log(chalk.green(` [INFO] Database Type: ${dbType.toUpperCase()}`));
|
|
273
|
+
const cacheMode = (env.CACHE_MODE || "memory").toLowerCase();
|
|
274
|
+
console.log(chalk.green(` [INFO] Cache Mode: ${cacheMode.toUpperCase()}`));
|
|
275
|
+
console.log("");
|
|
276
|
+
|
|
277
|
+
// -------------------------------------------------------------
|
|
278
|
+
// Summary
|
|
279
|
+
// -------------------------------------------------------------
|
|
280
|
+
console.log(chalk.bold.cyan("============================================================="));
|
|
281
|
+
if (issuesFound === 0 && warningsFound === 0) {
|
|
282
|
+
console.log(chalk.bold.green(" Diagnostic Summary: All checks passed with zero issues!"));
|
|
283
|
+
} else if (issuesFound === 0) {
|
|
284
|
+
console.log(chalk.bold.yellow(` Diagnostic Summary: System is operational with ${warningsFound} warning(s).`));
|
|
285
|
+
} else {
|
|
286
|
+
console.log(chalk.bold.red(` Diagnostic Summary: Found ${issuesFound} error(s) and ${warningsFound} warning(s).`));
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk.bold.cyan("============================================================="));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// If executed directly from CLI
|
|
292
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("doctor.js") || process.argv[2] === "doctor") {
|
|
293
|
+
runDoctor();
|
|
294
|
+
}
|
package/core/cli/init.js
CHANGED
|
@@ -118,6 +118,8 @@ class ProjectInit {
|
|
|
118
118
|
"docker",
|
|
119
119
|
".env_example",
|
|
120
120
|
"AGENTS.md",
|
|
121
|
+
"jsconfig.json",
|
|
122
|
+
".vscode",
|
|
121
123
|
];
|
|
122
124
|
|
|
123
125
|
try {
|
|
@@ -333,7 +335,10 @@ class ProjectInit {
|
|
|
333
335
|
dev: "node --watch --env-file=.env backend/index.js",
|
|
334
336
|
start: "node --env-file=.env backend/index.js",
|
|
335
337
|
init: "blue-bird",
|
|
338
|
+
doctor: "blue-bird doctor",
|
|
336
339
|
route: "blue-bird route",
|
|
340
|
+
migrate: "blue-bird migrate",
|
|
341
|
+
seed: "blue-bird seed",
|
|
337
342
|
"swagger-install": "blue-bird swagger-install",
|
|
338
343
|
docker: "blue-bird docker",
|
|
339
344
|
};
|
|
@@ -452,9 +457,33 @@ const initializer = new ProjectInit();
|
|
|
452
457
|
const args = process.argv.slice(2);
|
|
453
458
|
const command = args[0];
|
|
454
459
|
|
|
455
|
-
if (command === "route"
|
|
456
|
-
|
|
457
|
-
else if (command === "
|
|
458
|
-
|
|
459
|
-
else
|
|
460
|
+
if (command === "route" || command === "make:route") {
|
|
461
|
+
import("./route.js");
|
|
462
|
+
} else if (command === "doctor") {
|
|
463
|
+
import("./doctor.js");
|
|
464
|
+
} else if (
|
|
465
|
+
command === "nginx:conf" ||
|
|
466
|
+
command === "nginx:host" ||
|
|
467
|
+
command === "nginx"
|
|
468
|
+
) {
|
|
469
|
+
import("./nginx.js");
|
|
470
|
+
} else if (
|
|
471
|
+
command === "migrate" ||
|
|
472
|
+
command === "migrate:status" ||
|
|
473
|
+
command === "migrate:rollback" ||
|
|
474
|
+
command === "seed" ||
|
|
475
|
+
command === "make:migration" ||
|
|
476
|
+
command === "make:seed"
|
|
477
|
+
) {
|
|
478
|
+
import("./migrate.js");
|
|
479
|
+
} else if (command === "swagger-install") {
|
|
480
|
+
import("./swagger.js");
|
|
481
|
+
} else if (command === "docker") {
|
|
482
|
+
import("./docker.js");
|
|
483
|
+
} else if (command === "add") {
|
|
484
|
+
addCommand(args[1]);
|
|
485
|
+
} else {
|
|
486
|
+
initializer.run();
|
|
487
|
+
}
|
|
488
|
+
|
|
460
489
|
|