react-doctor 0.2.9 → 0.2.11

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-BliQX9s8.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-pbFEieEc.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,11 +6320,11 @@ 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;
6326
- const PERFECT_SCORE_RAINBOW_FRAME_COUNT = 16;
6327
- const PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS = 50;
6328
6328
  const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
6329
6329
  const RAINBOW_GRADIENT_WIDTH = 80;
6330
6330
  const RAINBOW_OKLCH_LIGHTNESS = .638;
@@ -6433,8 +6433,8 @@ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex,
6433
6433
  };
6434
6434
  const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
6435
6435
  const isPerfectScore = score === 100;
6436
- for (let frame = 0; frame <= SCORE_BAR_ANIMATION_FRAME_COUNT; frame += 1) {
6437
- const progress = easeOutCubic(frame / SCORE_BAR_ANIMATION_FRAME_COUNT);
6436
+ for (let frame = 0; frame <= 40; frame += 1) {
6437
+ const progress = easeOutCubic(frame / 40);
6438
6438
  const animatedScore = Math.round(score * progress);
6439
6439
  if (isPerfectScore) {
6440
6440
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
@@ -6444,16 +6444,16 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
6444
6444
  frame,
6445
6445
  projectName
6446
6446
  })}`);
6447
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6447
+ if (frame < 40) yield* sleep(50);
6448
6448
  continue;
6449
6449
  }
6450
6450
  const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
6451
6451
  const animatedBarLine = buildScoreBar(animatedScore, score);
6452
6452
  yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
6453
- if (frame < SCORE_BAR_ANIMATION_FRAME_COUNT) yield* sleep(SCORE_BAR_ANIMATION_FRAME_DELAY_MS);
6453
+ if (frame < 40) yield* sleep(50);
6454
6454
  }
6455
6455
  if (!isPerfectScore) return;
6456
- for (let frame = 0; frame < PERFECT_SCORE_RAINBOW_FRAME_COUNT; frame += 1) {
6456
+ for (let frame = 0; frame < 16; frame += 1) {
6457
6457
  yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
6458
6458
  score,
6459
6459
  displayScore: score,
@@ -6461,9 +6461,9 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
6461
6461
  frame,
6462
6462
  projectName
6463
6463
  })}`);
6464
- yield* sleep(PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS);
6464
+ yield* sleep(50);
6465
6465
  }
6466
- yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, PERFECT_SCORE_RAINBOW_FRAME_COUNT, projectName)}\x1b[2A`);
6466
+ yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
6467
6467
  });
6468
6468
  const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
6469
6469
  const isPerfectScore = scoreResult.score === 100;
@@ -6666,7 +6666,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6666
6666
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6667
6667
  //#endregion
6668
6668
  //#region src/cli/utils/version.ts
6669
- const VERSION = "0.2.9";
6669
+ const VERSION = "0.2.11";
6670
6670
  //#endregion
6671
6671
  //#region src/inspect.ts
6672
6672
  const silentConsole = makeNoopConsole();
@@ -6854,10 +6854,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
6854
6854
  return buildResult();
6855
6855
  });
6856
6856
  //#endregion
6857
- //#region src/cli/utils/constants.ts
6858
- const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
6859
- const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
6860
- //#endregion
6861
6857
  //#region src/cli/utils/get-staged-files.ts
6862
6858
  const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
6863
6859
  const getStagedSourceFiles = async (directory) => {
@@ -7416,7 +7412,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
7416
7412
  return true;
7417
7413
  };
7418
7414
  const shouldPromptInstallSetup = (options) => {
7419
- if (!options.hasScoredScan) return false;
7415
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7420
7416
  if (options.isJsonMode) return false;
7421
7417
  if (options.isScoreOnly) return false;
7422
7418
  if (options.isStaged) return false;
@@ -7426,13 +7422,13 @@ const shouldPromptInstallSetup = (options) => {
7426
7422
  return !hasDoctorScript(options.projectRoot);
7427
7423
  };
7428
7424
  const resolveInstallSetupProjectRoot = (options) => {
7429
- if (options.completedScanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7425
+ if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
7430
7426
  const packageDirectories = /* @__PURE__ */ new Set();
7431
- for (const scanDirectory of options.completedScanDirectories) {
7427
+ for (const scanDirectory of options.scanDirectories) {
7432
7428
  const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
7433
7429
  packageDirectories.add(packageDirectory);
7434
7430
  }
7435
- if (packageDirectories.size !== 1) return null;
7431
+ if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
7436
7432
  return [...packageDirectories][0] ?? null;
7437
7433
  };
7438
7434
  const defaultWait = (milliseconds) => new Promise((resolve) => {
@@ -7467,7 +7463,7 @@ const warnSetupPromptFailure = async (options, error) => {
7467
7463
  return;
7468
7464
  }
7469
7465
  try {
7470
- const { cliLogger } = await import("./cli-logger-BliQX9s8.js").then((n) => n.n);
7466
+ const { cliLogger } = await import("./cli-logger-pbFEieEc.js").then((n) => n.n);
7471
7467
  cliLogger.warn(message);
7472
7468
  } catch {}
7473
7469
  };
@@ -7483,7 +7479,7 @@ const promptInstallSetup = async (options) => {
7483
7479
  writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
7484
7480
  return;
7485
7481
  }
7486
- 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;
7487
7483
  const previousExitCode = process.exitCode;
7488
7484
  let setupExitCode;
7489
7485
  try {
@@ -7502,7 +7498,7 @@ const promptInstallSetup = async (options) => {
7502
7498
  }
7503
7499
  };
7504
7500
  const shouldShowAgentInstallHint = (options) => {
7505
- if (!options.hasScoredScan) return false;
7501
+ if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
7506
7502
  if (options.isJsonMode) return false;
7507
7503
  if (options.isScoreOnly) return false;
7508
7504
  if (options.isStaged) return false;
@@ -7567,7 +7563,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
7567
7563
  const { scanScope } = await prompts({
7568
7564
  type: "select",
7569
7565
  name: "scanScope",
7570
- message: "Select",
7566
+ message: "Choose what to scan",
7571
7567
  choices: [{
7572
7568
  title: "Full codebase",
7573
7569
  value: "full"
@@ -7709,7 +7705,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
7709
7705
  }
7710
7706
  if (packages.length === 0) return [rootDirectory];
7711
7707
  if (packages.length === 1) {
7712
- 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}`);
7713
7709
  return [packages[0].directory];
