react-doctor-cli-dev 1.0.2 → 1.0.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.
Binary file
@@ -1,69 +1,26 @@
1
1
  "use strict";
2
- // ─────────────────────────────────────────────────────────────
3
- // backend/src/db.ts
4
- //
5
- // SQLite database via better-sqlite3 (synchronous, fast).
6
- //
7
- // SCHEMA:
8
- //
9
- // reports — one row per analysis run
10
- // id — auto-increment PK
11
- // project — project name / hostname
12
- // score — 0-100 overall performance score
13
- // grade — A+, A, B, C, D, F
14
- // analyzed_at — ISO timestamp from the CLI
15
- // created_at — when the row was inserted
16
- // static_json — full StaticReport as JSON string
17
- // runtime_json — full RuntimeReport map as JSON string
18
- // (screenshots stripped — stored as files)
19
- // suggestions — full Suggestion[] as JSON string
20
- //
21
- // WHY SPLIT COLUMNS INSTEAD OF ONE payload COLUMN?
22
- // The old schema stored everything in a single `payload` TEXT
23
- // column. This meant the dashboard couldn't query individual
24
- // fields without parsing the whole blob. Splitting into three
25
- // typed columns lets us:
26
- // • SELECT only the part we need (e.g. just static_json for
27
- // the issues page)
28
- // • Keep the list endpoint fast (no giant blobs per row)
29
- // • Add indices on score/grade later without changing schema
30
- //
31
- // SCREENSHOT STORAGE:
32
- // Screenshots are base64 PNGs — up to 200KB each. Storing them
33
- // in SQLite bloats the DB and makes every query slower. Instead:
34
- // • The upload route strips dataUrls from runtime_json
35
- // • Saves each screenshot as a .png file in data/screenshots/
36
- // • Replaces dataUrl with a relative path /screenshots/<file>
37
- // The dashboard fetches screenshots via the static file route.
38
- // ─────────────────────────────────────────────────────────────
39
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
40
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
41
4
  };
42
5
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.screenshotsDir = void 0;
44
6
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
45
7
  const path_1 = __importDefault(require("path"));
46
8
  const fs_1 = __importDefault(require("fs"));
47
- const dotenv_1 = __importDefault(require("dotenv"));
48
- dotenv_1.default.config({ path: path_1.default.join(__dirname, "..", ".env") });
49
- const PACKAGE_ROOT = path_1.default.resolve(__dirname, "..", "..");
50
- const DEFAULT_DB = path_1.default.join(PACKAGE_ROOT, "backend", "data", "reports.db");
51
- const dbPath = process.env.DB_PATH || DEFAULT_DB;
9
+ const os_1 = __importDefault(require("os"));
10
+ // Store DB in user's home directory — works regardless of where process spawns from
11
+ const dbDir = path_1.default.join(os_1.default.homedir(), ".react-doctor");
12
+ const dbPath = process.env.DB_PATH || path_1.default.join(dbDir, "reports.db");
52
13
  fs_1.default.mkdirSync(path_1.default.dirname(dbPath), { recursive: true });
53
- fs_1.default.mkdirSync(path_1.default.join(path_1.default.dirname(dbPath), "screenshots"), { recursive: true });
54
14
  const db = new better_sqlite3_1.default(dbPath);
55
15
  db.exec(`
56
- CREATE TABLE IF NOT EXISTS reports (
57
- id INTEGER PRIMARY KEY AUTOINCREMENT,
58
- project TEXT NOT NULL,
59
- score INTEGER NOT NULL,
60
- grade TEXT NOT NULL,
61
- analyzed_at TEXT NOT NULL,
62
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
63
- static_json TEXT NOT NULL,
64
- runtime_json TEXT NOT NULL,
65
- suggestions TEXT NOT NULL
66
- );
16
+ CREATE TABLE IF NOT EXISTS reports (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ project TEXT NOT NULL,
19
+ score INTEGER NOT NULL,
20
+ grade TEXT NOT NULL,
21
+ analyzed_at TEXT NOT NULL,
22
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
23
+ payload TEXT NOT NULL
24
+ );
67
25
  `);
68
- exports.screenshotsDir = path_1.default.join(path_1.default.dirname(dbPath), "screenshots");
69
26
  exports.default = db;
