react-doctor 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
2
- import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-C35LXalM.js";
2
+ import { A as isReactDoctorError, C as filterSourceFiles, D as groupBy, E as getDiffInfo, F as runInspect, I as toRelativePath, M as listWorkspacePackages, N as resolveScanTarget, O as highlighter, P as restoreLegacyThrow, S as filterDiagnosticsForSurface, T as formatReactDoctorError, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as layerOtlp, k as isMonorepoRoot, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as formatErrorChain, x as discoverReactSubprojects, y as buildJsonReport } from "./cli-logger-BRBUS1pE.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, execSync } from "node:child_process";
5
5
  import path, { join } from "node:path";
@@ -6320,9 +6320,15 @@ const colorizeByScore = (text, score) => {
6320
6320
  return highlighter.error(text);
6321
6321
  };
6322
6322
  //#endregion
6323
+ //#region src/cli/utils/constants.ts
6324
+ const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6325
+ const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6326
+ //#endregion
6323
6327
  //#region src/cli/utils/render-score-header.ts
6324
- const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
6325
- const SCORE_BAR_ANIMATION_FRAME_DELAY_MS = 50;
6328
+ const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
6329
+ const RAINBOW_GRADIENT_WIDTH = 80;
6330
+ const RAINBOW_OKLCH_LIGHTNESS = .638;
6331
+ const RAINBOW_OKLCH_CHROMA = .129;
6326
6332
  const easeOutCubic = (progress) => 1 - (1 - progress) ** 3;
6327
6333
  const sleep = (milliseconds) => Effect.promise(() => new Promise((resolve) => setTimeout(resolve, milliseconds)));
6328
6334
  const buildScoreBarSegments = (filledCount) => {
@@ -6333,6 +6339,36 @@ const buildScoreBarSegments = (filledCount) => {
6333
6339
  };
6334
6340
  };
6335
6341
  const getFilledCount = (score) => Math.round(score / 100 * 50);
