react-doctor-cli-dev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/backend/.env +3 -0
  2. package/backend/dist/index.js +43 -0
  3. package/backend/dist/middleware/auth.js +16 -0
  4. package/backend/dist/routes/reports.js +93 -0
  5. package/backend/package-lock.json +2000 -0
  6. package/backend/package.json +30 -0
  7. package/backend/src/db.ts +24 -0
  8. package/backend/src/index.ts +49 -0
  9. package/backend/src/middleware/auth.ts +21 -0
  10. package/backend/src/routes/reports.ts +110 -0
  11. package/backend/tsconfig.json +12 -0
  12. package/cli/bin/react-doctor.js +29 -0
  13. package/cli/dist/commands/analyze.js +125 -0
  14. package/cli/dist/commands/full.js +366 -0
  15. package/cli/dist/commands/install.js +138 -0
  16. package/cli/dist/commands/profile.js +166 -0
  17. package/cli/dist/index.js +78 -0
  18. package/cli/dist/ui.js +113 -0
  19. package/cli/package-lock.json +936 -0
  20. package/cli/package.json +34 -0
  21. package/cli/src/commands/analyze.ts +162 -0
  22. package/cli/src/commands/full.ts +574 -0
  23. package/cli/src/commands/install.ts +163 -0
  24. package/cli/src/commands/profile.ts +246 -0
  25. package/cli/src/index.ts +84 -0
  26. package/cli/src/ui.ts +120 -0
  27. package/cli/tsconfig.json +16 -0
  28. package/core/report-compiler/index.ts +359 -0
  29. package/core/report-compiler/test-report-compiler.ts +126 -0
  30. package/core/rule-engine/context-builder.ts +146 -0
  31. package/core/rule-engine/evaluator.ts +131 -0
  32. package/core/rule-engine/index.ts +222 -0
  33. package/core/rule-engine/rules.json +304 -0
  34. package/core/rule-engine/suggestion-builder.ts +209 -0
  35. package/core/rule-engine/test-rule-engine.ts +144 -0
  36. package/core/rule-engine/types.ts +202 -0
  37. package/core/runtime/profiler/browser.ts +121 -0
  38. package/core/runtime/profiler/collectors.ts +216 -0
  39. package/core/runtime/profiler/index.ts +311 -0
  40. package/core/runtime/profiler/porfiler.ts +967 -0
  41. package/core/runtime/profiler/route-scanner.ts +76 -0
  42. package/core/runtime/profiler/score.ts +59 -0
  43. package/core/runtime/profiler/server.ts +115 -0
  44. package/core/runtime/profiler/types.ts +65 -0
  45. package/core/runtime/test-runtime-profiler.ts +226 -0
  46. package/core/static-ana/static/analyzer.ts +145 -0
  47. package/core/static-ana/static/ast-parser.ts +31 -0
  48. package/core/static-ana/static/detectors/console-log.ts +49 -0
  49. package/core/static-ana/static/detectors/dead-code.ts +51 -0
  50. package/core/static-ana/static/detectors/effect-loop.ts +45 -0
  51. package/core/static-ana/static/detectors/index.ts +16 -0
  52. package/core/static-ana/static/detectors/inline-function.ts +59 -0
  53. package/core/static-ana/static/detectors/inline-style.ts +52 -0
  54. package/core/static-ana/static/detectors/large-component.ts +79 -0
  55. package/core/static-ana/static/detectors/missing-key.ts +56 -0
  56. package/core/static-ana/static/detectors/missing-memo.ts +59 -0
  57. package/core/static-ana/static/detectors/prop-drilling.ts +66 -0
  58. package/core/static-ana/static/helpers.ts +81 -0
  59. package/core/static-ana/static/scanner.ts +93 -0
  60. package/core/static-ana/test-analyzer.ts +115 -0
  61. package/core/static-ana/types.ts +25 -0
  62. package/core/tests/mock-react-project/src/app.tsx +22 -0
  63. package/core/tests/mock-react-project/src/components/Button.tsx +9 -0
  64. package/core/tests/mock-react-project/src/components/Header.tsx +3 -0
  65. package/core/tests/mock-react-project/src/components/ListTesting.tsx +51 -0
  66. package/core/tests/mock-react-project/src/components/UserDashboard.tsx +66 -0
  67. package/core/tests/mock-react-project/src/utils.ts +4 -0
  68. package/package.json +55 -0
  69. package/react-doctor-cli-dev-1.0.0.tgz +0 -0
  70. package/shared/dist/index.d.ts +2 -0
  71. package/shared/dist/index.js +19 -0
  72. package/shared/dist/schemas.d.ts +91 -0
  73. package/shared/dist/schemas.js +82 -0
  74. package/shared/dist/types.d.ts +44 -0
  75. package/shared/dist/types.js +2 -0
  76. package/shared/package-lock.json +47 -0
  77. package/shared/package.json +21 -0
  78. package/shared/src/index.ts +4 -0
  79. package/shared/src/schemas.ts +136 -0
  80. package/shared/src/types.ts +137 -0
  81. package/shared/tsconfig.json +15 -0
  82. package/tsconfig.json +25 -0
