react-doctor-cli-dev 1.0.1 → 1.0.3

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.
@@ -1,110 +1,328 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // backend/src/routes/reports.ts
3
+ //
4
+ // All report endpoints.
5
+ //
6
+ // ENDPOINTS:
7
+ //
8
+ // GET /api/reports
9
+ // Returns a summary list (no blobs) — fast for the dashboard
10
+ // history page. Each row has id, project, score, grade, dates.
11
+ //
12
+ // GET /api/reports/:id
13
+ // Returns the full report for one run — static + runtime +
14
+ // suggestions all parsed back to objects.
15
+ //
16
+ // GET /api/reports/project/:name
17
+ // All runs for a named project, summary only.
18
+ //
19
+ // POST /api/reports/upload (requires x-api-key header)
20
+ // Accepts a FinalReport from the CLI.
21
+ // Strips screenshot dataUrls → saves as .png files.
22
+ // Stores static_json, runtime_json, suggestions in DB.
23
+ //
24
+ // SCREENSHOT HANDLING ON UPLOAD:
25
+ // The CLI sends the full FinalReport including base64 screenshots
26
+ // (up to 200KB each). We extract those before storing so the DB
27
+ // stays lean. Each screenshot is saved as:
28
+ // data/screenshots/<reportId>-<routeKey>-<label>.png
29
+ // And the dataUrl in runtime_json is replaced with:
30
+ // /screenshots/<filename>
31
+ // so the dashboard can load them as normal <img> tags.
32
+ // ─────────────────────────────────────────────────────────────
1
33
 
2
34
  import { Router, Request, Response, RequestHandler } from "express";
3
- import db from "../db";
4
- import { requireApiKey } from "../middleware/auth";
5
-
35
+ import path from "path";
36
+ import fs from "fs";
37
+ import db, { screenshotsDir } from "../db";
38
+ import { requireApiKey } from "../middleware/auth";
39
+
6
40
  const router = Router();
7
-
8
41
 
