@seip/blue-bird 1.1.2 → 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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "Zignd.html-css-class-completion",
4
+ "pranaygp.vscode-css-peek"
5
+ ]
6
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "html-css-class-completion.includeGlobPattern": "frontend/css/**/*.css",
3
+ "html-css-class-completion.enableEmmetSupport": true,
4
+ "css.validate": true,
5
+ "editor.quickSuggestions": {
6
+ "other": true,
7
+ "comments": false,
8
+ "strings": true
9
+ }
10
+ }
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. AI Development Guidelines
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
- ## 12. Database Module (database.js)
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,9 +283,79 @@ await connection.transaction(async (tx) => {
246
283
  });
247
284
  ```
248
285
 
249
- ## 11. Nginx Static Asset Caching
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
 
291
+ ## 16. VPS Permissions & Security Hardening
292
+
293
+ When deploying Blue Bird to Linux VPS servers using Docker Compose orchestration:
294
+
295
+ ### 1. FHS Deployment Standard
296
+ - **Avoid deploying inside `/home/user/`**: User home directories often enforce restrictive traversal permissions (`700`/`750`) or risk cross-user privilege escalation in multi-tenant environments.
297
+ - **Production Standard**: Always deploy in standard Filesystem Hierarchy Standard (FHS) locations:
298
+ - `/var/www/<project-name>` (Recommended for web applications)
299
+ - `/srv/<project-name>` (Alternative for site-specific service payloads)
300
+
301
+ ### 2. Permissions & Ownership Matrix
302
+
303
+ | Path / Target | Recommended Mode | Ownership | Description & Rationale |
304
+ |---|---|---|---|
305
+ | `/var/www/<project-name>` | `755` (`drwxr-xr-x`) | `$(whoami):$(whoami)` | Allows unprivileged Nginx container (`UID 101`) path traversal. |
306
+ | `frontend/` (directories) | `755` (`drwxr-xr-x`) | `$(whoami):$(whoami)` | Directory traversal for static web workers. |
307
+ | `frontend/` (files) | `644` (`-rw-r--r--`) | `$(whoami):$(whoami)` | Public read access for Nginx static serving. |
308
+ | `node_modules/` | Dirs `755`, Files `644` | `$(whoami):$(whoami)` | Strict security. Avoid blanket `chmod -R 755`. |
309
+ | `node_modules/.bin/` | `+x` (`chmod -R +x`) | `$(whoami):$(whoami)` | Preserves execution bits for CLI binaries and symlinks. |
310
+ | `.env` | `600` (`-rw-------`) | `$(whoami):$(whoami)` | Strict isolation for database credentials and JWT keys. Never `777`. |
311
+ | `database/` (SQLite) | Dir `755`, Files `644` | `$(whoami):$(whoami)` | Allows Node.js application process to read/write WAL journals. |
312
+
313
+ ### 3. Container-Level Hardening (Docker)
314
+ - Mount static assets as **read-only (`:ro`)** in `docker-compose.yml` for the Nginx service (e.g. `./frontend:/app/frontend:ro` or `/var/www/<project-name>/frontend:/app/frontend:ro`).
315
+ - Enforces the principle of least privilege: the web server worker cannot write or overwrite host assets even if compromised.
316
+
317
+ ### 4. Production Hardening Commands
318
+
319
+ ```bash
320
+ # 1. Set project ownership to current deploy user
321
+ sudo chown -R $(whoami):$(whoami) /var/www/<project-name>
322
+ cd /var/www/<project-name>
323
+
324
+ # 2. Ensure parent directory traversal permissions
325
+ chmod 755 /var /var/www /var/www/<project-name>
326
+
327
+ # 3. Set directory (755) and file (644) permissions for static frontend assets
328
+ find frontend -type d -exec chmod 755 {} +
329
+ find frontend -type f -exec chmod 644 {} +
330
+
331
+ # 4. Secure node_modules while preserving executable bits in .bin
332
+ find node_modules -type d -exec chmod 755 {} +
333
+ find node_modules -type f -exec chmod 644 {} +
334
+ chmod -R +x node_modules/.bin 2>/dev/null || true
335
+
336
+ # 5. Restrict environment credentials strictly to owner
337
+ chmod 600 .env
338
+
339
+ # 6. Set database permissions (for SQLite)
340
+ chmod 755 database 2>/dev/null || true
341
+ chmod 644 database/*.db 2>/dev/null || true
342
+ ```
343
+
344
+ ### 5. Failure Modes & Remediation Playbook
345
+ * **Error 13: Permission Denied on static assets (`stat() failed (13: Permission denied)`):**
346
+ - Cause: Nginx worker (`UID 101`) lacks path traversal (`+x`) or read (`+r`) permissions.
347
+ - Fix: `chmod 755 /var/www/<project-name> && find frontend -type d -exec chmod 755 {} + && find frontend -type f -exec chmod 644 {} +`
348
+ * **`sh: 1: blue-bird: Permission denied` on CLI:**
349
+ - Cause: Blanket `chmod 644` stripped execution permissions from binary links in `node_modules/.bin/`.
350
+ - Fix: `chmod -R +x node_modules/.bin` (or `npm rebuild`)
351
+ * **Docker Inode Desync (404 after `mv` / `rm -rf` directory):**
352
+ - Cause: Docker volume bind mounts bind to Linux filesystem inodes. Recreating the folder desynchronizes active mounts.
353
+ - Fix: `npx blue-bird docker stop && npx blue-bird docker start prod` (or `docker compose down && docker compose up -d`)
354
+ * **Real-time Diagnostics:**
355
+ - Nginx log stream: `docker compose logs -f nginx`
356
+ - Container visibility check: `docker exec -it <container_name>-nginx su -s /bin/sh nginx -c "ls -la /app/frontend"`
357
+ - Host path traversal audit: `namei -l /var/www/<project-name>/frontend/css/bluebird.css`
358
+
359
+ ---
360
+
254
361
  _This file can be retrieved by intelligent agents reading its absolute physical path during reasoning._
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).
@@ -504,6 +558,135 @@ To deploy via Docker:
504
558
  npx blue-bird docker start prod
505
559
  ```
506
560
 
561
+ ---
562
+
563
+ ### 🛡️ Production VPS Security & Permissions Hardening Guide
564
+
565
+ When deploying Blue Bird to a Linux VPS (Ubuntu, Debian, AlmaLinux, Rocky Linux), file permissions must strictly balance **least-privilege security** (preventing unauthorized read/write access to sensitive files) with **container accessibility** (allowing unprivileged container users like `nginx` to read static assets).
566
+
567
+ > [!CAUTION]
568
+ > **Never use `chmod -R 777` in production!** Giving full write permissions to all users creates severe security vulnerabilities, allowing compromised processes or unauthorized local users to modify application code, inject backdoors, or tamper with `.env` secrets.
569
+
570
+ #### 1. Deployment Directory (Filesystem Hierarchy Standard)
571
+
572
+ * ❌ **Avoid deploying inside `/home/user/`**: Deploying in home directories introduces security risks in multi-user environments and frequently causes `403 Forbidden` / `stat() failed (13: Permission denied)` errors due to restrictive default parent directory permissions (`700`/`750`). Recklessly loosening `/home/user/` exposes user SSH keys and profile data.
573
+ * ✅ **Recommended Production Standard**: Deploy in dedicated Filesystem Hierarchy Standard (FHS) system directories:
574
+ * `/var/www/<project-name>` (Standard for web applications and static content)
575
+ * `/srv/<project-name>` (Alternative standard for site-specific payloads)
576
+
577
+ #### 2. Recommended Permissions & Ownership Matrix
578
+
579
+ | Target Directory / File | Mode | Ownership | Description & Security Rationale |
580
+ |---|---|---|---|
581
+ | **Project Root** (`/var/www/<project-name>`) | `755` (`drwxr-xr-x`) | `$(whoami):$(whoami)` | Allows unprivileged container processes (`nginx` UID 101) to traverse down to the project tree. |
582
+ | **Static Frontend Dirs** (`frontend/`) | `755` (`drwxr-xr-x`) | `$(whoami):$(whoami)` | Grants traversal and read access for Nginx static serving. |
583
+ | **Static Frontend Files** (`frontend/**/*`) | `644` (`-rw-r--r--`) | `$(whoami):$(whoami)` | Read-only access for web server worker processes. |
584
+ | **Node Module Dirs** (`node_modules/`) | `755` (`drwxr-xr-x`) | `$(whoami):$(whoami)` | Standard directory access. Avoid blanket `chmod -R 755`. |
585
+ | **Node Module Files** (`node_modules/**/*`) | `644` (`-rw-r--r--`) | `$(whoami):$(whoami)` | Standard read-only permissions for non-binary dependencies. |
586
+ | **Executable Binaries** (`node_modules/.bin/`) | `+x` (`chmod -R +x`) | `$(whoami):$(whoami)` | Grants execution bit exclusively to CLI wrapper symlinks (`npx blue-bird`). |
587
+ | **Environment File** (`.env`) | `600` (`-rw-------`) | `$(whoami):$(whoami)` | Restricts database passwords, JWT secrets, and API keys exclusively to the owning user. Never `644` or `777`. |
588
+ | **Database Directory** (`database/` for SQLite) | `755` (dir) / `644` (file) | `$(whoami):$(whoami)` | Allows the Node.js application process inside the container to read and write WAL journal files. |
589
+
590
+ #### 3. Container-Level Hardening (Docker)
591
+
592
+ To enforce the principle of least privilege at the container boundary, explicitly mount static frontend assets in **read-only mode (`:ro`)** inside `docker-compose.yml` for the Nginx service:
593
+
594
+ ```yaml
595
+ services:
596
+ nginx:
597
+ image: nginx:alpine
598
+ volumes:
599
+ - ./frontend:/app/frontend:ro
600
+ - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
601
+ ```
602
+
603
+ This guarantees that even in the event of an Nginx worker compromise, static web assets cannot be overwritten or modified from within the container.
604
+
605
+ #### 4. Applying Secure Permissions on VPS
606
+
607
+ Run the following chained commands inside your VPS project directory:
608
+
609
+ ```bash
610
+ # 1. Set project ownership to current deploy user
611
+ sudo chown -R $(whoami):$(whoami) /var/www/<project-name>
612
+ cd /var/www/<project-name>
613
+
614
+ # 2. Ensure parent directory traversal permissions
615
+ chmod 755 /var /var/www /var/www/<project-name>
616
+
617
+ # 3. Apply safe permissions for static frontend assets
618
+ find frontend -type d -exec chmod 755 {} +
619
+ find frontend -type f -exec chmod 644 {} +
620
+
621
+ # 4. Secure node_modules while preserving executable bits in .bin
622
+ find node_modules -type d -exec chmod 755 {} +
623
+ find node_modules -type f -exec chmod 644 {} +
624
+ chmod -R +x node_modules/.bin 2>/dev/null || true
625
+
626
+ # 5. Lock down environment credentials
627
+ chmod 600 .env
628
+
629
+ # 6. Set database directory permissions (for SQLite)
630
+ chmod 755 database 2>/dev/null || true
631
+ chmod 644 database/*.db 2>/dev/null || true
632
+ ```
633
+
634
+ ---
635
+
636
+ ### 🔧 VPS Production Troubleshooting (Problems & Solutions)
637
+
638
+ #### 1. Static Assets Return 404 / 403 (`Permission denied`)
639
+ * **Symptom:** Opening pages returns 404 or missing CSS/JS (e.g. `/css/bluebird.css`, `/favicon.ico`), and `docker compose logs -f nginx` reports:
640
+ ```text
641
+ [crit] stat() "/app/frontend/css/bluebird.css" failed (13: Permission denied)
642
+ ```
643
+ * **Cause:** The Nginx container runs as an unprivileged user (`nginx`, UID 101 on Alpine). If parent directories lack traversal (`+x`) permissions, or files lack read (`+r`) permissions, Nginx is blocked from reading `/app/frontend`.
644
+ * **Solution:**
645
+ ```bash
646
+ # Grant traversal and read permissions:
647
+ chmod 755 /var/www/<project-name>
648
+ find frontend -type d -exec chmod 755 {} +
649
+ find frontend -type f -exec chmod 644 {} +
650
+ ```
651
+
652
+ #### 2. `npx blue-bird` fails with `sh: 1: blue-bird: Permission denied`
653
+ * **Symptom:** Running CLI commands like `npx blue-bird docker stop` or `npx blue-bird docker start prod` fails with permission errors.
654
+ * **Cause:** An overly aggressive global `find . -type f -exec chmod 644` stripped execution permissions from binary wrappers and symlinks in `node_modules/.bin/`.
655
+ * **Solution:**
656
+ ```bash
657
+ chmod -R +x node_modules/.bin
658
+ # Or rebuild native binaries:
659
+ npm rebuild
660
+ ```
661
+
662
+ #### 3. 404 Not Found after Folder Renaming or Moving (Docker Inode Desync)
663
+ * **Symptom:** Moving, replacing, or recreating the project folder (e.g. `mv project_old project` or `git clone` / `rm -rf`) while Docker containers were running leads to persistent 404 errors even though files exist on disk.
664
+ * **Cause:** Linux file descriptors and Docker volume mounts bind to disk **inodes**. When a directory is deleted and recreated, Docker mounts remain attached to the stale/dead inode until the container stack is restarted.
665
+ * **Solution:**
666
+ ```bash
667
+ docker compose down
668
+ docker compose up -d
669
+ # Or with Blue Bird CLI:
670
+ npx blue-bird docker stop
671
+ npx blue-bird docker start prod
672
+ ```
673
+
674
+ #### 4. Real-time Container Diagnostics
675
+ * **Inspect Nginx logs in real time:**
676
+ ```bash
677
+ docker compose logs -f nginx
678
+ ```
679
+ * **Test file visibility directly as the Nginx container user:**
680
+ ```bash
681
+ docker exec -it <container_name>-nginx su -s /bin/sh nginx -c "ls -la /app/frontend/css/bluebird.css"
682
+ ```
683
+ * **Inspect path traversal permissions on host:**
684
+ ```bash
685
+ namei -l /var/www/<project-name>/frontend/css/bluebird.css
686
+ ```
687
+
688
+ ---
689
+
507
690
  ### B. Standard Standalone PM2 / Node.js Runtime
508
691
 
509
692
  If you choose to run outside of Docker, you must set up the reverse proxy and databases manually. To deploy in a standard Linux environment using PM2:
@@ -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
+ }