6342
+ const joinScoreHeaderFrame = (lines) => `${lines[0]}\n\r${lines[1]}\n\r${lines[2]}\n\r${lines[3]}\n`;
6343
+ const buildRawScoreBar = (displayScore) => {
6344
+ const { filledSegment, emptySegment } = buildScoreBarSegments(getFilledCount(displayScore));
6345
+ return filledSegment + emptySegment;
6346
+ };
6347
+ const buildScoreHeaderLine = (faceLine, rightColumnContent) => {
6348
+ return ` ${faceLine}${rightColumnContent.length > 0 ? " " : ""}${rightColumnContent}`;
6349
+ };
6350
+ const getRightColumnOffset = (faceLine) => ` ${faceLine} `.length;
6351
+ const clampColorChannel = (value) => Math.max(0, Math.min(255, Math.round(value)));
6352
+ const encodeSrgb = (value) => value <= .0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - .055;
6353
+ const oklchToRgb = (lightness, chroma, hue) => {
6354
+ const hueRadians = hue * Math.PI / 180;
6355
+ const labA = chroma * Math.cos(hueRadians);
6356
+ const labB = chroma * Math.sin(hueRadians);
6357
+ const longCone = (lightness + .3963377774 * labA + .2158037573 * labB) ** 3;
6358
+ const mediumCone = (lightness - .1055613458 * labA - .0638541728 * labB) ** 3;
6359
+ const shortCone = (lightness - .0894841775 * labA - 1.291485548 * labB) ** 3;
6360
+ return {
6361
+ red: clampColorChannel(encodeSrgb(4.0767416621 * longCone - 3.3077115913 * mediumCone + .2309699292 * shortCone) * 255),
6362
+ green: clampColorChannel(encodeSrgb(-1.2684380046 * longCone + 2.6097574011 * mediumCone - .3413193965 * shortCone) * 255),
6363
+ blue: clampColorChannel(encodeSrgb(-.0041960863 * longCone - .7034186147 * mediumCone + 1.707614701 * shortCone) * 255)
6364
+ };
6365
+ };
6366
+ const colorizeTrueColor = (text, { red, green, blue }) => `\x1b[38;2;${red};${green};${blue}m${text}\x1b[39m`;
6367
+ const colorizeRainbowText = (text, frame, offset = 0) => [...text].map((character, index) => {
6368
+ if (character === " ") return character;
6369
+ return colorizeTrueColor(character, oklchToRgb(RAINBOW_OKLCH_LIGHTNESS, RAINBOW_OKLCH_CHROMA, ((index + offset) / RAINBOW_GRADIENT_WIDTH * 360 + frame * RAINBOW_HUE_SHIFT_PER_FRAME) % 360));
6370
+ }).join("");
6371
+ const buildRainbowHeaderLine = (faceLine, rightColumnContent, frame) => colorizeRainbowText(buildScoreHeaderLine(faceLine, rightColumnContent), frame);
6336
6372
  const buildScoreBar = (displayScore, colorScore = displayScore) => {
6337
6373
  const { filledSegment, emptySegment } = buildScoreBarSegments(getFilledCount(displayScore));
6338
6374
  return colorizeByScore(filledSegment, colorScore) + highlighter.dim(emptySegment);
@@ -6343,18 +6379,19 @@ const getDoctorFace = (score) => {
6343
6379
  return ["x x", " ▽ "];
6344
6380
  };
6345
6381
  const BRANDING_LINE = `React Doctor ${highlighter.dim("(https://react.doctor)")}`;
6346
- const buildFaceRenderedLines = (score) => {
6382
+ const RAW_BRANDING_LINE = "React Doctor (https://react.doctor)";
6383
+ const buildRawFaceLines = (score) => {
6347
6384
  const [eyes, mouth] = getDoctorFace(score);
6348
- const colorize = (text) => colorizeByScore(text, score);
6349
6385
  return [
6350
6386
  "┌─────┐",
6351
6387
  `│ ${eyes} │`,
6352
6388
  `│ ${mouth} │`,
6353
6389
  "└─────┘"
6354
- ].map(colorize);
6390
+ ];
6355
6391
  };
6356
- const buildScoreHeaderLine = (faceLine, rightColumnContent) => {
6357
- return ` ${faceLine}${rightColumnContent.length > 0 ? " " : ""}${rightColumnContent}`;
6392
+ const buildFaceRenderedLines = (score) => {
6393
+ const colorize = (text) => colorizeByScore(text, score);
6394
+ return buildRawFaceLines(score).map(colorize);
6358
6395
  };
6359
6396
  const writeScoreHeaderLine = (line) => Effect.sync(() => {
6360
6397
  process.stdout.write(line);
@@ -6365,29 +6402,97 @@ const buildScoreLine = (displayScore, finalScore, label, projectName) => {
6365
6402
  const projectSuffix = projectName ? ` ${highlighter.dim("·")} ${highlighter.dim(projectName)}` : "";
6366
6403
  return `${scoreNumber} ${highlighter.dim(`/ 100`)} ${scoreLabel}${projectSuffix}`;
6367
6404
  };
6405
+ const buildRawScoreLine = (displayScore, label, projectName) => {
6406
+ return `${displayScore} / 100 ${label}${projectName ? ` · ${projectName}` : ""}`;
6407
+ };
6408
+ const buildRainbowScoreHeaderFrame = ({ score, displayScore, label, frame, projectName }) => {
6409
+ const rawFaceLines = buildRawFaceLines(score);
6410
+ return joinScoreHeaderFrame([
6411
+ buildRainbowHeaderLine(rawFaceLines[0] ?? "", buildRawScoreLine(displayScore, label, projectName), frame),
6412
+ buildRainbowHeaderLine(rawFaceLines[1] ?? "", buildRawScoreBar(displayScore), frame),
6413
+ buildRainbowHeaderLine(rawFaceLines[2] ?? "", RAW_BRANDING_LINE, frame),
6414
+ buildRainbowHeaderLine(rawFaceLines[3] ?? "", "", frame)
6415
+ ]);
6416
+ };
6417
+ const buildFinalPerfectScoreHeaderFrame = (score, label, frame, projectName) => {
6418
+ const rawFaceLines = buildRawFaceLines(score);
6419
+ const renderedFaceLines = buildFaceRenderedLines(score);
6420
+ const rainbowBarLine = colorizeRainbowText(buildRawScoreBar(score), frame, getRightColumnOffset(rawFaceLines[1] ?? ""));
6421
+ return joinScoreHeaderFrame([
6422
+ buildScoreHeaderLine(renderedFaceLines[0] ?? "", buildScoreLine(score, score, label, projectName)),
6423
+ buildScoreHeaderLine(renderedFaceLines[1] ?? "", rainbowBarLine),
6424
+ buildScoreHeaderLine(renderedFaceLines[2] ?? "", BRANDING_LINE),
6425
+ buildScoreHeaderLine(renderedFaceLines[3] ?? "", "")
6426
+ ]);
6427
+ };
6428
+ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex, renderedFaceLine, rawFaceLine, rightColumnContent, rawRightColumnContent, score }) => {
6429
+ if (!isPerfectScore) return buildScoreHeaderLine(renderedFaceLine, rightColumnContent);
6430
+ if (shouldAnimate) return buildRainbowHeaderLine(rawFaceLine, rawRightColumnContent, 0);
6431
+ if (lineIndex !== 1) return buildScoreHeaderLine(renderedFaceLine, rightColumnContent);
6432
+ return buildScoreHeaderLine(renderedFaceLine, colorizeRainbowText(buildRawScoreBar(score), 0, getRightColumnOffset(rawFaceLine)));
6433
+ };
6368
6434
  const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
6369
- for (let frame = 0; frame <= SCORE_BAR_ANIMATION_FRAME_COUNT; frame += 1) {
6370
- const progress = easeOutCubic(frame / SCORE_BAR_ANIMATION_FRAME_COUNT);
6435
+ const isPerfectScore = score === 100;
6436
+ for (let frame = 0; frame <= 40; frame += 1) {
6437
+ const progress = easeOutCubic(frame / 40);
6371
6438
  const animatedScore = Math.round(score * progress);
6439
+ if (isPerfectScore) {
6440
+ yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
6441
+ score,
6442
+ displayScore: animatedScore,
6443
+ label,
6444
+ frame,
6445
+ projectName
6446
+ })}`);
6447
+ if (frame < 40) yield* sleep(50);
6448
+ continue;
6449
+ }
6372
6450
  const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
6373
6451
  const animatedBarLine = buildScoreBar(animatedScore, score);
6374
6452
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
6375
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6453
+ if (frame < 40) yield* sleep(50);
6454
+ }
6455
+ if (!isPerfectScore) return;
6456
+ for (let frame = 0; frame < 16; frame += 1) {
6457
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
6458
+ score,
6459
+ displayScore: score,
6460
+ label,
6461
+ frame,
6462
+ projectName
6463
+ })}`);
6464
+ yield* sleep(50);
6376
6465
  }