9
- router.get("/", (req: Request, res: Response) => {
42
+ // ── GET /api/reports ─────────────────────────────────────────
43
+ // Summary list — no blobs, just the columns the history page needs.
44
+
45
+ router.get("/", (_req: Request, res: Response) => {
10
46
  try {
11
47
  const rows = db.prepare(`
12
48
  SELECT id, project, score, grade, analyzed_at, created_at
13
- FROM reports
14
- ORDER BY created_at DESC
15
- LIMIT 50
49
+ FROM reports
50
+ ORDER BY created_at DESC
51
+ LIMIT 100
16
52
  `).all() as any[];
17
-
53
+
18
54
  res.json({ count: rows.length, reports: rows });
19
55
  } catch (err: any) {
56
+ console.error("GET / error:", err.message);
20
57
  res.status(500).json({ error: "Internal server error" });
21
58
  }
22
59
  });
23
-
60
+
61
+ // ── GET /api/reports/project/:name ───────────────────────────
62
+ // All runs for one project, summary only.
24
63
 
25
64
  router.get("/project/:name", (req: Request, res: Response) => {
26
65
  try {
27
66
  const rows = db.prepare(`
28
67
  SELECT id, project, score, grade, analyzed_at, created_at
29
- FROM reports
30
- WHERE project = ?
31
- ORDER BY created_at DESC
68
+ FROM reports
69
+ WHERE project = ?
70
+ ORDER BY created_at DESC
32
71
  `).all(req.params.name) as any[];
33
-
34
- res.json({
35
- project: req.params.name,
36
- count: rows.length,
37
- reports: rows,
38
- });
72
+
73
+ res.json({ project: req.params.name, count: rows.length, reports: rows });
39
74
  } catch (err: any) {
75
+ console.error("GET /project/:name error:", err.message);
40
76
  res.status(500).json({ error: "Internal server error" });
41
77
  }
42
78
  });
43
-
44
- // ==========================================
45
- // Endpoint 1: POST /api/report/upload
46
- // يستقبل التقرير من الـ CLI ويتحقق منه ثم يحفظه
47
- // ==========================================
48
- router.post("/upload", requireApiKey as RequestHandler, (req: Request, res: Response) => {
49
- try {
50
- const report = req.body;
51
-
52
- // التحقق من وجود الحقول الإلزامية الثلاثة
53
- if (!report || !report.projectName || !report.analyzedAt || report.performanceScore === undefined) {
54
- res.status(400).json({ error: "Invalid report — missing required fields" });
55
- return;
79
+
80
+ // ── POST /api/reports/upload ──────────────────────────────────
81
+ // Receives a FinalReport from the CLI, strips screenshots to disk,
82
+ // and stores the three JSON blobs in separate columns.
83
+
84
+ router.post(
85
+ "/upload",
86
+ requireApiKey as RequestHandler,
87
+ (req: Request, res: Response) => {
88
+ try {
89
+ const body = req.body;
90
+
91
+ // ── Validate required top-level fields ──────────────────
92
+ if (
93
+ !body ||
94
+ !body.projectName ||
95
+ !body.analyzedAt ||
96
+ body.performanceScore === undefined ||
97
+ !body.static ||
98
+ !body.runtime ||
99
+ !body.suggestions
100
+ ) {
101
+ res.status(400).json({
102
+ error: "Invalid report",
103
+ missing: getMissingFields(body),
104
+ });
105
+ return;
106
+ }
107
+
108
+ // ── Extract grade from static report ────────────────────
109
+ const grade: string = body.static?.grade ?? "N/A";
110
+
111
+ // ── Strip screenshots from runtime, save as .png files ──
112
+ // We do this BEFORE inserting so the DB never holds base64.
113
+ // The row ID isn't known yet — we'll rename after insert.
114
+ // For now we use a temp prefix and rename below.
115
+ const { cleanedRuntime, pendingScreenshots } = extractScreenshots(body.runtime);
116
+
117
+ // ── Insert the row ───────────────────────────────────────
118
+ const stmt = db.prepare(`
119
+ INSERT INTO reports
120
+ (project, score, grade, analyzed_at, static_json, runtime_json, suggestions)
121
+ VALUES
122
+ (?, ?, ?, ?, ?, ?, ?)
123
+ `);
124
+
125
+ const result = stmt.run(
126
+ body.projectName,
127
+ body.performanceScore,
128
+ grade,
129
+ body.analyzedAt,
130
+ JSON.stringify(body.static),
131
+ JSON.stringify(cleanedRuntime),
132
+ JSON.stringify(body.suggestions),
133
+ ) as any;
134
+
135
+ const reportId: number = result.lastInsertRowid;
136
+
137
+ // ── Save screenshots with final filenames ────────────────
138
+ const savedScreenshots = saveScreenshots(reportId, pendingScreenshots);
139
+
140
+ // ── Patch runtime_json with final screenshot paths ───────
141
+ // Now that we have the reportId we can write the correct paths.
142
+ if (savedScreenshots.length > 0) {
143
+ const patchedRuntime = patchScreenshotPaths(
144
+ cleanedRuntime,
145
+ savedScreenshots,
146
+ );
147
+ db.prepare(
148
+ "UPDATE reports SET runtime_json = ? WHERE id = ?"
149
+ ).run(JSON.stringify(patchedRuntime), reportId);
150
+ }
151
+
152
+ res.status(201).json({
153
+ message: "Report saved successfully",
154
+ id: reportId,
155
+ screenshots: savedScreenshots.length,
156
+ });
157
+ } catch (err: any) {
158
+ console.error("POST /upload error:", err.message);
159
+ res.status(500).json({ error: "Internal server error" });
56
160
  }
57
-
58
- const grade = report.static?.grade ?? "N/A";
59
-
60
- // تجهيز استعلام الإدخال لـ SQLite
61
- const stmt = db.prepare(`
62
- INSERT INTO reports (project, score, grade, analyzed_at, payload)
63
- VALUES (?, ?, ?, ?, ?)
64
- `);
65
-
66
- // تنفيذ الاستعلام وحفظ جسم التقرير كـ string
67
- const result = stmt.run(
68
- report.projectName,
69
- report.performanceScore,
70
- grade,
71
- report.analyzedAt,
72
- JSON.stringify(report)
73
- );
74
-
75
- res.status(201).json({
76
- message: "Report saved successfully",
77
- id: result.lastInsertRowid,
78
- });
79
- } catch (err: any) {
80
- console.error("Upload error:", err.message);
81
- res.status(500).json({ error: "Internal server error" });
82
- }
83
- });
84
-
161
+ },
162
+ );
163
+
164
+ // ── GET /api/reports/:id ─────────────────────────────────────
165
+ // Full report for one run — parses all three JSON columns back
166
+ // to objects and returns a unified response.
167
+
85
168
  router.get("/:id", (req: Request, res: Response) => {
86
169
  try {
87
170
  const row = db.prepare(
88
171
  "SELECT * FROM reports WHERE id = ?"
89
172
  ).get(req.params.id) as any;
90
-
173
+
91
174
  if (!row) {
92
175
  res.status(404).json({ error: "Report not found" });
93
176
  return;
94
177
  }
95
-
178
+
96
179
  res.json({
97
- id: row.id,
98
- project: row.project,
99
- score: row.score,
100
- grade: row.grade,
101
- analyzedAt: row.analyzed_at,
102
- createdAt: row.created_at,
103
- report: JSON.parse(row.payload),
180
+ id: row.id,
181
+ project: row.project,
182
+ score: row.score,
183
+ grade: row.grade,
184
+ analyzedAt: row.analyzed_at,
185
+ createdAt: row.created_at,
186
+ // Parse the three JSON blobs back into objects
187
+ static: JSON.parse(row.static_json),
188
+ runtime: JSON.parse(row.runtime_json),
189
+ suggestions: JSON.parse(row.suggestions),
104
190
  });
105
191
  } catch (err: any) {
192
+ console.error("GET /:id error:", err.message);
106
193
  res.status(500).json({ error: "Internal server error" });
107
194
  }
108
195
  });
109
-
110
- export default router;
196
+
197
+ export default router;
198
+
199
+ // ─────────────────────────────────────────────────────────────
200
+ // HELPERS
201
+ // ─────────────────────────────────────────────────────────────
202
+
203
+ function getMissingFields(body: any): string[] {
204
+ const required = ["projectName", "analyzedAt", "performanceScore", "static", "runtime", "suggestions"];
205
+ return required.filter(f => body?.[f] === undefined || body?.[f] === null);
206
+ }
207
+
208
+ interface PendingScreenshot {
209
+ routeKey: string;
210
+ label: string;
211
+ buffer: Buffer;
212
+ // placeholder path written into cleanedRuntime — replaced after insert
213
+ tempPath: string;
214
+ }
215
+
216
+ /**
217
+ * Walk the runtime map, strip every screenshot.dataUrl,
218
+ * and collect them as Buffers ready to write to disk.
219
+ *
220
+ * Returns:
221
+ * cleanedRuntime — runtime map with dataUrls replaced by tempPath markers
222
+ * pendingScreenshots — list of screenshots to save once we have a reportId
223
+ */
224
+ function extractScreenshots(
225
+ runtime: Record<string, any>,
226
+ ): { cleanedRuntime: Record<string, any>; pendingScreenshots: PendingScreenshot[] } {
227
+ const pending: PendingScreenshot[] = [];
228
+ const cleaned: Record<string, any> = {};
229
+
230
+ for (const [routeKey, routeData] of Object.entries(runtime)) {
231
+ const routeClone = { ...routeData };
232
+
233
+ if (Array.isArray(routeClone.screenshots)) {
234
+ routeClone.screenshots = routeClone.screenshots.map((shot: any) => {
235
+ if (!shot.dataUrl || !shot.dataUrl.startsWith("data:image/png;base64,")) {
236
+ return shot;
237
+ }
238
+
239
+ const base64 = shot.dataUrl.replace("data:image/png;base64,", "");
240
+ const buffer = Buffer.from(base64, "base64");
241
+
242
+ // Sanitise routeKey for use in a filename — replace "/" and ":" with "-"
243
+ const safeRoute = routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
244
+ const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
245
+ const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
246
+
247
+ pending.push({ routeKey, label: shot.label, buffer, tempPath });
248
+
249
+ return { ...shot, dataUrl: tempPath };
250
+ });
251
+ }
252
+
253
+ cleaned[routeKey] = routeClone;
254
+ }
255
+
256
+ return { cleanedRuntime: cleaned, pendingScreenshots: pending };
257
+ }
258
+
259
+ interface SavedScreenshot {
260
+ routeKey: string;
261
+ label: string;
262
+ tempPath: string;
263
+ filePath: string; // relative URL path served by express static
264
+ }
265
+
266
+ /**
267
+ * Write each screenshot buffer to data/screenshots/<reportId>-<route>-<label>.png
268
+ * Returns the list of saved files with their final URL paths.
269
+ */
270
+ function saveScreenshots(
271
+ reportId: number,
272
+ pending: PendingScreenshot[],
273
+ ): SavedScreenshot[] {
274
+ const saved: SavedScreenshot[] = [];
275
+
276
+ for (const shot of pending) {
277
+ const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
278
+ const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
279
+ const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
280
+ const fullPath = path.join(screenshotsDir, filename);
281
+
282
+ try {
283
+ fs.writeFileSync(fullPath, shot.buffer);
284
+ saved.push({
285
+ routeKey: shot.routeKey,
286
+ label: shot.label,
287
+ tempPath: shot.tempPath,
288
+ filePath: `/screenshots/${filename}`,
289
+ });
290
+ } catch (err: any) {
291
+ console.warn(`Could not save screenshot ${filename}: ${err.message}`);
292
+ }
293
+ }
294
+
295
+ return saved;
296
+ }
297
+
298
+ /**
299
+ * Replace the __PENDING__ markers in runtime_json with
300
+ * the final /screenshots/<file> URL paths.
301
+ */
302
+ function patchScreenshotPaths(
303
+ runtime: Record<string, any>,
304
+ saved: SavedScreenshot[],
305
+ ): Record<string, any> {
306
+ // Build a lookup from tempPath → filePath
307
+ const lookup: Record<string, string> = {};
308
+ for (const s of saved) lookup[s.tempPath] = s.filePath;
309
+
310
+ const patched: Record<string, any> = {};
311
+
312
+ for (const [routeKey, routeData] of Object.entries(runtime)) {
313
+ const routeClone = { ...routeData };
314
+
315
+ if (Array.isArray(routeClone.screenshots)) {
316
+ routeClone.screenshots = routeClone.screenshots.map((shot: any) => {
317
+ if (shot.dataUrl && lookup[shot.dataUrl]) {
318
+ return { ...shot, dataUrl: lookup[shot.dataUrl] };
319
+ }
320
+ return shot;
321
+ });
322
+ }
323
+
324
+ patched[routeKey] = routeClone;
325
+ }
326
+
327
+ return patched;
328
+ }
@@ -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
  }