@@ -9,34 +9,27 @@ const helmet_1 = __importDefault(require("helmet"));
9
9
  const dotenv_1 = __importDefault(require("dotenv"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const reports_1 = __importDefault(require("./routes/reports"));
12
- const db_1 = require("./db");
13
- dotenv_1.default.config({ path: path_1.default.join(__dirname, "..", ".env") });
12
+ // Load .env from the backend folder, not from cwd
13
+ dotenv_1.default.config({ path: path_1.default.join(__dirname, '..', '.env') });
14
14
  const app = (0, express_1.default)();
15
15
  const PORT = process.env.PORT || 3000;
16
- // ── Middleware ────────────────────────────────────────────────
16
+ const API_KEY = process.env.API_KEY || "react-doctor-secret-key-change-this";
17
+ // Make API_KEY available globally so auth middleware can use it
18
+ process.env.API_KEY = API_KEY;
17
19
  app.use((0, helmet_1.default)());
18
20
  app.use((0, cors_1.default)());
19
- app.use(express_1.default.json({ limit: "50mb" }));
20
- // ── Static: screenshots saved by the upload route ────────────
21
- // Served at /screenshots/<filename>
22
- // The dashboard uses these as <img src="/screenshots/...">
23
- app.use("/screenshots", express_1.default.static(db_1.screenshotsDir));
24
- // ── Routes ────────────────────────────────────────────────────
25
- app.use("/api/reports", reports_1.default);
26
- // ── Health check ──────────────────────────────────────────────
27
- app.get("/health", (_req, res) => {
28
- res.json({ status: "ok", timestamp: new Date().toISOString() });
21
+ app.use(express_1.default.json({ limit: '50mb' }));
22
+ app.use('/api/reports', reports_1.default);
23
+ app.get('/health', (_req, res) => {
24
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
29
25
  });
30
- // ── 404 ───────────────────────────────────────────────────────
31
- app.use((_req, res) => {
32
- res.status(404).json({ message: "Route not found" });
26
+ app.use((req, res) => {
27
+ res.status(404).json({ message: 'Route not found' });
33
28
  });
34
- // ── Error handler ─────────────────────────────────────────────
35
29
  app.use((err, _req, res, _next) => {
36
30
  console.error(err.stack);
37
- res.status(500).json({ message: "Internal Server Error" });
31
+ res.status(500).json({ message: 'Internal Server Error' });
38
32
  });
39
- // ── Start ─────────────────────────────────────────────────────
40
33
  app.listen(PORT, () => {
41
- console.log(`React Doctor backend running on http://localhost:${PORT}`);
34
+ console.log(`🩺 React Doctor backend running on http://localhost:${PORT}`);
42
35
  });
@@ -31,39 +31,6 @@
31
31
  // /screenshots/<filename>
32
32
  // so the dashboard can load them as normal <img> tags.
33
33
  // ─────────────────────────────────────────────────────────────
34
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
35
- if (k2 === undefined) k2 = k;
36
- var desc = Object.getOwnPropertyDescriptor(m, k);
37
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
38
- desc = { enumerable: true, get: function() { return m[k]; } };
39
- }
40
- Object.defineProperty(o, k2, desc);
41
- }) : (function(o, m, k, k2) {
42
- if (k2 === undefined) k2 = k;
43
- o[k2] = m[k];
44
- }));
45
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
46
- Object.defineProperty(o, "default", { enumerable: true, value: v });
47
- }) : function(o, v) {
48
- o["default"] = v;
49
- });
50
- var __importStar = (this && this.__importStar) || (function () {
51
- var ownKeys = function(o) {
52
- ownKeys = Object.getOwnPropertyNames || function (o) {
53
- var ar = [];
54
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
55
- return ar;
56
- };
57
- return ownKeys(o);
58
- };
59
- return function (mod) {
60
- if (mod && mod.__esModule) return mod;
61
- var result = {};
62
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
63
- __setModuleDefault(result, mod);
64
- return result;
65
- };
66
- })();
67
34
  var __importDefault = (this && this.__importDefault) || function (mod) {
68
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
69
36
  };
@@ -71,8 +38,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
71
38
  const express_1 = require("express");
72
39
  const path_1 = __importDefault(require("path"));
73
40
  const fs_1 = __importDefault(require("fs"));
74
- const db_1 = __importStar(require("../db"));
41
+ const db_1 = __importDefault(require("../db"));
75
42
  const auth_1 = require("../middleware/auth");
43
+ const screenshotsDir = "data/screenshots";
76
44
  const router = (0, express_1.Router)();
77
45
  // ── GET /api/reports ─────────────────────────────────────────
78
46
  // Summary list — no blobs, just the columns the history page needs.