6466
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
6377
6467
  });
6378
6468
  const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
6469
+ const isPerfectScore = scoreResult.score === 100;
6379
6470
  const renderedFaceLines = buildFaceRenderedLines(scoreResult.score);
6471
+ const rawFaceLines = buildRawFaceLines(scoreResult.score);
6380
6472
  const shouldAnimate = !isSpinnerSilent() && isSpinnerInteractive(process.stdout);
6473
+ const displayScore = shouldAnimate ? 0 : scoreResult.score;
6381
6474
  const rightColumnLines = [
6382
- buildScoreLine(shouldAnimate ? 0 : scoreResult.score, scoreResult.score, scoreResult.label, projectName),
6475
+ buildScoreLine(displayScore, scoreResult.score, scoreResult.label, projectName),
6383
6476
  shouldAnimate ? buildScoreBar(0, scoreResult.score) : buildScoreBar(scoreResult.score),
6384
6477
  BRANDING_LINE,
6385
6478
  ""
6386
6479
  ];
6387
- for (let lineIndex = 0; lineIndex < renderedFaceLines.length; lineIndex += 1) {
6388
- const rightColumnContent = rightColumnLines[lineIndex] ?? "";
6389
- yield* Console.log(buildScoreHeaderLine(renderedFaceLines[lineIndex], rightColumnContent));
6390
- }
6480
+ const rawRightColumnLines = [
6481
+ buildRawScoreLine(displayScore, scoreResult.label, projectName),
6482
+ buildRawScoreBar(displayScore),
6483
+ RAW_BRANDING_LINE,
6484
+ ""
6485
+ ];
6486
+ for (let lineIndex = 0; lineIndex < renderedFaceLines.length; lineIndex += 1) yield* Console.log(buildInitialScoreHeaderLine({
6487
+ isPerfectScore,
6488
+ shouldAnimate,
6489
+ lineIndex,
6490
+ renderedFaceLine: renderedFaceLines[lineIndex] ?? "",
6491
+ rawFaceLine: rawFaceLines[lineIndex] ?? "",
6492
+ rightColumnContent: rightColumnLines[lineIndex] ?? "",
6493
+ rawRightColumnContent: rawRightColumnLines[lineIndex] ?? "",
6494
+ score: scoreResult.score
6495
+ }));
6391
6496
  yield* Console.log("");