7714
7710
  }
7715
7711
  if (projectFlag) return resolveProjectFlag(projectFlag, packages);
@@ -7733,13 +7729,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
7733
7729
  return resolvedDirectories;
7734
7730
  };
7735
7731
  const printDiscoveredProjects = (packages) => {
7736
- 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(", ")}`);
7737
7733
  };
7738
7734
  const promptProjectSelection = async (workspacePackages, rootDirectory) => {
7739
7735
  const { selectedDirectories } = await prompts({
7740
7736
  type: "multiselect",
7741
7737
  name: "selectedDirectories",
7742
- message: "Select projects to scan",
7738
+ message: "Select projects",
7743
7739
  choices: workspacePackages.map((workspacePackage) => ({
7744
7740
  title: workspacePackage.name,
7745
7741
  description: path.relative(rootDirectory, workspacePackage.directory),
@@ -7996,13 +7992,13 @@ const inspectAction = async (directory, flags) => {
7996
7992
  });
7997
7993
  const setupProjectRoot = resolveInstallSetupProjectRoot({
7998
7994
  scanRoot: resolvedDirectory,
7999
- completedScanDirectories: completedScans.map((scan) => scan.directory)
7995
+ scanDirectories: projectDirectories
8000
7996
  });
8001
7997
  if (setupProjectRoot !== null) {
8002
- const hasScoredScan = completedScans.some((scan) => scan.result.score !== null);
7998
+ const hasCompletedScan = completedScans.length > 0;
8003
7999
  await promptInstallSetup({
8004
8000
  projectRoot: setupProjectRoot,
8005
- hasScoredScan,
8001
+ hasCompletedScan,
8006
8002
  issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
8007
8003
  isJsonMode,
8008
8004
  isScoreOnly,
@@ -8011,7 +8007,7 @@ const inspectAction = async (directory, flags) => {
8011
8007
  });
8012
8008
  if (shouldShowAgentInstallHint({
8013
8009
  projectRoot: setupProjectRoot,
8014
- hasScoredScan,
8010
+ hasCompletedScan,
8015
8011
  isJsonMode,
8016
8012
  isScoreOnly,
8017
8013
  isStaged: Boolean(flags.staged)
@@ -8643,8 +8639,12 @@ const installReactDoctorGitHook = (options) => {
8643
8639
  return installDirectGitHook(options);
8644
8640
  };
8645
8641
  //#endregion
8646
- //#region src/cli/utils/install-skill.ts
8647
- 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";
8648
8648
  const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
8649
8649
  "ghooks",
8650
8650
  "git-hooks-js",
@@ -8825,7 +8825,30 @@ const getSkillSourceDirectory = () => {
8825
8825
  const distDirectory = path.dirname(fileURLToPath(import.meta.url));
8826
8826
  return path.join(distDirectory, "skills", SKILL_NAME);
8827
8827
  };
8828
- 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 = {}) => {
8829
8852
  const requestedProjectRoot = options.projectRoot ?? process.cwd();
8830
8853
  const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
8831
8854
  const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
@@ -8846,7 +8869,8 @@ const runInstallSkill = async (options = {}) => {
8846
8869
  const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
8847
8870
  const gitHookPath = gitHookTarget?.hookPath;
8848
8871
  const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
8849
- const selectedAgents = skipPrompts ? detectedAgents : (await prompts({
8872
+ const prompt = options.prompt ?? prompts;
8873
+ const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
8850
8874
  type: "multiselect",
8851
8875
  name: "agents",
8852
8876
  message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
@@ -8859,13 +8883,48 @@ const runInstallSkill = async (options = {}) => {
8859
8883
  min: 1
8860
8884
  }, promptOptions)).agents ?? [];
8861
8885
  if (selectedAgents.length === 0) return;
8862
- const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !skipPrompts && Boolean((await prompts({
8863
- type: "confirm",
8864
- name: "installGitHook",
8865
- message: "Check for issues before each commit?",
8866
- initial: true
8867
- }, promptOptions)).installGitHook));
8868
- 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);
8869
8928
  if (options.dryRun) {
8870
8929
  cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
8871
8930
  for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
@@ -8874,6 +8933,7 @@ const runInstallSkill = async (options = {}) => {
8874
8933
  cliLogger.dim(" Dev dependency: react-doctor");
8875
8934
  if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
8876
8935
  if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
8936
+ if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
8877
8937
  return;
8878
8938
  }
8879
8939
  const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
@@ -8921,47 +8981,15 @@ const runInstallSkill = async (options = {}) => {
8921
8981
  throw error;
8922
8982
  }
8923
8983
  }
8924
- const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
8925
- const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
8926
- if (!existsSync(workflowTargetPath) && !skipPrompts) {
8927
- const hasExistingWorkflows = existsSync(workflowsDirectory);
8928
- const { shouldInstallWorkflow } = await prompts({
8929
- type: "confirm",
8930
- name: "shouldInstallWorkflow",
8931
- message: "Add a GitHub Actions workflow to scan PRs?",
8932
- initial: hasExistingWorkflows
8933
- }, promptOptions);
8934
- if (shouldInstallWorkflow) {
8935
- if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
8936
- const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
8937
- try {
8938
- writeFileSync(workflowTargetPath, [
8939
- "name: React Doctor",
8940
- "",
8941
- "on:",
8942
- " pull_request:",
8943
- " branches: [main]",
8944
- "",
8945
- "permissions:",
8946
- " contents: read",
8947
- " pull-requests: write",
8948
- "",
8949
- "jobs:",
8950
- " react-doctor:",
8951
- " runs-on: ubuntu-latest",
8952
- " steps:",
8953
- " - uses: actions/checkout@v4",
8954
- " - uses: millionco/react-doctor@main",
8955
- " with:",
8956
- " github-token: ${{ secrets.GITHUB_TOKEN }}",
8957
- " diff: main",
8958
- ""
8959
- ].join("\n"));
8960
- workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
8961
- } catch (error) {
8962
- workflowSpinner.fail("Failed to add GitHub Actions workflow.");
8963
- throw error;
8964
- }
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;
8965
8993
  }
8966
8994
  }
8967
8995
  };
@@ -8971,7 +8999,7 @@ const installAction = async (options, command) => {
8971
8999
  Effect.runSync(printBrandedHeader);
8972
9000
  try {
8973
9001
  const parentOptions = command?.parent?.opts?.();
8974
- await runInstallSkill({
9002
+ await runInstallReactDoctor({
8975
9003
  yes: options.yes ?? parentOptions?.yes,
8976
9004
  dryRun: options.dryRun,
8977
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`
@@ -321,6 +333,13 @@ interface ProjectInfo {
321
333
  * — no `rn-*` rules load for the project at all.
322
334
  */
323
335
  hasReactNativeWorkspace: boolean;
336
+ /**
337
+ * `true` when the project (or any of its workspace packages) declares
338
+ * `react-native-reanimated`. Lets diagnostics surface reanimated's
339
+ * Compiler-compatible `.get()` / `.set()` accessors only where they
340
+ * apply, instead of on every React Native project.
341
+ */
342
+ hasReanimated: boolean;
324
343
  sourceFileCount: number;
325
344
  }
326
345
  //#endregion