@@ -241,7 +209,7 @@ function saveScreenshots(reportId, pending) {
241
209
  const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
242
210
  const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
243
211
  const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
244
- const fullPath = path_1.default.join(db_1.screenshotsDir, filename);
212
+ const fullPath = path_1.default.join(screenshotsDir, filename);
245
213
  try {
246
214
  fs_1.default.writeFileSync(fullPath, shot.buffer);
247
215
  saved.push({
package/backend/src/db.ts CHANGED
@@ -1,70 +1,25 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // backend/src/db.ts
3
- //
4
- // SQLite database via better-sqlite3 (synchronous, fast).
5
- //
6
- // SCHEMA:
7
- //
8
- // reports — one row per analysis run
9
- // id — auto-increment PK
10
- // project — project name / hostname
11
- // score — 0-100 overall performance score
12
- // grade — A+, A, B, C, D, F
13
- // analyzed_at — ISO timestamp from the CLI
14
- // created_at — when the row was inserted
15
- // static_json — full StaticReport as JSON string
16
- // runtime_json — full RuntimeReport map as JSON string
17
- // (screenshots stripped — stored as files)
18
- // suggestions — full Suggestion[] as JSON string
19
- //
20
- // WHY SPLIT COLUMNS INSTEAD OF ONE payload COLUMN?
21
- // The old schema stored everything in a single `payload` TEXT
22
- // column. This meant the dashboard couldn't query individual
23
- // fields without parsing the whole blob. Splitting into three
24
- // typed columns lets us:
25
- // • SELECT only the part we need (e.g. just static_json for
26
- // the issues page)
27
- // • Keep the list endpoint fast (no giant blobs per row)
28
- // • Add indices on score/grade later without changing schema
29
- //
30
- // SCREENSHOT STORAGE:
31
- // Screenshots are base64 PNGs — up to 200KB each. Storing them
32
- // in SQLite bloats the DB and makes every query slower. Instead:
33
- // • The upload route strips dataUrls from runtime_json
34
- // • Saves each screenshot as a .png file in data/screenshots/
35
- // • Replaces dataUrl with a relative path /screenshots/<file>
36
- // The dashboard fetches screenshots via the static file route.
37
- // ─────────────────────────────────────────────────────────────
38
-
39
1
  import Database from "better-sqlite3";
40
- import path from "path";
41
- import fs from "fs";
42
- import dotenv from "dotenv";
43
-
44
- dotenv.config({ path: path.join(__dirname, "..", ".env") });
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import os from "os";
45
5
 
46
- const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
47
- const DEFAULT_DB = path.join(PACKAGE_ROOT, "backend", "data", "reports.db");
48
- const dbPath = process.env.DB_PATH || DEFAULT_DB;
6
+ // Store DB in user's home directory — works regardless of where process spawns from
7
+ const dbDir = path.join(os.homedir(), ".react-doctor");
8
+ const dbPath = process.env.DB_PATH || path.join(dbDir, "reports.db");
49
9
 
50
10
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
51
- fs.mkdirSync(path.join(path.dirname(dbPath), "screenshots"), { recursive: true });
52
11
 
53
12
  const db = new Database(dbPath);
54
-
55
13
  db.exec(`
56
- CREATE TABLE IF NOT EXISTS reports (
57
- id INTEGER PRIMARY KEY AUTOINCREMENT,
58
- project TEXT NOT NULL,
59
- score INTEGER NOT NULL,
60
- grade TEXT NOT NULL,
61
- analyzed_at TEXT NOT NULL,
62
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
63
- static_json TEXT NOT NULL,
64
- runtime_json TEXT NOT NULL,
65
- suggestions TEXT NOT NULL
66
- );
14
+ CREATE TABLE IF NOT EXISTS reports (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ project TEXT NOT NULL,
17
+ score INTEGER NOT NULL,
18
+ grade TEXT NOT NULL,
19
+ analyzed_at TEXT NOT NULL,
20
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
21
+ payload TEXT NOT NULL
22
+ );
67
23
  `);
68
24
 
69
- export const screenshotsDir = path.join(path.dirname(dbPath), "screenshots");
70
- export default db;
25
+ export default db;
@@ -1,46 +1,39 @@
1
- import express, { Request, Response, NextFunction } from "express";
2
- import cors from "cors";
3
- import helmet from "helmet";
4
- import dotenv from "dotenv";
5
- import path from "path";
6
- import reportRoutes from "./routes/reports";
7
- import db, { screenshotsDir } from "./db";
8
-
9
- dotenv.config({ path: path.join(__dirname, "..", ".env") });
10
-
11
- const app = express();
1
+ import express, { Request, Response, NextFunction } from 'express';
2
+ import cors from 'cors';
3
+ import helmet from 'helmet';
4
+ import dotenv from 'dotenv';
5
+ import path from 'path';
6
+ import reportRoutes from './routes/reports';
7
+ import db from './db';
8
+
9
+ // Load .env from the backend folder, not from cwd
10
+ dotenv.config({ path: path.join(__dirname, '..', '.env') });
11
+
12
+ const app = express();
12
13
  const PORT = process.env.PORT || 3000;
14
+ const API_KEY = process.env.API_KEY || "react-doctor-secret-key-change-this";
15
+
16
+ // Make API_KEY available globally so auth middleware can use it
17
+ process.env.API_KEY = API_KEY;
13
18
 
14
- // ── Middleware ────────────────────────────────────────────────
15
19
  app.use(helmet());
16
20
  app.use(cors());
17
- app.use(express.json({ limit: "50mb" }));
21
+ app.use(express.json({ limit: '50mb' }));
22
+ app.use('/api/reports', reportRoutes);
18
23
 
19
- // ── Static: screenshots saved by the upload route ────────────
20
- // Served at /screenshots/<filename>
21
- // The dashboard uses these as <img src="/screenshots/...">
22
- app.use("/screenshots", express.static(screenshotsDir));
23
-
24
- // ── Routes ────────────────────────────────────────────────────
25
- app.use("/api/reports", reportRoutes);
26
-
27
- // ── Health check ──────────────────────────────────────────────
28
- app.get("/health", (_req: Request, res: Response) => {
29
- res.json({ status: "ok", timestamp: new Date().toISOString() });
24
+ app.get('/health', (_req: Request, res: Response) => {
25
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
30
26
  });
31
27
 
32
- // ── 404 ───────────────────────────────────────────────────────
33
- app.use((_req: Request, res: Response) => {
34
- res.status(404).json({ message: "Route not found" });
28
+ app.use((req: Request, res: Response) => {
29
+ res.status(404).json({ message: 'Route not found' });
35
30
  });
36
31
 
37
- // ── Error handler ─────────────────────────────────────────────
38
32
  app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
39
33
  console.error(err.stack);
40
- res.status(500).json({ message: "Internal Server Error" });
34
+ res.status(500).json({ message: 'Internal Server Error' });
41
35
  });
42
36
 
43
- // ── Start ─────────────────────────────────────────────────────
44
37
  app.listen(PORT, () => {
45
- console.log(`React Doctor backend running on http://localhost:${PORT}`);
46
- });
38
+ console.log(`🩺 React Doctor backend running on http://localhost:${PORT}`);
39
+ });
@@ -34,9 +34,11 @@
34
34
  import { Router, Request, Response, RequestHandler } from "express";
35
35
  import path from "path";
36
36
  import fs from "fs";
37
- import db, { screenshotsDir } from "../db";
37
+ import db from "../db";
38
38
  import { requireApiKey } from "../middleware/auth";
39
39
 
40
+ const screenshotsDir = "data/screenshots";
41
+
40
42
  const router = Router();
41
43
 
42
44
  // ── GET /api/reports ─────────────────────────────────────────
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ // ─────────────────────────────────────────────────────────────
3
+ // cli/src/commands/dashboard.ts
4
+ //
5
+ // react-doctor dashboard
6
+ //
7
+ // Opens the React Doctor dashboard in the browser.
8
+ //
9
+ // WHAT IT DOES:
10
+ // 1. Checks if the backend is already running on the port
11
+ // 2. If not — starts it automatically (same logic as --upload)
12
+ // 3. Opens http://localhost:PORT in the default browser
13
+ //
14
+ // This command is the natural companion to --upload.
15
+ // Workflow:
16
+ // react-doctor full ./my-app --upload ← runs analysis + saves report
17
+ // react-doctor dashboard ← opens the dashboard to view it
18
+ //
19
+ // Or in one shot:
20
+ // react-doctor full ./my-app --upload && react-doctor dashboard
21
+ // ─────────────────────────────────────────────────────────────
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.registerDashboardCommand = registerDashboardCommand;
27
+ const path_1 = __importDefault(require("path"));
28
+ const fs_1 = __importDefault(require("fs"));
29
+ const axios_1 = __importDefault(require("axios"));
30
+ const child_process_1 = require("child_process");
31
+ const chalk_1 = __importDefault(require("chalk"));
32
+ const ui_1 = require("../ui");
33
+ function registerDashboardCommand(program) {
34
+ program
35
+ .command("dashboard")
36
+ .description("Open the React Doctor dashboard (auto-starts backend if needed)")
37
+ .option("--port <port>", "Port the backend runs on", "3000")
38
+ .option("--api-key <key>", "API key for the backend", process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this")
39
+ .option("--no-banner", "Skip the banner")
40
+ .action(async (options) => {
41
+ if (!options.noBanner)
42
+ (0, ui_1.printBanner)();
43
+ const port = options.port;
44
+ const apiUrl = `http://localhost:${port}`;
45
+ (0, ui_1.printSection)("Dashboard");
46
+ (0, ui_1.printInfo)("Backend URL", apiUrl);
47
+ console.log();
48
+ const spin = (0, ui_1.spinner)("Checking backend status...");
49
+ try {
50
+ // ── 1. Check if backend is already up ─────────────────
51
+ let backendRunning = false;
52
+ try {
53
+ await axios_1.default.get(`${apiUrl}/health`, { timeout: 2000 });
54
+ backendRunning = true;
55
+ }
56
+ catch {
57
+ backendRunning = false;
58
+ }
59
+ // ── 2. Start backend if not running ───────────────────
60
+ if (!backendRunning) {
61
+ spin.text = " Backend not running — starting automatically...";
62
+ // Locate backend folder (sibling of cli/)
63
+ const projectRoot = path_1.default.resolve(__dirname, "..", "..", "..");
64
+ const backendRoot = path_1.default.resolve(projectRoot, "backend");
65
+ const backendDist = path_1.default.join(backendRoot, "dist", "index.js");
66
+ const backendSrc = path_1.default.join(backendRoot, "src", "index.ts");
67
+ let command;
68
+ let args;
69
+ if (fs_1.default.existsSync(backendDist)) {
70
+ command = "node";
71
+ args = [backendDist];
72
+ }
73
+ else if (fs_1.default.existsSync(backendSrc)) {
74
+ command = "npx";
75
+ args = ["ts-node", backendSrc];
76
+ }
77
+ else {
78
+ spin.fail(chalk_1.default.red("Backend not found"));
79
+ (0, ui_1.printFail)(`Could not find backend at: ${backendRoot}\n\n` +
80
+ ` Make sure the 'backend/' folder exists next to 'cli/'.`);
81
+ process.exit(1);
82
+ }
83
+ // Create data dir inside npm global cache for the backend DB
84
+ const dataDir = path_1.default.join(backendRoot, "data");
85
+ fs_1.default.mkdirSync(dataDir, { recursive: true });
86
+ (0, child_process_1.spawn)(command, args, {
87
+ stdio: "ignore",
88
+ detached: true,
89
+ env: {
90
+ ...process.env,
91
+ API_KEY: options.apiKey,
92
+ PORT: port,
93
+ DB_PATH: path_1.default.join(dataDir, "reports.db"),
94
+ },
95
+ cwd: backendRoot,
96
+ }).unref(); // let CLI exit without killing the server
97
+ // Wait for backend to be ready (up to 15 seconds)
98
+ let ready = false;
99
+ let retries = 0;
100
+ while (!ready && retries < 15) {
101
+ try {
102
+ await axios_1.default.get(`${apiUrl}/health`, { timeout: 1000 });
103
+ ready = true;
104
+ }
105
+ catch {
106
+ await new Promise(r => setTimeout(r, 1000));
107
+ retries++;
108
+ }
109
+ }
110
+ if (!ready) {
111
+ spin.fail(chalk_1.default.red("Backend failed to start"));
112
+ (0, ui_1.printFail)("Backend did not respond after 15 seconds.");
113
+ process.exit(1);
114
+ }
115
+ spin.succeed(chalk_1.default.green("Backend started successfully"));
116
+ }
117
+ else {
118
+ spin.succeed(chalk_1.default.green("Backend already running"));
119
+ }
120
+ // ── 3. Open dashboard in default browser ──────────────
121
+ const dashboardUrl = apiUrl;
122
+ console.log();
123
+ (0, ui_1.printInfo)("Opening", dashboardUrl);
124
+ console.log();
125
+ // Cross-platform browser open
126
+ const openCmd = process.platform === "win32" ? ["cmd", ["/c", "start", dashboardUrl]] :
127
+ process.platform === "darwin" ? ["open", [dashboardUrl]] :
128
+ ["xdg-open", [dashboardUrl]];
129
+ (0, child_process_1.spawn)(openCmd[0], openCmd[1], {
130
+ stdio: "ignore",
131
+ detached: true,
132
+ }).unref();
133
+ (0, ui_1.printDone)(`Dashboard opened at ${chalk_1.default.cyan(dashboardUrl)}`);
134
+ // ── 4. Show quick API reference ───────────────────────
135
+ console.log(chalk_1.default.gray(" Available endpoints:"));
136
+ console.log(chalk_1.default.cyan(` GET ${apiUrl}/health`));
137
+ console.log(chalk_1.default.cyan(` GET ${apiUrl}/api/reports`));
138
+ console.log(chalk_1.default.cyan(` GET ${apiUrl}/api/reports/:id`));
139
+ console.log(chalk_1.default.cyan(` GET ${apiUrl}/api/reports/project/:name`));
140
+ console.log(chalk_1.default.cyan(` POST ${apiUrl}/api/reports/upload`));
141
+ console.log();
142
+ console.log(chalk_1.default.gray(" Tip: run ") +
143
+ chalk_1.default.cyan("react-doctor full ./ --upload") +
144
+ chalk_1.default.gray(" to add a new report.\n"));
145
+ }
146
+ catch (err) {
147
+ spin.fail(chalk_1.default.red("Dashboard failed to open"));
148
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
149
+ process.exit(1);
150
+ }
151
+ });
152
+ }
package/cli/dist/index.js CHANGED
@@ -1,77 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  // ─────────────────────────────────────────────────────────────
4
- // cli/src/index.ts
5
- //
6
- // The CLI entry point. This is the file that runs when the
7
- // user types "react-doctor" in their terminal.
8
- //
9
- // HOW IT WORKS:
10
- // 1. Commander.js parses the command and flags from argv
11
- // 2. The matching command handler is called
12
- // 3. The handler imports core modules and runs the pipeline
13
- //
14
- // HOW THE BINARY REGISTRATION WORKS:
15
- // package.json has a "bin" field:
16
- // "bin": { "react-doctor": "./dist/index.js" }
17
- //
18
- // After "npm link" (dev) or "npm install" (production),
19
- // npm creates a symlink from the system's bin directory
20
- // to this file. That's what makes "react-doctor" a real
21
- // terminal command available anywhere.
22
- //
23
- // THE SHEBANG (#!/usr/bin/env node) on line 1:
24
- // This tells the OS to run this file with Node.js when
25
- // called directly as a script. Without it, the OS doesn't
26
- // know which interpreter to use.
4
+ // cli/src/index.ts — CLI entry point
27
5
  // ─────────────────────────────────────────────────────────────
28
6
  Object.defineProperty(exports, "__esModule", { value: true });
29
7
  const commander_1 = require("commander");
30
8
  const analyze_1 = require("./commands/analyze");
31
9
  const profile_1 = require("./commands/profile");
32
10
  const full_1 = require("./commands/full");
33
- const install_1 = require("./commands/install");
11
+ const dashboard_1 = require("./commands/dashboard");
34
12
  const program = new commander_1.Command();
35
13
  // ── Program metadata ──────────────────────────────────────────
36
14
  program
37
15
  .name("react-doctor")
38
16
  .description("React performance analyzer — static analysis + runtime profiling + smart suggestions")
39
- .version("1.0.0");
40
- // ── Register all commands ─────────────────────────────────────
41
- // Each function adds one command to the program.
42
- // The order here is the order they appear in --help output.
17
+ .version("1.0.2");
18
+ // ── Register commands ─────────────────────────────────────────
43
19
  (0, full_1.registerFullCommand)(program); // react-doctor full
44
20
  (0, analyze_1.registerAnalyzeCommand)(program); // react-doctor analyze
45
21
  (0, profile_1.registerProfileCommand)(program); // react-doctor profile
46
- (0, install_1.registerInstallCommand)(program); // react-doctor install
47
- // ── Usage examples shown at bottom of --help ─────────────────
22
+ (0, dashboard_1.registerDashboardCommand)(program); // react-doctor dashboard
23
+ // ── Usage examples ────────────────────────────────────────────
48
24
  program.addHelpText("after", `
49
25
  Examples:
50
- $ react-doctor full ./my-app Desktop only (default)
51
- $ react-doctor full ./my-app --mobile Mobile only
52
- $ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
53
- $ react-doctor full ./my-app --cpu 4 Simulate slow Android device
54
- $ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
55
- $ react-doctor full ./my-app --throttle 3g Simulate 3G network
56
- $ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
57
- $ react-doctor full ./my-app --upload Upload results to dashboard
26
+ $ react-doctor full ./my-app Run full diagnostic (desktop)
27
+ $ react-doctor full ./my-app --mobile Include mobile viewport
28
+ $ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
29
+ $ react-doctor full ./my-app --cpu 4 Simulate slow Android device
30
+ $ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
31
+ $ react-doctor full ./my-app --throttle 3g Simulate 3G network
32
+ $ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
33
+ $ react-doctor full ./my-app --upload Run + save report to dashboard
58
34
 
59
- $ react-doctor analyze ./my-app Static code analysis only
60
- $ react-doctor analyze ./my-app --full Static + runtime + rules
35
+ $ react-doctor analyze ./my-app Static code analysis only
36
+ $ react-doctor analyze ./my-app --full Static + runtime + rules
61
37
 
62
- $ react-doctor profile ./my-app Desktop only (default)
63
- $ react-doctor profile ./my-app --mobile Mobile only
64
- $ react-doctor profile ./my-app --desktop --mobile Both devices
65
- $ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
66
- $ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
67
- $ react-doctor profile ./my-app --throttle 3g Simulate 3G network
38
+ $ react-doctor profile ./my-app Runtime profiling only (desktop)
39
+ $ react-doctor profile ./my-app --mobile Mobile viewport
40
+ $ react-doctor profile ./my-app --desktop --mobile Both devices
41
+ $ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
42
+ $ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
43
+ $ react-doctor profile ./my-app --throttle 3g Simulate 3G network
68
44
 
69
- $ react-doctor install Install from GitHub into a project
70
- $ react-doctor install --path ./my-app Install into a specific folder
45
+ $ react-doctor dashboard Open dashboard (auto-starts backend)
46
+ $ react-doctor dashboard --port 4000 Use custom port
71
47
  `);
72
48
  // ── Show help if called with no arguments ─────────────────────
73
- // Without this, calling "react-doctor" with no command just
74
- // exits silently, which is confusing. This prints help instead.
75
49
  if (process.argv.length < 3) {
76
50
  program.help();
77
51
  }
@@ -0,0 +1,179 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // cli/src/commands/dashboard.ts
3
+ //
4
+ // react-doctor dashboard
5
+ //
6
+ // Opens the React Doctor dashboard in the browser.
7
+ //
8
+ // WHAT IT DOES:
9
+ // 1. Checks if the backend is already running on the port
10
+ // 2. If not — starts it automatically (same logic as --upload)
11
+ // 3. Opens http://localhost:PORT in the default browser
12
+ //
13
+ // This command is the natural companion to --upload.
14
+ // Workflow:
15
+ // react-doctor full ./my-app --upload ← runs analysis + saves report
16
+ // react-doctor dashboard ← opens the dashboard to view it
17
+ //
18
+ // Or in one shot:
19
+ // react-doctor full ./my-app --upload && react-doctor dashboard
20
+ // ─────────────────────────────────────────────────────────────
21
+
22
+ import { Command } from "commander";
23
+ import path from "path";
24
+ import fs from "fs";
25
+ import axios from "axios";
26
+ import { spawn } from "child_process";
27
+ import chalk from "chalk";
28
+ import {
29
+ printBanner, printSection,
30
+ printDone, printFail, printInfo, spinner,
31
+ } from "../ui";
32
+
33
+ export function registerDashboardCommand(program: Command): void {
34
+ program
35
+ .command("dashboard")
36
+ .description("Open the React Doctor dashboard (auto-starts backend if needed)")
37
+ .option(
38
+ "--port <port>",
39
+ "Port the backend runs on",
40
+ "3000",
41
+ )
42
+ .option(
43
+ "--api-key <key>",
44
+ "API key for the backend",
45
+ process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this",
46
+ )
47
+ .option("--no-banner", "Skip the banner")
48
+ .action(async (options) => {
49
+
50
+ if (!options.noBanner) printBanner();
51
+
52
+ const port = options.port;
53
+ const apiUrl = `http://localhost:${port}`;
54
+
55
+ printSection("Dashboard");
56
+ printInfo("Backend URL", apiUrl);
57
+ console.log();
58
+
59
+ const spin = spinner("Checking backend status...");
60
+
61
+ try {
62
+ // ── 1. Check if backend is already up ─────────────────
63
+ let backendRunning = false;
64
+ try {
65
+ await axios.get(`${apiUrl}/health`, { timeout: 2000 });
66
+ backendRunning = true;
67
+ } catch {
68
+ backendRunning = false;
69
+ }
70
+
71
+ // ── 2. Start backend if not running ───────────────────
72
+ if (!backendRunning) {
73
+ spin.text = " Backend not running — starting automatically...";
74
+
75
+ // Locate backend folder (sibling of cli/)
76
+ const projectRoot = path.resolve(__dirname, "..", "..", "..");
77
+ const backendRoot = path.resolve(projectRoot, "backend");
78
+ const backendDist = path.join(backendRoot, "dist", "index.js");
79
+ const backendSrc = path.join(backendRoot, "src", "index.ts");
80
+
81
+ let command: string;
82
+ let args: string[];
83
+
84
+ if (fs.existsSync(backendDist)) {
85
+ command = "node";
86
+ args = [backendDist];
87
+ } else if (fs.existsSync(backendSrc)) {
88
+ command = "npx";
89
+ args = ["ts-node", backendSrc];
90
+ } else {
91
+ spin.fail(chalk.red("Backend not found"));
92
+ printFail(
93
+ `Could not find backend at: ${backendRoot}\n\n` +
94
+ ` Make sure the 'backend/' folder exists next to 'cli/'.`,
95
+ );
96
+ process.exit(1);
97
+ }
98
+
99
+ // Create data dir inside npm global cache for the backend DB
100
+ const dataDir = path.join(backendRoot, "data");
101
+ fs.mkdirSync(dataDir, { recursive: true });
102
+
103
+ spawn(command, args, {
104
+ stdio: "ignore",
105
+ detached: true,
106
+ env: {
107
+ ...process.env,
108
+ API_KEY: options.apiKey,
109
+ PORT: port,
110
+ DB_PATH: path.join(dataDir, "reports.db"),
111
+ },
112
+ cwd: backendRoot,
113
+ }).unref(); // let CLI exit without killing the server
114
+
115
+ // Wait for backend to be ready (up to 15 seconds)
116
+ let ready = false;
117
+ let retries = 0;
118
+ while (!ready && retries < 15) {
119
+ try {
120
+ await axios.get(`${apiUrl}/health`, { timeout: 1000 });
121
+ ready = true;
122
+ } catch {
123
+ await new Promise(r => setTimeout(r, 1000));
124
+ retries++;
125
+ }
126
+ }
127
+
128
+ if (!ready) {
129
+ spin.fail(chalk.red("Backend failed to start"));
130
+ printFail("Backend did not respond after 15 seconds.");
131
+ process.exit(1);
132
+ }
133
+
134
+ spin.succeed(chalk.green("Backend started successfully"));
135
+ } else {
136
+ spin.succeed(chalk.green("Backend already running"));
137
+ }
138
+
139
+ // ── 3. Open dashboard in default browser ──────────────
140
+ const dashboardUrl = apiUrl;
141
+
142
+ console.log();
143
+ printInfo("Opening", dashboardUrl);
144
+ console.log();
145
+
146
+ // Cross-platform browser open
147
+ const openCmd =
148
+ process.platform === "win32" ? ["cmd", ["/c", "start", dashboardUrl]] :
149
+ process.platform === "darwin" ? ["open", [dashboardUrl]] :
150
+ ["xdg-open", [dashboardUrl]];
151
+
152
+ spawn(openCmd[0] as string, openCmd[1] as string[], {
153
+ stdio: "ignore",
154
+ detached: true,
155
+ }).unref();
156
+
157
+ printDone(`Dashboard opened at ${chalk.cyan(dashboardUrl)}`);
158
+
159
+ // ── 4. Show quick API reference ───────────────────────
160
+ console.log(chalk.gray(" Available endpoints:"));
161
+ console.log(chalk.cyan(` GET ${apiUrl}/health`));
162
+ console.log(chalk.cyan(` GET ${apiUrl}/api/reports`));
163
+ console.log(chalk.cyan(` GET ${apiUrl}/api/reports/:id`));
164
+ console.log(chalk.cyan(` GET ${apiUrl}/api/reports/project/:name`));
165
+ console.log(chalk.cyan(` POST ${apiUrl}/api/reports/upload`));
166
+ console.log();
167
+ console.log(
168
+ chalk.gray(" Tip: run ") +
169
+ chalk.cyan("react-doctor full ./ --upload") +
170
+ chalk.gray(" to add a new report.\n"),
171
+ );
172
+
173
+ } catch (err: any) {
174
+ spin.fail(chalk.red("Dashboard failed to open"));
175
+ console.log(chalk.red(`\n ${err.message}\n`));
176
+ process.exit(1);
177
+ }
178
+ });
179
+ }
package/cli/src/index.ts CHANGED
@@ -1,35 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // ─────────────────────────────────────────────────────────────
3
- // cli/src/index.ts
4
- //
5
- // The CLI entry point. This is the file that runs when the
6
- // user types "react-doctor" in their terminal.
7
- //
8
- // HOW IT WORKS:
9
- // 1. Commander.js parses the command and flags from argv
10
- // 2. The matching command handler is called
11
- // 3. The handler imports core modules and runs the pipeline
12
- //
13
- // HOW THE BINARY REGISTRATION WORKS:
14
- // package.json has a "bin" field:
15
- // "bin": { "react-doctor": "./dist/index.js" }
16
- //
17
- // After "npm link" (dev) or "npm install" (production),
18
- // npm creates a symlink from the system's bin directory
19
- // to this file. That's what makes "react-doctor" a real
20
- // terminal command available anywhere.
21
- //
22
- // THE SHEBANG (#!/usr/bin/env node) on line 1:
23
- // This tells the OS to run this file with Node.js when
24
- // called directly as a script. Without it, the OS doesn't
25
- // know which interpreter to use.
3
+ // cli/src/index.ts — CLI entry point
26
4
  // ─────────────────────────────────────────────────────────────
27
5
 
28
6
  import { Command } from "commander";
29
- import { registerAnalyzeCommand } from "./commands/analyze";
30
- import { registerProfileCommand } from "./commands/profile";
31
- import { registerFullCommand } from "./commands/full";
32
- import { registerInstallCommand } from "./commands/install";
7
+ import { registerAnalyzeCommand } from "./commands/analyze";
8
+ import { registerProfileCommand } from "./commands/profile";
9
+ import { registerFullCommand } from "./commands/full";
10
+ import { registerDashboardCommand } from "./commands/dashboard";
33
11
 
34
12
  const program = new Command();
35
13
 
@@ -37,46 +15,41 @@ const program = new Command();
37
15
  program
38
16
  .name("react-doctor")
39
17
  .description("React performance analyzer — static analysis + runtime profiling + smart suggestions")
40
- .version("1.0.0");
18
+ .version("1.0.2");
41
19
 
42
- // ── Register all commands ─────────────────────────────────────
43
- // Each function adds one command to the program.
44
- // The order here is the order they appear in --help output.
20
+ // ── Register commands ─────────────────────────────────────────
21
+ registerFullCommand(program); // react-doctor full
22
+ registerAnalyzeCommand(program); // react-doctor analyze
23
+ registerProfileCommand(program); // react-doctor profile
24
+ registerDashboardCommand(program); // react-doctor dashboard
45
25
 
46
- registerFullCommand(program); // react-doctor full
47
- registerAnalyzeCommand(program); // react-doctor analyze
48
- registerProfileCommand(program); // react-doctor profile
49
- registerInstallCommand(program); // react-doctor install
50
-
51
- // ── Usage examples shown at bottom of --help ─────────────────
26
+ // ── Usage examples ────────────────────────────────────────────
52
27
  program.addHelpText("after", `
53
28
  Examples:
54
- $ react-doctor full ./my-app Desktop only (default)
55
- $ react-doctor full ./my-app --mobile Mobile only
56
- $ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
57
- $ react-doctor full ./my-app --cpu 4 Simulate slow Android device
58
- $ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
59
- $ react-doctor full ./my-app --throttle 3g Simulate 3G network
60
- $ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
61
- $ react-doctor full ./my-app --upload Upload results to dashboard
62
-
63
- $ react-doctor analyze ./my-app Static code analysis only
64
- $ react-doctor analyze ./my-app --full Static + runtime + rules
65
-
66
- $ react-doctor profile ./my-app Desktop only (default)
67
- $ react-doctor profile ./my-app --mobile Mobile only
68
- $ react-doctor profile ./my-app --desktop --mobile Both devices
69
- $ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
70
- $ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
71
- $ react-doctor profile ./my-app --throttle 3g Simulate 3G network
72
-
73
- $ react-doctor install Install from GitHub into a project
74
- $ react-doctor install --path ./my-app Install into a specific folder
29
+ $ react-doctor full ./my-app Run full diagnostic (desktop)
30
+ $ react-doctor full ./my-app --mobile Include mobile viewport
31
+ $ react-doctor full ./my-app --desktop --mobile Both desktop and mobile
32
+ $ react-doctor full ./my-app --cpu 4 Simulate slow Android device
33
+ $ react-doctor full ./my-app --throttle slow4g Simulate slow 4G network
34
+ $ react-doctor full ./my-app --throttle 3g Simulate 3G network
35
+ $ react-doctor full ./my-app --cpu 4 --throttle 3g Slow device + slow network
36
+ $ react-doctor full ./my-app --upload Run + save report to dashboard
37
+
38
+ $ react-doctor analyze ./my-app Static code analysis only
39
+ $ react-doctor analyze ./my-app --full Static + runtime + rules
40
+
41
+ $ react-doctor profile ./my-app Runtime profiling only (desktop)
42
+ $ react-doctor profile ./my-app --mobile Mobile viewport
43
+ $ react-doctor profile ./my-app --desktop --mobile Both devices
44
+ $ react-doctor profile ./my-app --cpu 4 4x CPU slowdown simulation
45
+ $ react-doctor profile ./my-app --throttle slow4g Simulate slow 4G network
46
+ $ react-doctor profile ./my-app --throttle 3g Simulate 3G network
47
+
48
+ $ react-doctor dashboard Open dashboard (auto-starts backend)
49
+ $ react-doctor dashboard --port 4000 Use custom port
75
50
  `);
76
51
 
77
52
  // ── Show help if called with no arguments ─────────────────────
78
- // Without this, calling "react-doctor" with no command just
79
- // exits silently, which is confusing. This prints help instead.
80
53
  if (process.argv.length < 3) {
81
54
  program.help();
82
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor-cli-dev",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "React performance analyzer with static analysis, runtime profiling, rule engine, and dashboard upload",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",