6392
6497
  if (shouldAnimate) {
6393
6498
  yield* writeScoreHeaderLine("\x1B[5A");
@@ -6561,7 +6666,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6561
6666
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6562
6667
  //#endregion
6563
6668
  //#region src/cli/utils/version.ts
6564
- const VERSION = "0.2.8";
6669
+ const VERSION = "0.2.10";
6565
6670
  //#endregion
6566
6671
  //#region src/inspect.ts
6567
6672
  const silentConsole = makeNoopConsole();
@@ -6749,10 +6854,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
6749
6854
  return buildResult();
6750
6855
  });
6751
6856
  //#endregion
6752
- //#region src/cli/utils/constants.ts
6753
- const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6754
- const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6755
- //#endregion
6756
6857
  //#region src/cli/utils/get-staged-files.ts
6757
6858
  const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
6758
6859
  const getStagedSourceFiles = async (directory) => {
@@ -7311,7 +7412,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
7311
7412
  return true;
7312
7413
  };
7313
7414
  const shouldPromptInstallSetup = (options) => {
7314
- if (!options.hasScoredScan) return false;
7415
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7315
7416
  if (options.isJsonMode) return false;
7316
7417
  if (options.isScoreOnly) return false;
7317
7418
  if (options.isStaged) return false;
@@ -7321,13 +7422,13 @@ const shouldPromptInstallSetup = (options) => {
7321
7422
  return !hasDoctorScript(options.projectRoot);
7322
7423
  };
7323
7424
  const resolveInstallSetupProjectRoot = (options) => {
7324
- if (options.completedScanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7425
+ if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7325
7426
  const packageDirectories = /* @__PURE__ */ new Set();
7326
- for (const scanDirectory of options.completedScanDirectories) {
7427
+ for (const scanDirectory of options.scanDirectories) {
7327
7428
  const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
7328
7429
  packageDirectories.add(packageDirectory);
7329
7430
  }
7330
- if (packageDirectories.size !== 1) return null;
7431
+ if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
7331
7432
  return [...packageDirectories][0] ?? null;
7332
7433
  };
7333
7434
  const defaultWait = (milliseconds) => new Promise((resolve) => {
@@ -7362,7 +7463,7 @@ const warnSetupPromptFailure = async (options, error) => {
7362
7463
  return;
7363
7464
  }
7364
7465
  try {
7365
- const { cliLogger } = await import("./cli-logger-C35LXalM.js").then((n) => n.n);
7466
+ const { cliLogger } = await import("./cli-logger-BRBUS1pE.js").then((n) => n.n);
7366
7467
  cliLogger.warn(message);
7367
7468
  } catch {}
7368
7469
  };
@@ -7378,7 +7479,7 @@ const promptInstallSetup = async (options) => {
7378
7479
  writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
7379
7480
  return;
7380
7481
  }
7381
- const install = options.install ?? (await Promise.resolve().then(() => install_skill_exports)).runInstallSkill;
7482
+ const install = options.install ?? (await Promise.resolve().then(() => install_react_doctor_exports)).runInstallReactDoctor;
7382
7483
  const previousExitCode = process.exitCode;
7383
7484
  let setupExitCode;
7384
7485
  try {
@@ -7397,7 +7498,7 @@ const promptInstallSetup = async (options) => {
7397
7498
  }
7398
7499
  };
7399
7500
  const shouldShowAgentInstallHint = (options) => {
7400
- if (!options.hasScoredScan) return false;
7501
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7401
7502
  if (options.isJsonMode) return false;
7402
7503
  if (options.isScoreOnly) return false;
7403
7504
  if (options.isStaged) return false;
@@ -7462,7 +7563,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
7462
7563
  const { scanScope } = await prompts({
7463
7564
  type: "select",
7464
7565
  name: "scanScope",
7465
- message: "Select",
7566
+ message: "Choose what to scan",
7466
7567
  choices: [{
7467
7568
  title: "Full codebase",
7468
7569
  value: "full"
@@ -7604,7 +7705,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
7604
7705
  }
7605
7706
  if (packages.length === 0) return [rootDirectory];
7606
7707
  if (packages.length === 1) {
7607
- cliLogger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages[0].name}`);
7708
+ cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages[0].name}`);
7608
7709
  return [packages[0].directory];
7609
7710
  }
7610
7711
  if (projectFlag) return resolveProjectFlag(projectFlag, packages);
@@ -7628,13 +7729,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
7628
7729
  return resolvedDirectories;
7629
7730
  };
7630
7731
  const printDiscoveredProjects = (packages) => {
7631
- cliLogger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
7732
+ cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
7632
7733
  };
7633
7734
  const promptProjectSelection = async (workspacePackages, rootDirectory) => {
7634
7735
  const { selectedDirectories } = await prompts({
7635
7736
  type: "multiselect",
7636
7737
  name: "selectedDirectories",
7637
- message: "Select projects to scan",
7738
+ message: "Select projects",
7638
7739
  choices: workspacePackages.map((workspacePackage) => ({
7639
7740
  title: workspacePackage.name,
7640
7741
  description: path.relative(rootDirectory, workspacePackage.directory),
@@ -7891,13 +7992,13 @@ const inspectAction = async (directory, flags) => {
7891
7992
  });
7892
7993
  const setupProjectRoot = resolveInstallSetupProjectRoot({
7893
7994
  scanRoot: resolvedDirectory,
7894
- completedScanDirectories: completedScans.map((scan) => scan.directory)
7995
+ scanDirectories: projectDirectories
7895
7996
  });
7896
7997
  if (setupProjectRoot !== null) {
7897
- const hasScoredScan = completedScans.some((scan) => scan.result.score !== null);
7998
+ const hasCompletedScan = completedScans.length > 0;
7898
7999
  await promptInstallSetup({
7899
8000
  projectRoot: setupProjectRoot,
7900
- hasScoredScan,
8001
+ hasCompletedScan,
7901
8002
  issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
7902
8003
  isJsonMode,
7903
8004
  isScoreOnly,
@@ -7906,7 +8007,7 @@ const inspectAction = async (directory, flags) => {
7906
8007
  });
7907
8008
  if (shouldShowAgentInstallHint({
7908
8009
  projectRoot: setupProjectRoot,
7909
- hasScoredScan,
8010
+ hasCompletedScan,
7910
8011
  isJsonMode,
7911
8012
  isScoreOnly,
7912
8013
  isStaged: Boolean(flags.staged)
@@ -8538,8 +8639,12 @@ const installReactDoctorGitHook = (options) => {
8538
8639
  return installDirectGitHook(options);
8539
8640
  };
8540
8641
  //#endregion
8541
- //#region src/cli/utils/install-skill.ts
8542
- var install_skill_exports = /* @__PURE__ */ __exportAll({ runInstallSkill: () => runInstallSkill });
8642
+ //#region src/cli/utils/install-react-doctor.ts
8643
+ var install_react_doctor_exports = /* @__PURE__ */ __exportAll({ runInstallReactDoctor: () => runInstallReactDoctor });
8644
+ const SETUP_OPTION_GIT_HOOK = "git-hook";
8645
+ const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
8646
+ const SETUP_OPTION_WORKFLOW = "workflow";
8647
+ const SETUP_OPTION_SKIP = "skip";
8543
8648
  const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
8544
8649
  "ghooks",
8545
8650
  "git-hooks-js",
@@ -8646,7 +8751,8 @@ const defaultInstallDependencyRunner = (input) => {
8646
8751
  env: {
8647
8752
  ...process.env,
8648
8753
  REACT_DOCTOR_INSTALL: "1"
8649
- }
8754
+ },
8755
+ shell: process.platform === "win32"
8650
8756
  });
8651
8757
  };
8652
8758
  const installReactDoctorDependency = async (options) => {
@@ -8719,7 +8825,30 @@ const getSkillSourceDirectory = () => {
8719
8825
  const distDirectory = path.dirname(fileURLToPath(import.meta.url));
8720
8826
  return path.join(distDirectory, "skills", SKILL_NAME);
8721
8827
  };
8722
- const runInstallSkill = async (options = {}) => {
8828
+ const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
8829
+ const buildWorkflowContent = () => [
8830
+ "name: React Doctor",
8831
+ "",
8832
+ "on:",
8833
+ " pull_request:",
8834
+ " branches: [main]",
8835
+ "",
8836
+ "permissions:",
8837
+ " contents: read",
8838
+ " pull-requests: write",
8839
+ "",
8840
+ "jobs:",
8841
+ " react-doctor:",
8842
+ " runs-on: ubuntu-latest",
8843
+ " steps:",
8844
+ " - uses: actions/checkout@v4",
8845
+ " - uses: millionco/react-doctor@main",
8846
+ " with:",
8847
+ " github-token: ${{ secrets.GITHUB_TOKEN }}",
8848
+ " diff: main",
8849
+ ""
8850
+ ].join("\n");
8851
+ const runInstallReactDoctor = async (options = {}) => {
8723
8852
  const requestedProjectRoot = options.projectRoot ?? process.cwd();
8724
8853
  const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
8725
8854
  const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
@@ -8740,7 +8869,8 @@ const runInstallSkill = async (options = {}) => {
8740
8869
  const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
8741
8870
  const gitHookPath = gitHookTarget?.hookPath;
8742
8871
  const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
8743
- const selectedAgents = skipPrompts ? detectedAgents : (await prompts({
8872
+ const prompt = options.prompt ?? prompts;
8873
+ const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
8744
8874
  type: "multiselect",
8745
8875
  name: "agents",
8746
8876
  message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
@@ -8753,13 +8883,48 @@ const runInstallSkill = async (options = {}) => {
8753
8883
  min: 1
8754
8884
  }, promptOptions)).agents ?? [];
8755
8885
  if (selectedAgents.length === 0) return;
8756
- const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !skipPrompts && Boolean((await prompts({
8757
- type: "confirm",
8758
- name: "installGitHook",
8759
- message: "Check for issues before each commit?",
8760
- initial: true
8761
- }, promptOptions)).installGitHook));
8762
- const shouldInstallAgentHooks = Boolean(options.agentHooks);
8886
+ const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8887
+ const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8888
+ const hasExistingWorkflows = existsSync(workflowsDirectory);
8889
+ const canInstallWorkflow = !existsSync(workflowTargetPath);
8890
+ const setupActionChoices = [
8891
+ ...gitHookPath === null || gitHookPath === void 0 ? [] : [{
8892
+ title: "Pre-commit hook",
8893
+ description: "Check staged changes before each commit",
8894
+ value: SETUP_OPTION_GIT_HOOK,
8895
+ selected: true
8896
+ }],
8897
+ ...canInstallNativeAgentHooks(selectedAgents) ? [{
8898
+ title: "Agent hooks",
8899
+ description: "Ask Claude Code or Cursor to scan after code edits",
8900
+ value: SETUP_OPTION_AGENT_HOOKS,
8901
+ selected: Boolean(options.agentHooks)
8902
+ }] : [],
8903
+ ...canInstallWorkflow ? [{
8904
+ title: "GitHub Actions workflow",
8905
+ description: "Scan pull requests in CI",
8906
+ value: SETUP_OPTION_WORKFLOW,
8907
+ selected: hasExistingWorkflows
8908
+ }] : []
8909
+ ];
8910
+ const setupChoices = setupActionChoices.length === 0 ? [] : [{
8911
+ title: "Skip optional setup",
8912
+ description: "Install only the agent skill and package setup",
8913
+ value: SETUP_OPTION_SKIP,
8914
+ selected: false
8915
+ }, ...setupActionChoices];
8916
+ const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
8917
+ type: "multiselect",
8918
+ name: "setupOptions",
8919
+ message: "Select additional React Doctor setup:",
8920
+ choices: setupChoices,
8921
+ instructions: false
8922
+ }, promptOptions)).setupOptions ?? [];
8923
+ const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
8924
+ const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
8925
+ const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
8926
+ const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
8927
+ const shouldInstallWorkflow = !skipPrompts && !didSkipOptionalSetup && canInstallWorkflow && selectedSetupActions.includes(SETUP_OPTION_WORKFLOW);
8763
8928
  if (options.dryRun) {
8764
8929
  cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
8765
8930
  for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
@@ -8768,6 +8933,7 @@ const runInstallSkill = async (options = {}) => {
8768
8933
  cliLogger.dim(" Dev dependency: react-doctor");
8769
8934
  if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
8770
8935
  if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
8936
+ if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
8771
8937
  return;
8772
8938
  }
8773
8939
  const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
@@ -8815,47 +8981,15 @@ const runInstallSkill = async (options = {}) => {
8815
8981
  throw error;
8816
8982
  }
8817
8983
  }
8818
- const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8819
- const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8820
- if (!existsSync(workflowTargetPath) && !skipPrompts) {
8821
- const hasExistingWorkflows = existsSync(workflowsDirectory);
8822
- const { shouldInstallWorkflow } = await prompts({
8823
- type: "confirm",
8824
- name: "shouldInstallWorkflow",
8825
- message: "Add a GitHub Actions workflow to scan PRs?",
8826
- initial: hasExistingWorkflows
8827
- }, promptOptions);
8828
- if (shouldInstallWorkflow) {
8829
- if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
8830
- const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
8831
- try {
8832
- writeFileSync(workflowTargetPath, [
8833
- "name: React Doctor",
8834
- "",
8835
- "on:",
8836
- " pull_request:",
8837
- " branches: [main]",
8838
- "",
8839
- "permissions:",
8840
- " contents: read",
8841
- " pull-requests: write",
8842
- "",
8843
- "jobs:",
8844
- " react-doctor:",
8845
- " runs-on: ubuntu-latest",
8846
- " steps:",
8847
- " - uses: actions/checkout@v4",
8848
- " - uses: millionco/react-doctor@main",
8849
- " with:",
8850
- " github-token: ${{ secrets.GITHUB_TOKEN }}",
8851
- " diff: main",
8852
- ""
8853
- ].join("\n"));
8854
- workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
8855
- } catch (error) {
8856
- workflowSpinner.fail("Failed to add GitHub Actions workflow.");
8857
- throw error;
8858
- }
8984
+ if (shouldInstallWorkflow) {
8985
+ if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
8986
+ const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
8987
+ try {
8988
+ writeFileSync(workflowTargetPath, buildWorkflowContent());
8989
+ workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
8990
+ } catch (error) {
8991
+ workflowSpinner.fail("Failed to add GitHub Actions workflow.");
8992
+ throw error;
8859
8993
  }
8860
8994
  }
8861
8995
  };
@@ -8865,7 +8999,7 @@ const installAction = async (options, command) => {
8865
8999
  Effect.runSync(printBrandedHeader);
8866
9000
  try {
8867
9001
  const parentOptions = command?.parent?.opts?.();
8868
- await runInstallSkill({
9002
+ await runInstallReactDoctor({
8869
9003
  yes: options.yes ?? parentOptions?.yes,
8870
9004
  dryRun: options.dryRun,
8871
9005
  agentHooks: options.agentHooks,
package/dist/index.d.ts CHANGED
@@ -232,7 +232,9 @@ interface ReactDoctorConfig {
232
232
  * `categories` field, but keyed by React Doctor's display
233
233
  * categories (`"Server"`, `"React Native"`, `"Architecture"`,
234
234
  * `"Bundle Size"`, `"State & Effects"`, `"Security"`,
235
- * `"Accessibility"`, `"Performance"`, `"Correctness"`, …).
235
+ * `"Accessibility"`, `"Performance"`, `"Correctness"`,
236
+ * `"Next.js"`, `"Preact"`, `"TanStack Query"`,
237
+ * `"TanStack Start"`, …).
236
238
  *
237
239
  * ```json
238
240
  * { "categories": { "React Native": "warn", "Server": "off" } }
@@ -296,7 +298,7 @@ interface Diagnostic {
296
298
  }
297
299
  //#endregion
298
300
  //#region src/types/project-info.d.ts
299
- type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "unknown";
301
+ type Framework = "nextjs" | "vite" | "cra" | "remix" | "gatsby" | "expo" | "react-native" | "tanstack-start" | "preact" | "unknown";
300
302
  interface ProjectInfo {
301
303
  rootDirectory: string;
302
304
  projectName: string;
@@ -307,6 +309,16 @@ interface ProjectInfo {
307
309
  hasTypeScript: boolean;
308
310
  hasReactCompiler: boolean;
309
311
  hasTanStackQuery: boolean;
312
+ /**
313
+ * `true` when `preact` is declared anywhere in the project's
314
+ * dependency manifest. Drives the `preact` capability in
315
+ * `buildCapabilities`, which gates every `preact-*` rule. Modeled
316
+ * on `hasTanStackQuery` rather than the `framework` field because
317
+ * the dominant Preact setup today is Preact-on-Vite — those
318
+ * projects classify as `framework: "vite"` for build-tool reasons
319
+ * but still need Preact-specific rules to fire.
320
+ */
321
+ hasPreact: boolean;
310
322
  /**
311
323
  * `true` when the project (or any of its workspace packages) declares
312
324
  * React Native or Expo as a dependency. Enables the `react-native`
@@ -355,8 +367,14 @@ interface DiagnoseResult {
355
367
  skippedCheckReasons?: Record<string, string>;
356
368
  project: ProjectInfo;
357
369
  elapsedMilliseconds: number;
358
- } //#endregion
359
- //#region src/types/handle-error.d.ts
370
+ }
371
+ /**
372
+ * A single project to scan as part of a `diagnoseProjects()` batch.
373
+ * Scan options (`deadCode`, `lint`, etc.) are flat on the entry and
374
+ * layer on top of the global defaults — omitted fields fall through.
375
+ * `config` is a full `ReactDoctorConfig` override that replaces the
376
+ * on-disk `react-doctor.config.json` for this project's scan.
377
+ */
360
378
  //#endregion
361
379
  //#region src/types/inspect.d.ts
362
380
  interface InspectResult {
@@ -492,13 +510,18 @@ declare class PackageJsonNotFoundError extends Error {
492
510
  readonly directory: string;
493
511
  constructor(directory: string, options?: ErrorOptions);
494
512
  }
513
+ declare class NotADirectoryError extends Error {
514
+ readonly name = "NotADirectoryError";
515
+ readonly resolvedPath: string;
516
+ constructor(resolvedPath: string, options?: ErrorOptions);
517
+ }
495
518
  declare class AmbiguousProjectError extends Error {
496
519
  readonly name = "AmbiguousProjectError";
497
520
  readonly directory: string;
498
521
  readonly candidates: readonly string[];
499
522
  constructor(directory: string, candidates: readonly string[], options?: ErrorOptions);
500
523
  }
501
- declare const isProjectDiscoveryError: (value: unknown) => value is ProjectNotFoundError | NoReactDependencyError | PackageJsonNotFoundError | AmbiguousProjectError; //#endregion
524
+ declare const isProjectDiscoveryError: (value: unknown) => value is ProjectNotFoundError | NoReactDependencyError | PackageJsonNotFoundError | NotADirectoryError | AmbiguousProjectError; //#endregion
502
525
  //#region src/project-info/utils/is-directory.d.ts
503
526
  //#endregion
504
527
  //#region src/errors.d.ts
@@ -651,7 +674,41 @@ declare const summarizeDiagnostics: (diagnostics: Diagnostic[], worstScore?: num
651
674
  //#endregion
652
675
  //#region ../api/dist/index.d.ts
653
676
  //#region src/diagnose.d.ts
654
- declare const diagnose: (directory: string, options?: DiagnoseOptions) => Promise<DiagnoseResult>; //#endregion
677
+ declare const diagnose: (directory: string, options?: DiagnoseOptions) => Promise<DiagnoseResult>;
678
+ /**
679
+ * Scan multiple projects in parallel and return per-project scores,
680
+ * diagnostics, and an aggregate score (worst-of across all projects).
681
+ *
682
+ * Each project runs its own independent `runInspect` pipeline — the
683
+ * same pipeline `diagnose()` uses — so per-project config overrides,
684
+ * dead-code analysis, and scoring all work identically to a single
685
+ * `diagnose()` call.
686
+ *
687
+ * Projects that fail (e.g. missing `package.json`, no React dependency)
688
+ * are included in the result with `ok: false` rather than aborting the
689
+ * entire batch, so callers always receive partial results.
690
+ *
691
+ * ```ts
692
+ * const result = await diagnoseProjects({
693
+ * projects: [
694
+ * { directory: "packages/app" },
695
+ * { directory: "packages/shared", deadCode: false },
696
+ * { directory: "packages/admin", config: {
697
+ * rules: { "react-doctor/no-array-index-as-key": "off" },
698
+ * }},
699
+ * ],
700
+ * concurrency: 4,
701
+ * });
702
+ *
703
+ * for (const project of result.projects) {
704
+ * if (project.ok) {
705
+ * console.log(project.directory, project.score);
706
+ * } else {
707
+ * console.error(project.directory, project.error);
708
+ * }
709
+ * }
710
+ * ```
711
+ */
655
712
  //#endregion
656
713
  //#region src/index.d.ts
657
714
  declare const clearCaches: () => void;
@@ -662,5 +719,5 @@ interface ToJsonReportOptions {
662
719
  }
663
720
  declare const toJsonReport: (result: DiagnoseResult, options: ToJsonReportOptions) => JsonReport;
664
721
  //#endregion
665
- export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, PackageJsonNotFoundError, type ProjectInfo, ProjectNotFoundError, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
722
+ export { AmbiguousProjectError, type DiagnoseOptions, type DiagnoseResult, type Diagnostic, type DiffInfo, type JsonReport, type JsonReportDiffInfo, type JsonReportError, type JsonReportMode, type JsonReportProjectEntry, type JsonReportSummary, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, type ProjectInfo, ProjectNotFoundError, type ReactDoctorConfig, ReactDoctorError, type ScoreResult, buildJsonReport, buildJsonReportError, clearCaches, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
666
723
  //# sourceMappingURL=index.d.ts.map