@@ -0,0 +1,574 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // cli/src/commands/full.ts
3
+ //
4
+ // react-doctor full <projectPath>
5
+ //
6
+ // The main command. Runs the entire React Doctor pipeline:
7
+ //
8
+ // 1. FileScanner → finds all JSX/TSX files
9
+ // 2. StaticAnalyzer → detects bad code patterns
10
+ // 3. RuntimeProfiler → measures live browser performance
11
+ // 4. RuleEngine → combines both reports into suggestions
12
+ // 5. ReportCompiler → merges everything into finalreport.json
13
+ // 6. Upload → sends the report to the backend API
14
+ //
15
+ // HOW IMPORTS WORK:
16
+ // The CLI imports core modules directly as TypeScript classes.
17
+ // No shell commands, no spawning child processes, no ts-node
18
+ // inside ts-node. Everything runs in the same Node.js process,
19
+ // which means objects are passed between steps in memory —
20
+ // fast, clean, and no disk reads between steps.
21
+ //
22
+ // The core folder is 2 levels up from cli/src/commands/:
23
+ // cli/src/commands/full.ts
24
+ // → ../../.. = react-tool root
25
+ // → ../../../core = core folder
26
+ // ─────────────────────────────────────────────────────────────
27
+
28
+ import { Command } from "commander";
29
+ import path from "path";
30
+ import fs from "fs";
31
+ import chalk from "chalk";
32
+ import axios from "axios";
33
+ import { spawn } from "child_process";
34
+ import {
35
+ printBanner,
36
+ printSection,
37
+ printResult,
38
+ printDone,
39
+ printFail,
40
+ printInfo,
41
+ scoreBadge,
42
+ severityIcon,
43
+ vitalStatus,
44
+ spinner,
45
+ } from "../ui";
46
+
47
+ // ── Core imports ──────────────────────────────────────────────
48
+ // These are the actual classes from the core folder.
49
+ // We use require() with a resolved path so they work whether
50
+ // the CLI is run from its own folder or from the project root.
51
+
52
+ function getCoreModule(relativePath: string) {
53
+ // __dirname = cli/src/commands/
54
+ // 3 levels up = react-tool root
55
+ // then into core/
56
+ return require(
57
+ path.resolve(__dirname, "..", "..", "..", "core", relativePath),
58
+ );
59
+ }
60
+
61
+ // ─────────────────────────────────────────────────────────────
62
+ // REGISTER COMMAND
63
+ // ─────────────────────────────────────────────────────────────
64
+
65
+ export function registerFullCommand(program: Command): void {
66
+ program
67
+ .command("full")
68
+ .description(
69
+ "Run the complete React Doctor diagnostic (static + runtime + rules)",
70
+ )
71
+ .argument(
72
+ "[projectPath]",
73
+ "Path to the React project (defaults to current directory)",
74
+ process.cwd(),
75
+ )
76
+ .option(
77
+ "--desktop",
78
+ "Profile on desktop viewport 1280x720 (default if neither flag is passed)",
79
+ false,
80
+ )
81
+ .option(
82
+ "--mobile",
83
+ "Profile on mobile viewport — iPhone 12 Pro 390x844",
84
+ false,
85
+ )
86
+ .option(
87
+ "--cpu <rate>",
88
+ "CPU throttle rate for profiler: 1 (real speed) | 4 (Lighthouse mobile) | 6 (low-end)",
89
+ (v: string) => parseInt(v) as 1 | 4 | 6,
90
+ 1,
91
+ )
92
+ .option(
93
+ "--throttle <preset>",
94
+ "Network throttle: none | slow4g | 3g (only meaningful against deployed URLs)",
95
+ "none",
96
+ )
97
+ .option(
98
+ "--upload",
99
+ "Upload the final report to the React Doctor backend API",
100
+ false,
101
+ )
102
+ .option(
103
+ "--api-url <url>",
104
+ "Backend API URL to upload to",
105
+ "http://localhost:3000",
106
+ )
107
+ // ✅ Single --api-key option (defined BEFORE .action)
108
+ .option(
109
+ "--api-key <key>",
110
+ "API key for backend authentication (overrides REACT_DOCTOR_API_KEY env var)",
111
+ process.env.REACT_DOCTOR_API_KEY || "react-doctor-secret-key-change-this",
112
+ )
113
+ .option("--no-banner", "Skip the banner")
114
+ // ✅ .action() comes LAST, with no trailing semicolon/comment
115
+ .action(async (projectPath: string, options) => {
116
+ await runFullCommand(projectPath, options);
117
+ });
118
+ }
119
+ // ─────────────────────────────────────────────────────────────
120
+ // MAIN RUNNER
121
+ // Exported so other commands (analyze --full) can call it too.
122
+ // ─────────────────────────────────────────────────────────────
123
+
124
+ export async function runFullCommand(
125
+ projectPath: string,
126
+ options: {
127
+ desktop?: boolean;
128
+ mobile?: boolean;
129
+ cpu?: 1 | 4 | 6;
130
+ throttle?: string;
131
+ upload?: boolean;
132
+ apiUrl?: string;
133
+ noBanner?: boolean;
134
+ apiKey?: string;
135
+ } = {},
136
+ ): Promise<void> {
137
+ const resolvedPath = path.resolve(projectPath);
138
+
139
+ if (!options.noBanner) printBanner();
140
+
141
+ // ── Validate that target is a React project ────────────────
142
+ if (!fs.existsSync(path.join(resolvedPath, "package.json"))) {
143
+ printFail(
144
+ `No package.json found at: ${resolvedPath}\n\n` +
145
+ ` Make sure you point to the root of a React project.\n` +
146
+ ` Example: react-doctor full ./my-react-app`,
147
+ );
148
+ process.exit(1);
149
+ }
150
+
151
+ // ── Determine device configuration ──────────────────────────
152
+ // --desktop and --mobile are independent flags.
153
+ // If neither is passed, desktop is the default.
154
+ // If only --mobile is passed, only mobile runs.
155
+ // If both are passed, both run in one pass.
156
+ const wantDesktop = options.desktop || (!options.desktop && !options.mobile);
157
+ const wantMobile = options.mobile ?? false;
158
+
159
+ const devices: ("desktop" | "mobile")[] | "desktop" | "mobile" =
160
+ wantDesktop && wantMobile
161
+ ? ["desktop", "mobile"]
162
+ : wantMobile
163
+ ? "mobile"
164
+ : "desktop";
165
+
166
+ const deviceLabel =
167
+ wantDesktop && wantMobile
168
+ ? "desktop + mobile"
169
+ : wantMobile
170
+ ? "mobile"
171
+ : "desktop";
172
+
173
+ const throttleLabel = options.throttle ?? "none";
174
+ const cpuLabel = options.cpu ?? 1;
175
+
176
+ printSection("Full Diagnostic");
177
+ printInfo("Project", resolvedPath);
178
+ printInfo("Device", deviceLabel);
179
+ printInfo("CPU", `${cpuLabel}x`);
180
+ printInfo("Network", throttleLabel);
181
+
182
+ // ── Output directory ───────────────────────────────────────
183
+ // Reports are saved inside the user's project in a hidden
184
+ // .react-doctor/ folder — easy to find, easy to gitignore.
185
+ const outputDir = path.join(resolvedPath, ".react-doctor");
186
+ fs.mkdirSync(outputDir, { recursive: true });
187
+
188
+ // ════════════════════════════════════════════════════════════
189
+ // STEP 1 — STATIC ANALYSIS
190
+ // ════════════════════════════════════════════════════════════
191
+
192
+ printSection("Step 1 / 4 — Static Analysis");
193
+
194
+ let staticReport: any;
195
+
196
+ const staticSpin = spinner("Scanning JSX/TSX source files...");
197
+ try {
198
+ const { FileScanner } = getCoreModule("static-ana/static/scanner");
199
+ const { StaticAnalyzer } = getCoreModule("static-ana/static/analyzer");
200
+
201
+ const scanner = new FileScanner();
202
+ const analyzer = new StaticAnalyzer();
203
+
204
+ const files = await scanner.findFiles(resolvedPath);
205
+ staticSpin.text = ` Analyzing ${files.length} file(s)...`;
206
+
207
+ staticReport = await analyzer.analyze(files);
208
+
209
+ fs.writeFileSync(
210
+ path.join(outputDir, "staticreport.json"),
211
+ JSON.stringify(staticReport, null, 2),
212
+ );
213
+
214
+ const critical =
215
+ staticReport.issues?.filter((i: any) => i.severity === "critical")
216
+ .length ?? 0;
217
+ const warnings =
218
+ staticReport.issues?.filter((i: any) => i.severity === "warning")
219
+ .length ?? 0;
220
+ const infos =
221
+ staticReport.issues?.filter((i: any) => i.severity === "info").length ??
222
+ 0;
223
+ const total = staticReport.issues?.length ?? 0;
224
+
225
+ staticSpin.succeed(
226
+ chalk.green(`Static analysis complete — ${files.length} files scanned`),
227
+ );
228
+
229
+ printResult(
230
+ "Files analyzed",
231
+ String(staticReport.filesAnalyzed ?? 0),
232
+ "info",
233
+ );
234
+ printResult("Total issues", String(total), total > 0 ? "warn" : "good");
235
+ printResult("Critical", String(critical), critical > 0 ? "poor" : "good");
236
+ printResult("Warnings", String(warnings), warnings > 0 ? "warn" : "good");
237
+ printResult("Info", String(infos), "info");
238
+ printResult("Health grade", staticReport.grade ?? "N/A", "info");
239
+ } catch (err: any) {
240
+ staticSpin.fail(chalk.red("Static analysis failed"));
241
+ console.log(chalk.red(`\n ${err.message}\n`));
242
+ staticReport = null;
243
+ }
244
+
245
+ // ════════════════════════════════════════════════════════════
246
+ // STEP 2 — RUNTIME PROFILING
247
+ // ════════════════════════════════════════════════════════════
248
+
249
+ printSection("Step 2 / 4 — Runtime Profiler");
250
+
251
+ let runtimeReports: Record<string, any> = {};
252
+
253
+ const profilingSpin = spinner("Starting dev server and launching Chrome...");
254
+ try {
255
+ const { RuntimeProfiler } = getCoreModule("runtime/profiler/index");
256
+
257
+ const profiler = new RuntimeProfiler(resolvedPath, outputDir);
258
+ profilingSpin.text = " Profiling... (this takes ~30 seconds per route)";
259
+
260
+ runtimeReports = await profiler.profile([], {
261
+ device: devices,
262
+ throttle: throttleLabel,
263
+ cpuThrottle: cpuLabel,
264
+ });
265
+
266
+ fs.writeFileSync(
267
+ path.join(outputDir, "runtimereport.json"),
268
+ JSON.stringify(runtimeReports, null, 2),
269
+ );
270
+
271
+ const routeKeys = Object.keys(runtimeReports);
272
+ profilingSpin.succeed(
273
+ chalk.green(
274
+ `Profiling complete — ${routeKeys.length} route/device combination(s)`,
275
+ ),
276
+ );
277
+
278
+ // Print results for each route
279
+ for (const [key, report] of Object.entries(runtimeReports)) {
280
+ const [route, device] = key.includes("::")
281
+ ? key.split("::")
282
+ : [key, "desktop"];
283
+
284
+ console.log();
285
+ console.log(
286
+ ` ${chalk.bold(route)} ${chalk.gray(`[${device}]`)} Score: ${scoreBadge(report.performanceScore)}`,
287
+ );
288
+ // ── Device / CPU / Network line ──────────────────────────
289
+ console.log(
290
+ ` ${chalk.gray("Device:")} ${device} ` +
291
+ `${chalk.gray("CPU:")} ${report.cpuThrottling ?? cpuLabel}x ` +
292
+ `${chalk.gray("Network:")} ${throttleLabel}`,
293
+ );
294
+
295
+ printResult(
296
+ "LCP",
297
+ `${report.metrics.lcp.toFixed(0)}ms`,
298
+ vitalStatus("lcp", report.metrics.lcp),
299
+ );
300
+ printResult(
301
+ "FCP",
302
+ `${report.metrics.fcp.toFixed(0)}ms`,
303
+ vitalStatus("fcp", report.metrics.fcp),
304
+ );
305
+ printResult(
306
+ "TTFB",
307
+ `${report.metrics.ttfb.toFixed(0)}ms`,
308
+ vitalStatus("ttfb", report.metrics.ttfb),
309
+ );
310
+ printResult(
311
+ "CLS",
312
+ report.metrics.cls.toFixed(3),
313
+ vitalStatus("cls", report.metrics.cls),
314
+ );
315
+ printResult(
316
+ "INP",
317
+ `${report.metrics.inp.toFixed(0)}ms`,
318
+ vitalStatus("inp", report.metrics.inp),
319
+ );
320
+ printResult(
321
+ "Render time",
322
+ `${report.renderTime}ms`,
323
+ report.renderTime <= 2000
324
+ ? "good"
325
+ : report.renderTime <= 4000
326
+ ? "warn"
327
+ : "poor",
328
+ );
329
+
330
+ if ((report.errors ?? []).length > 0) {
331
+ const errs = report.errors.filter(
332
+ (e: any) => e.type === "error",
333
+ ).length;
334
+ const warn = report.errors.filter(
335
+ (e: any) => e.type === "warning",
336
+ ).length;
337
+ printResult(
338
+ "Issues",
339
+ `${errs} error(s) ${warn} warning(s)`,
340
+ errs > 0 ? "poor" : "warn",
341
+ );
342
+ } else {
343
+ printResult("Issues", "None detected", "good");
344
+ }
345
+ }
346
+ } catch (err: any) {
347
+ profilingSpin.fail(chalk.red("Runtime profiling failed"));
348
+ console.log(chalk.red(`\n ${err.message}\n`));
349
+ // Profiling failure is not fatal — rule engine can still run on static data
350
+ }
351
+
352
+ // ════════════════════════════════════════════════════════════
353
+ // STEP 3 — RULE ENGINE
354
+ // ════════════════════════════════════════════════════════════
355
+
356
+ printSection("Step 3 / 4 — Rule Engine");
357
+
358
+ let ruleResults: any[] = [];
359
+
360
+ const ruleSpin = spinner("Evaluating rules against both reports...");
361
+ try {
362
+ const { RuleEngine } = getCoreModule("rule-engine/index");
363
+
364
+ const engine = new RuleEngine(outputDir);
365
+ ruleResults = await engine.run(staticReport, runtimeReports);
366
+
367
+ const allSuggestions = ruleResults.flatMap((r: any) => r.suggestions);
368
+ fs.writeFileSync(
369
+ path.join(outputDir, "suggestions.json"),
370
+ JSON.stringify(ruleResults, null, 2),
371
+ );
372
+
373
+ const total = allSuggestions.length;
374
+ const critical = allSuggestions.filter(
375
+ (s: any) => s.severity === "critical",
376
+ ).length;
377
+ const warnings = allSuggestions.filter(
378
+ (s: any) => s.severity === "warning",
379
+ ).length;
380
+ const infos = allSuggestions.filter(
381
+ (s: any) => s.severity === "info",
382
+ ).length;
383
+
384
+ ruleSpin.succeed(
385
+ chalk.green(`Rule Engine complete — ${total} suggestion(s) generated`),
386
+ );
387
+
388
+ printResult("Critical", String(critical), critical > 0 ? "poor" : "good");
389
+ printResult("Warnings", String(warnings), warnings > 0 ? "warn" : "good");
390
+ printResult("Info", String(infos), "info");
391
+
392
+ if (total > 0) {
393
+ console.log();
394
+ console.log(chalk.gray(" Top suggestions:"));
395
+ allSuggestions.slice(0, 5).forEach((s: any) => {
396
+ const icon = severityIcon(s.severity);
397
+ const comp = s.affectedComponent
398
+ ? chalk.cyan(` [${s.affectedComponent}]`)
399
+ : "";
400
+ console.log(` ${icon} ${s.title}${comp}`);
401
+ });
402
+ if (total > 5) {
403
+ console.log(
404
+ chalk.gray(`\n ... and ${total - 5} more in the full report.`),
405
+ );
406
+ }
407
+ }
408
+ } catch (err: any) {
409
+ ruleSpin.fail(chalk.red("Rule Engine failed"));
410
+ console.log(chalk.red(`\n ${err.message}\n`));
411
+ }
412
+
413
+ // ════════════════════════════════════════════════════════════
414
+ // STEP 4 — REPORT COMPILER
415
+ // ════════════════════════════════════════════════════════════
416
+
417
+ printSection("Step 4 / 4 — Report Compiler");
418
+
419
+ let finalReport: any = null;
420
+
421
+ const compilerSpin = spinner("Compiling final report...");
422
+ try {
423
+ const { ReportCompiler } = getCoreModule("report-compiler/index");
424
+
425
+ const compiler = new ReportCompiler(outputDir);
426
+
427
+ finalReport = await compiler.compile(
428
+ staticReport,
429
+ runtimeReports,
430
+ ruleResults,
431
+ );
432
+
433
+ fs.writeFileSync(
434
+ path.join(outputDir, "finalreport.json"),
435
+ JSON.stringify(finalReport, null, 2),
436
+ );
437
+
438
+ compilerSpin.succeed(chalk.green("Final report compiled"));
439
+ printResult(
440
+ "Overall score",
441
+ scoreBadge(finalReport.performanceScore),
442
+ "none",
443
+ );
444
+ printResult(
445
+ "Report saved",
446
+ path.join(outputDir, "finalreport.json"),
447
+ "info",
448
+ );
449
+ } catch (err: any) {
450
+ compilerSpin.fail(chalk.red("Report Compiler failed"));
451
+ console.log(chalk.red(`\n ${err.message}\n`));
452
+ }
453
+
454
+ // ════════════════════════════════════════════════════════════
455
+ // OPTIONAL — UPLOAD TO BACKEND API
456
+ // ════════════════════════════════════════════════════════════
457
+
458
+ if (options.upload && finalReport) {
459
+ printSection("Uploading to Backend");
460
+
461
+ const uploadSpin = spinner(`Connecting to ${options.apiUrl}...`);
462
+
463
+ try {
464
+ // Ensure apiUrl has a default
465
+ const apiUrl = options.apiUrl ?? "http://localhost:3000";
466
+
467
+ // 1. Check if backend is already running
468
+ try {
469
+ await axios.get(`${apiUrl}/health`, { timeout: 2000 });
470
+ uploadSpin.text = "Backend detected. Preparing upload...";
471
+ } catch (err) {
472
+ // 2. If not running, start it automatically
473
+
474
+ // Determine the project root directory (where cli/ and backend/ are siblings)
475
+ const projectRoot = path.resolve(__dirname, "..", "..", "..");
476
+
477
+ // Backend is a sibling folder to cli at projectRoot
478
+ const backendRoot = path.resolve(projectRoot, "backend");
479
+
480
+ // Check for compiled JS first (for installed packages)
481
+ const backendDist = path.join(backendRoot, "dist", "index.js");
482
+ // Check for TS source (for local dev)
483
+ const backendSrc = path.join(backendRoot, "src", "index.ts");
484
+
485
+ let command: string;
486
+ let args: string[];
487
+
488
+ if (fs.existsSync(backendDist)) {
489
+ command = "node";
490
+ args = [backendDist];
491
+ } else if (fs.existsSync(backendSrc)) {
492
+ command = "npx";
493
+ args = ["ts-node", backendSrc];
494
+ } else {
495
+ throw new Error(`Cannot find backend at: ${backendRoot}. Ensure 'backend' folder exists next to 'cli'.`);
496
+ }
497
+
498
+ uploadSpin.text = "Backend not found. Starting local server automatically...";
499
+
500
+ // Extract port safely
501
+ const port = new URL(apiUrl).port || "3000";
502
+
503
+ // Spawn the backend process
504
+ // Create backend data directory in the target project
505
+ const backendDataDir = path.join(outputDir, "backend-data");
506
+ fs.mkdirSync(backendDataDir, { recursive: true });
507
+
508
+ const backendProcess = spawn(command, args, {
509
+ stdio: "inherit",
510
+ env: {
511
+ ...process.env,
512
+ API_KEY: options.apiKey || "react-doctor-secret-key-change-this",
513
+ PORT: port,
514
+ DB_PATH: path.join(backendDataDir, "reports.db"),
515
+ },
516
+ cwd: backendRoot
517
+ });
518
+
519
+ // 3. Wait for backend to be ready
520
+ let isReady = false;
521
+ let retries = 0;
522
+ while (!isReady && retries < 15) {
523
+ try {
524
+ await axios.get(`${apiUrl}/health`, { timeout: 1000 });
525
+ isReady = true;
526
+ } catch {
527
+ await new Promise((r) => setTimeout(r, 1000));
528
+ retries++;
529
+ }
530
+ }
531
+
532
+ if (!isReady) throw new Error("Backend failed to start after 15 seconds.");
533
+ uploadSpin.text = "Backend started successfully!";
534
+ }
535
+
536
+ // 4. Perform the actual upload
537
+ uploadSpin.text = "Uploading report...";
538
+ await axios.post(`${apiUrl}/api/reports/upload`, finalReport, {
539
+ headers: {
540
+ "Content-Type": "application/json",
541
+ "x-api-key": options.apiKey || "react-doctor-secret-key-change-this",
542
+ } as Record<string, string>,
543
+ timeout: 10000,
544
+ });
545
+
546
+ uploadSpin.succeed(chalk.green("Report uploaded successfully"));
547
+ } catch (err: any) {
548
+ uploadSpin.fail(chalk.yellow("Upload failed — report saved locally"));
549
+ console.log(chalk.gray(` ${err.message}`));
550
+ }
551
+ }
552
+ // ════════════════════════════════════════════════════════════
553
+ // FINAL SUMMARY
554
+ // ════════════════════════════════════════════════════════════
555
+
556
+ printSection("Summary");
557
+
558
+ console.log(chalk.gray(" Reports saved to:"));
559
+ console.log(chalk.cyan(` ${path.join(outputDir, "staticreport.json")}`));
560
+ console.log(chalk.cyan(` ${path.join(outputDir, "runtimereport.json")}`));
561
+ console.log(chalk.cyan(` ${path.join(outputDir, "suggestions.json")}`));
562
+ console.log(chalk.cyan(` ${path.join(outputDir, "finalreport.json")}`));
563
+ console.log();
564
+
565
+ if (!options.upload) {
566
+ console.log(
567
+ chalk.gray(" Tip: add ") +
568
+ chalk.cyan("--upload") +
569
+ chalk.gray(" to send results to the dashboard."),
570
+ );
571
+ }
572
+
573
+ printDone("Full diagnostic finished.");
574
+ }