react-doctor 0.2.5 → 0.2.6

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.
@@ -4,7 +4,7 @@ import { spawn, spawnSync } from "node:child_process";
4
4
  import * as Path from "node:path";
5
5
  import path from "node:path";
6
6
  import * as fs$1 from "node:fs";
7
- import fs, { existsSync, readdirSync } from "node:fs";
7
+ import fs, { existsSync, readFileSync, readdirSync } from "node:fs";
8
8
  import * as Schema from "effect/Schema";
9
9
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES } from "oxlint-plugin-react-doctor";
10
10
  import * as Cause from "effect/Cause";
@@ -2960,6 +2960,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
2960
2960
  const SCORE_API_URL = "https://www.react.doctor/api/score";
2961
2961
  const SHARE_BASE_URL = "https://www.react.doctor/share";
2962
2962
  const FETCH_TIMEOUT_MS = 1e4;
2963
+ const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
2963
2964
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
2964
2965
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
2965
2966
  const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
@@ -4427,6 +4428,20 @@ const parseGithubRepoFromRemoteUrl = (remoteUrl) => {
4427
4428
  const urlMatch = /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+)\/([^/\s]+)$/.exec(withoutGitSuffix);
4428
4429
  return urlMatch ? `${urlMatch[1]}/${urlMatch[2]}` : null;
4429
4430
  };
4431
+ const parseGithubRepo = (repo) => {
4432
+ const [owner, name, ...extraParts] = repo.split("/");
4433
+ if (owner === void 0 || name === void 0 || extraParts.length > 0) return null;
4434
+ if (owner.length === 0 || name.length === 0) return null;
4435
+ return {
4436
+ owner,
4437
+ name
4438
+ };
4439
+ };
4440
+ const parseGithubViewerPermission = (stdout) => {
4441
+ const value = trimOrNull(stdout);
4442
+ if (value === null || value === "null") return null;
4443
+ return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
4444
+ };
4430
4445
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
4431
4446
  /**
4432
4447
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -4452,9 +4467,10 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4452
4467
  * reason: GitInvocationFailed })` so the rest of the codebase
4453
4468
  * sees a single failure channel.
4454
4469
  */
4455
- const runGit = (directory, args) => Effect.scoped(Effect.gen(function* () {
4456
- const handle = yield* spawner.spawn(ChildProcess.make("git", [...args], {
4457
- cwd: directory,
4470
+ const runCommand = (input) => Effect.scoped(Effect.gen(function* () {
4471
+ const handle = yield* spawner.spawn(ChildProcess.make(input.command, [...input.args], {
4472
+ cwd: input.directory,
4473
+ env: input.env,
4458
4474
  extendEnv: true
4459
4475
  }));
4460
4476
  const [stdout, stderr, status] = yield* Effect.all([
@@ -4467,11 +4483,23 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4467
4483
  stdout,
4468
4484
  stderr
4469
4485
  };
4470
- })).pipe(Effect.catchTag("PlatformError", (cause) => new ReactDoctorError({ reason: new GitInvocationFailed({
4471
- args: [...args],
4472
- directory,
4473
- cause
4474
- }) })));
4486
+ })).pipe(Effect.catchTag("PlatformError", (cause) => {
4487
+ if (input.command !== "git") return Effect.succeed({
4488
+ status: 127,
4489
+ stdout: "",
4490
+ stderr: String(cause)
4491
+ });
4492
+ return new ReactDoctorError({ reason: new GitInvocationFailed({
4493
+ args: [...input.args],
4494
+ directory: input.directory,
4495
+ cause
4496
+ }) });
4497
+ }));
4498
+ const runGit = (directory, args) => runCommand({
4499
+ command: "git",
4500
+ args,
4501
+ directory
4502
+ });
4475
4503
  const currentBranch = (directory) => runGit(directory, [
4476
4504
  "rev-parse",
4477
4505
  "--abbrev-ref",
@@ -4506,11 +4534,43 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4506
4534
  "--get",
4507
4535
  "remote.origin.url"
4508
4536
  ]).pipe(Effect.map((result) => result.status === 0 ? parseGithubRepoFromRemoteUrl(result.stdout) : null));
4537
+ const githubViewerPermission = (input) => Effect.gen(function* () {
4538
+ const parsedRepo = parseGithubRepo(input.repo);
4539
+ if (parsedRepo === null) return null;
4540
+ const resultOption = yield* runCommand({
4541
+ command: "gh",
4542
+ args: [
4543
+ "api",
4544
+ "graphql",
4545
+ "-F",
4546
+ `owner=${parsedRepo.owner}`,
4547
+ "-F",
4548
+ `name=${parsedRepo.name}`,
4549
+ "-f",
4550
+ `query=
4551
+ query(\$owner: String!, \$name: String!) {
4552
+ repository(owner: \$owner, name: \$name) {
4553
+ viewerPermission
4554
+ }
4555
+ }
4556
+ `,
4557
+ "--jq",
4558
+ ".data.repository.viewerPermission"
4559
+ ],
4560
+ directory: input.directory,
4561
+ env: { GH_PROMPT_DISABLED: "1" }
4562
+ }).pipe(Effect.timeoutOption(GITHUB_VIEWER_PERMISSION_TIMEOUT_MS));
4563
+ if (Option.isNone(resultOption)) return null;
4564
+ const result = resultOption.value;
4565
+ if (result.status !== 0) return null;
4566
+ return parseGithubViewerPermission(result.stdout);
4567
+ }).pipe(Effect.catch(() => Effect.succeed(null)));
4509
4568
  return Git.of({
4510
4569
  currentBranch,
4511
4570
  defaultBranch,
4512
4571
  headSha,
4513
4572
  githubRepo,
4573
+ githubViewerPermission,
4514
4574
  branchExists,
4515
4575
  diffSelection: ({ directory, explicitBaseBranch }) => Effect.gen(function* () {
4516
4576
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* Effect.fail(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
@@ -4605,6 +4665,7 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4605
4665
  defaultBranch: () => Effect.succeed(snapshot.defaultBranch ?? null),
4606
4666
  headSha: () => Effect.succeed(snapshot.headSha ?? null),
4607
4667
  githubRepo: () => Effect.succeed(snapshot.githubRepo ?? null),
4668
+ githubViewerPermission: () => Effect.succeed(snapshot.githubViewerPermission ?? null),
4608
4669
  branchExists: (_directory, branch) => Effect.succeed(snapshot.branchExists?.get(branch) ?? false),
4609
4670
  diffSelection: () => Effect.succeed(snapshot.diffSelection ?? null),
4610
4671
  stagedFilePaths: () => Effect.succeed(snapshot.stagedFiles ?? []),
@@ -5947,7 +6008,11 @@ const calculateScore = async (diagnostics, options = {}) => {
5947
6008
  ...options.metadata?.framework ? { framework: options.metadata.framework } : {},
5948
6009
  ...options.metadata?.reactVersion ? { reactVersion: options.metadata.reactVersion } : {},
5949
6010
  ...typeof options.metadata?.sourceFileCount === "number" ? { sourceFileCount: options.metadata.sourceFileCount } : {},
5950
- ...options.metadata?.defaultBranch ? { defaultBranch: options.metadata.defaultBranch } : {}
6011
+ ...options.metadata?.defaultBranch ? { defaultBranch: options.metadata.defaultBranch } : {},
6012
+ ...options.metadata?.doctorVersion ? { doctorVersion: options.metadata.doctorVersion } : {},
6013
+ ...options.metadata?.githubEventName ? { githubEventName: options.metadata.githubEventName } : {},
6014
+ ...options.metadata?.githubActorAssociation ? { githubActorAssociation: options.metadata.githubActorAssociation } : {},
6015
+ ...options.metadata?.githubViewerPermission ? { githubViewerPermission: options.metadata.githubViewerPermission } : {}
5951
6016
  }));
5952
6017
  const response = await fetch(requestUrl, {
5953
6018
  method: "POST",
@@ -5992,6 +6057,32 @@ var Score = class Score extends Context.Service()("react-doctor/Score") {
5992
6057
  }) }));
5993
6058
  static layerOf = (result) => Layer.succeed(Score, Score.of({ compute: () => Effect.succeed(result) }));
5994
6059
  };
6060
+ const getObjectProperty = (value, propertyName) => {
6061
+ if (typeof value !== "object" || value === null) return void 0;
6062
+ return Reflect.get(value, propertyName);
6063
+ };
6064
+ const getStringProperty = (value, propertyName) => {
6065
+ const propertyValue = getObjectProperty(value, propertyName);
6066
+ return typeof propertyValue === "string" && propertyValue.length > 0 ? propertyValue : void 0;
6067
+ };
6068
+ const readGithubEventPayload = (eventPath) => {
6069
+ if (eventPath === void 0 || eventPath.length === 0) return null;
6070
+ try {
6071
+ return JSON.parse(readFileSync(eventPath, "utf8"));
6072
+ } catch {
6073
+ return null;
6074
+ }
6075
+ };
6076
+ const resolveGithubActionsScoreMetadata = (environment = process.env) => {
6077
+ if (environment.GITHUB_ACTIONS !== "true") return {};
6078
+ const pullRequest = getObjectProperty(readGithubEventPayload(environment.GITHUB_EVENT_PATH), "pull_request");
6079
+ const eventName = environment.GITHUB_EVENT_NAME;
6080
+ const actorAssociation = getStringProperty(pullRequest, "author_association");
6081
+ return {
6082
+ ...eventName !== void 0 && eventName.length > 0 ? { githubEventName: eventName } : {},
6083
+ ...actorAssociation !== void 0 ? { githubActorAssociation: actorAssociation } : {}
6084
+ };
6085
+ };
5995
6086
  const NO_HOOKS = {
5996
6087
  beforeLint: () => Effect.void,
5997
6088
  afterLint: () => Effect.void
@@ -6047,13 +6138,21 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6047
6138
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6048
6139
  gitService.defaultBranch(scanDirectory).pipe(Effect.orElseSucceed(() => null))
6049
6140
  ], { concurrency: 3 });
6141
+ const githubActionsScoreMetadata = input.isCi ? resolveGithubActionsScoreMetadata() : {};
6142
+ const githubViewerPermission = input.resolveLocalGithubViewerPermission === true && !input.isCi && repo !== null ? yield* gitService.githubViewerPermission({
6143
+ directory: scanDirectory,
6144
+ repo
6145
+ }).pipe(Effect.orElseSucceed(() => null)) : null;
6050
6146
  const scoreMetadata = {
6051
6147
  ...repo !== null ? { repo } : {},
6052
6148
  ...sha !== null ? { sha } : {},
6053
6149
  framework: project.framework,
6054
6150
  ...project.reactVersion !== null ? { reactVersion: project.reactVersion } : {},
6055
6151
  sourceFileCount: project.sourceFileCount,
6056
- ...defaultBranch !== null ? { defaultBranch } : {}
6152
+ ...defaultBranch !== null ? { defaultBranch } : {},
6153
+ ...input.doctorVersion !== void 0 ? { doctorVersion: input.doctorVersion } : {},
6154
+ ...githubActionsScoreMetadata,
6155
+ ...githubViewerPermission !== null ? { githubViewerPermission } : {}
6057
6156
  };
6058
6157
  const lintIncludePaths = computeJsxIncludePaths([...input.includePaths]) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
6059
6158
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
@@ -6676,4 +6775,4 @@ const cliLogger = {
6676
6775
  //#endregion
6677
6776
  export { formatReactDoctorError as A, toRelativePath as B, buildJsonReportError as C, filterSourceFiles as D, filterDiagnosticsForSurface as E, layerOtlp as F, listWorkspacePackages as I, loadConfigWithSource as L, groupBy as M, highlighter as N, formatErrorChain as O, isReactDoctorError as P, resolveConfigRootDir as R, buildJsonReport as S, discoverReactSubprojects as T, Reporter as _, Config as a, Score as b, Git as c, MILLISECONDS_PER_SECOND as d, NoReactDependencyError as f, ProjectNotFoundError as g, Project as h, CANONICAL_GITHUB_URL as i, getDiffInfo as j, formatFrameworkName as k, LintPartialFailures as l, OXLINT_NODE_REQUIREMENT as m, cli_logger_exports as n, DeadCode as o, NodeResolver as p, AmbiguousProjectError as r, Files as s, cliLogger as t, Linter as u, SHARE_BASE_URL as v, calculateScore as w, StagedFiles as x, SKILL_NAME as y, runInspect as z };
6678
6777
 
6679
- //# sourceMappingURL=cli-logger-CISyjOAb.js.map
6778
+ //# sourceMappingURL=cli-logger-Iz5pfDnL.js.map
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 formatReactDoctorError, B as toRelativePath, C as buildJsonReportError, D as filterSourceFiles, E as filterDiagnosticsForSurface, F as layerOtlp, I as listWorkspacePackages, L as loadConfigWithSource, M as groupBy, N as highlighter, O as formatErrorChain, P as isReactDoctorError, R as resolveConfigRootDir, S as buildJsonReport, T as discoverReactSubprojects, _ as Reporter, a as Config, b as Score, c as Git, d as MILLISECONDS_PER_SECOND, f as NoReactDependencyError, g as ProjectNotFoundError, h as Project, i as CANONICAL_GITHUB_URL, j as getDiffInfo, k as formatFrameworkName, l as LintPartialFailures, m as OXLINT_NODE_REQUIREMENT, o as DeadCode, p as NodeResolver, r as AmbiguousProjectError, s as Files, t as cliLogger, u as Linter, v as SHARE_BASE_URL, w as calculateScore, x as StagedFiles, y as SKILL_NAME, z as runInspect } from "./cli-logger-CISyjOAb.js";
2
+ import { A as formatReactDoctorError, B as toRelativePath, C as buildJsonReportError, D as filterSourceFiles, E as filterDiagnosticsForSurface, F as layerOtlp, I as listWorkspacePackages, L as loadConfigWithSource, M as groupBy, N as highlighter, O as formatErrorChain, P as isReactDoctorError, R as resolveConfigRootDir, S as buildJsonReport, T as discoverReactSubprojects, _ as Reporter, a as Config, b as Score, c as Git, d as MILLISECONDS_PER_SECOND, f as NoReactDependencyError, g as ProjectNotFoundError, h as Project, i as CANONICAL_GITHUB_URL, j as getDiffInfo, k as formatFrameworkName, l as LintPartialFailures, m as OXLINT_NODE_REQUIREMENT, o as DeadCode, p as NodeResolver, r as AmbiguousProjectError, s as Files, t as cliLogger, u as Linter, v as SHARE_BASE_URL, w as calculateScore, x as StagedFiles, y as SKILL_NAME, z as runInspect } from "./cli-logger-Iz5pfDnL.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import path, { join } from "node:path";
@@ -6545,6 +6545,9 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
6545
6545
  });
6546
6546
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
6547
6547
  //#endregion
6548
+ //#region src/cli/utils/version.ts
6549
+ const VERSION = "0.2.6";
6550
+ //#endregion
6548
6551
  //#region src/inspect.ts
6549
6552
  const silentConsole = new Proxy({}, { get: () => () => void 0 });
6550
6553
  const runConsole = (effect) => {
@@ -6636,7 +6639,9 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
6636
6639
  ignoredTags: options.ignoredTags,
6637
6640
  nodeBinaryPath: resolvedNodeBinaryPath ?? void 0,
6638
6641
  runDeadCode: options.deadCode,
6639
- isCi: options.isCi
6642
+ isCi: options.isCi,
6643
+ doctorVersion: VERSION,
6644
+ resolveLocalGithubViewerPermission: !options.noScore
6640
6645
  }, {
6641
6646
  beforeLint: (projectInfo, lintIncludePaths) => Effect.gen(function* () {
6642
6647
  const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
@@ -6825,9 +6830,6 @@ const handleError = (error, options = { shouldExit: true }) => {
6825
6830
  process.exitCode = 1;
6826
6831
  };
6827
6832
  //#endregion
6828
- //#region src/cli/utils/version.ts
6829
- const VERSION = "0.2.5";
6830
- //#endregion
6831
6833
  //#region src/cli/utils/json-mode.ts
6832
6834
  let context = null;
6833
6835
  /**
@@ -7124,7 +7126,7 @@ const warnSetupPromptFailure = async (options, error) => {
7124
7126
  return;
7125
7127
  }
7126
7128
  try {
7127
- const { cliLogger } = await import("./cli-logger-CISyjOAb.js").then((n) => n.n);
7129
+ const { cliLogger } = await import("./cli-logger-Iz5pfDnL.js").then((n) => n.n);
7128
7130
  cliLogger.warn(message);
7129
7131
  } catch {}
7130
7132
  };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import * as Schema from "effect/Schema";
3
3
  import * as fs$1 from "node:fs";
4
- import fs, { existsSync, readdirSync } from "node:fs";
4
+ import fs, { existsSync, readFileSync, readdirSync } from "node:fs";
5
5
  import * as Path from "node:path";
6
6
  import path from "node:path";
7
7
  import { spawn, spawnSync } from "node:child_process";
@@ -2987,6 +2987,7 @@ const isTailwindAtLeast = (detected, required) => {
2987
2987
  const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
2988
2988
  const SCORE_API_URL = "https://www.react.doctor/api/score";
2989
2989
  const FETCH_TIMEOUT_MS = 1e4;
2990
+ const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
2990
2991
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
2991
2992
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
2992
2993
  const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -4441,6 +4442,20 @@ const parseGithubRepoFromRemoteUrl = (remoteUrl) => {
4441
4442
  const urlMatch = /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+)\/([^/\s]+)$/.exec(withoutGitSuffix);
4442
4443
  return urlMatch ? `${urlMatch[1]}/${urlMatch[2]}` : null;
4443
4444
  };
4445
+ const parseGithubRepo = (repo) => {
4446
+ const [owner, name, ...extraParts] = repo.split("/");
4447
+ if (owner === void 0 || name === void 0 || extraParts.length > 0) return null;
4448
+ if (owner.length === 0 || name.length === 0) return null;
4449
+ return {
4450
+ owner,
4451
+ name
4452
+ };
4453
+ };
4454
+ const parseGithubViewerPermission = (stdout) => {
4455
+ const value = trimOrNull(stdout);
4456
+ if (value === null || value === "null") return null;
4457
+ return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
4458
+ };
4444
4459
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
4445
4460
  /**
4446
4461
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
@@ -4466,9 +4481,10 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4466
4481
  * reason: GitInvocationFailed })` so the rest of the codebase
4467
4482
  * sees a single failure channel.
4468
4483
  */
4469
- const runGit = (directory, args) => Effect.scoped(Effect.gen(function* () {
4470
- const handle = yield* spawner.spawn(ChildProcess.make("git", [...args], {
4471
- cwd: directory,
4484
+ const runCommand = (input) => Effect.scoped(Effect.gen(function* () {
4485
+ const handle = yield* spawner.spawn(ChildProcess.make(input.command, [...input.args], {
4486
+ cwd: input.directory,
4487
+ env: input.env,
4472
4488
  extendEnv: true
4473
4489
  }));
4474
4490
  const [stdout, stderr, status] = yield* Effect.all([
@@ -4481,11 +4497,23 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4481
4497
  stdout,
4482
4498
  stderr
4483
4499
  };
4484
- })).pipe(Effect.catchTag("PlatformError", (cause) => new ReactDoctorError({ reason: new GitInvocationFailed({
4485
- args: [...args],
4486
- directory,
4487
- cause
4488
- }) })));
4500
+ })).pipe(Effect.catchTag("PlatformError", (cause) => {
4501
+ if (input.command !== "git") return Effect.succeed({
4502
+ status: 127,
4503
+ stdout: "",
4504
+ stderr: String(cause)
4505
+ });
4506
+ return new ReactDoctorError({ reason: new GitInvocationFailed({
4507
+ args: [...input.args],
4508
+ directory: input.directory,
4509
+ cause
4510
+ }) });
4511
+ }));
4512
+ const runGit = (directory, args) => runCommand({
4513
+ command: "git",
4514
+ args,
4515
+ directory
4516
+ });
4489
4517
  const currentBranch = (directory) => runGit(directory, [
4490
4518
  "rev-parse",
4491
4519
  "--abbrev-ref",
@@ -4520,11 +4548,43 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4520
4548
  "--get",
4521
4549
  "remote.origin.url"
4522
4550
  ]).pipe(Effect.map((result) => result.status === 0 ? parseGithubRepoFromRemoteUrl(result.stdout) : null));
4551
+ const githubViewerPermission = (input) => Effect.gen(function* () {
4552
+ const parsedRepo = parseGithubRepo(input.repo);
4553
+ if (parsedRepo === null) return null;
4554
+ const resultOption = yield* runCommand({
4555
+ command: "gh",
4556
+ args: [
4557
+ "api",
4558
+ "graphql",
4559
+ "-F",
4560
+ `owner=${parsedRepo.owner}`,
4561
+ "-F",
4562
+ `name=${parsedRepo.name}`,
4563
+ "-f",
4564
+ `query=
4565
+ query(\$owner: String!, \$name: String!) {
4566
+ repository(owner: \$owner, name: \$name) {
4567
+ viewerPermission
4568
+ }
4569
+ }
4570
+ `,
4571
+ "--jq",
4572
+ ".data.repository.viewerPermission"
4573
+ ],
4574
+ directory: input.directory,
4575
+ env: { GH_PROMPT_DISABLED: "1" }
4576
+ }).pipe(Effect.timeoutOption(GITHUB_VIEWER_PERMISSION_TIMEOUT_MS));
4577
+ if (Option.isNone(resultOption)) return null;
4578
+ const result = resultOption.value;
4579
+ if (result.status !== 0) return null;
4580
+ return parseGithubViewerPermission(result.stdout);
4581
+ }).pipe(Effect.catch(() => Effect.succeed(null)));
4523
4582
  return Git.of({
4524
4583
  currentBranch,
4525
4584
  defaultBranch,
4526
4585
  headSha,
4527
4586
  githubRepo,
4587
+ githubViewerPermission,
4528
4588
  branchExists,
4529
4589
  diffSelection: ({ directory, explicitBaseBranch }) => Effect.gen(function* () {
4530
4590
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* Effect.fail(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
@@ -4619,6 +4679,7 @@ var Git = class Git extends Context.Service()("react-doctor/Git") {
4619
4679
  defaultBranch: () => Effect.succeed(snapshot.defaultBranch ?? null),
4620
4680
  headSha: () => Effect.succeed(snapshot.headSha ?? null),
4621
4681
  githubRepo: () => Effect.succeed(snapshot.githubRepo ?? null),
4682
+ githubViewerPermission: () => Effect.succeed(snapshot.githubViewerPermission ?? null),
4622
4683
  branchExists: (_directory, branch) => Effect.succeed(snapshot.branchExists?.get(branch) ?? false),
4623
4684
  diffSelection: () => Effect.succeed(snapshot.diffSelection ?? null),
4624
4685
  stagedFilePaths: () => Effect.succeed(snapshot.stagedFiles ?? []),
@@ -5961,7 +6022,11 @@ const calculateScore = async (diagnostics, options = {}) => {
5961
6022
  ...options.metadata?.framework ? { framework: options.metadata.framework } : {},
5962
6023
  ...options.metadata?.reactVersion ? { reactVersion: options.metadata.reactVersion } : {},
5963
6024
  ...typeof options.metadata?.sourceFileCount === "number" ? { sourceFileCount: options.metadata.sourceFileCount } : {},
5964
- ...options.metadata?.defaultBranch ? { defaultBranch: options.metadata.defaultBranch } : {}
6025
+ ...options.metadata?.defaultBranch ? { defaultBranch: options.metadata.defaultBranch } : {},
6026
+ ...options.metadata?.doctorVersion ? { doctorVersion: options.metadata.doctorVersion } : {},
6027
+ ...options.metadata?.githubEventName ? { githubEventName: options.metadata.githubEventName } : {},
6028
+ ...options.metadata?.githubActorAssociation ? { githubActorAssociation: options.metadata.githubActorAssociation } : {},
6029
+ ...options.metadata?.githubViewerPermission ? { githubViewerPermission: options.metadata.githubViewerPermission } : {}
5965
6030
  }));
5966
6031
  const response = await fetch(requestUrl, {
5967
6032
  method: "POST",
@@ -6006,6 +6071,32 @@ var Score = class Score extends Context.Service()("react-doctor/Score") {
6006
6071
  }) }));
6007
6072
  static layerOf = (result) => Layer.succeed(Score, Score.of({ compute: () => Effect.succeed(result) }));
6008
6073
  };
6074
+ const getObjectProperty = (value, propertyName) => {
6075
+ if (typeof value !== "object" || value === null) return void 0;
6076
+ return Reflect.get(value, propertyName);
6077
+ };
6078
+ const getStringProperty = (value, propertyName) => {
6079
+ const propertyValue = getObjectProperty(value, propertyName);
6080
+ return typeof propertyValue === "string" && propertyValue.length > 0 ? propertyValue : void 0;
6081
+ };
6082
+ const readGithubEventPayload = (eventPath) => {
6083
+ if (eventPath === void 0 || eventPath.length === 0) return null;
6084
+ try {
6085
+ return JSON.parse(readFileSync(eventPath, "utf8"));
6086
+ } catch {
6087
+ return null;
6088
+ }
6089
+ };
6090
+ const resolveGithubActionsScoreMetadata = (environment = process.env) => {
6091
+ if (environment.GITHUB_ACTIONS !== "true") return {};
6092
+ const pullRequest = getObjectProperty(readGithubEventPayload(environment.GITHUB_EVENT_PATH), "pull_request");
6093
+ const eventName = environment.GITHUB_EVENT_NAME;
6094
+ const actorAssociation = getStringProperty(pullRequest, "author_association");
6095
+ return {
6096
+ ...eventName !== void 0 && eventName.length > 0 ? { githubEventName: eventName } : {},
6097
+ ...actorAssociation !== void 0 ? { githubActorAssociation: actorAssociation } : {}
6098
+ };
6099
+ };
6009
6100
  const NO_HOOKS = {
6010
6101
  beforeLint: () => Effect.void,
6011
6102
  afterLint: () => Effect.void
@@ -6061,13 +6152,21 @@ const runInspect = (input, hooks = {}) => Effect.gen(function* () {
6061
6152
  gitService.headSha(scanDirectory).pipe(Effect.orElseSucceed(() => null)),
6062
6153
  gitService.defaultBranch(scanDirectory).pipe(Effect.orElseSucceed(() => null))
6063
6154
  ], { concurrency: 3 });
6155
+ const githubActionsScoreMetadata = input.isCi ? resolveGithubActionsScoreMetadata() : {};
6156
+ const githubViewerPermission = input.resolveLocalGithubViewerPermission === true && !input.isCi && repo !== null ? yield* gitService.githubViewerPermission({
6157
+ directory: scanDirectory,
6158
+ repo
6159
+ }).pipe(Effect.orElseSucceed(() => null)) : null;
6064
6160
  const scoreMetadata = {
6065
6161
  ...repo !== null ? { repo } : {},
6066
6162
  ...sha !== null ? { sha } : {},
6067
6163
  framework: project.framework,
6068
6164
  ...project.reactVersion !== null ? { reactVersion: project.reactVersion } : {},
6069
6165
  sourceFileCount: project.sourceFileCount,
6070
- ...defaultBranch !== null ? { defaultBranch } : {}
6166
+ ...defaultBranch !== null ? { defaultBranch } : {},
6167
+ ...input.doctorVersion !== void 0 ? { doctorVersion: input.doctorVersion } : {},
6168
+ ...githubActionsScoreMetadata,
6169
+ ...githubViewerPermission !== null ? { githubViewerPermission } : {}
6071
6170
  };
6072
6171
  const lintIncludePaths = computeJsxIncludePaths([...input.includePaths]) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
6073
6172
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
@@ -6617,7 +6716,8 @@ const diagnose = async (directory, options = {}) => {
6617
6716
  adoptExistingLintConfig: initialLoadedConfig?.config?.adoptExistingLintConfig ?? true,
6618
6717
  ignoredTags: new Set(initialLoadedConfig?.config?.ignore?.tags ?? []),
6619
6718
  runDeadCode: options.deadCode ?? initialLoadedConfig?.config?.deadCode ?? true,
6620
- isCi: false
6719
+ isCi: false,
6720
+ resolveLocalGithubViewerPermission: true
6621
6721
  });
6622
6722
  const output = await Effect.runPromise(program.pipe(Effect.provide(buildLayerStack()), Effect.provide(layerOtlp), Effect.catchReasons("ReactDoctorError", {
6623
6723
  NoReactDependency: (reason) => Effect.die(new NoReactDependencyError(reason.directory)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -57,15 +57,15 @@
57
57
  "oxlint": "^1.66.0",
58
58
  "prompts": "^2.4.2",
59
59
  "typescript": ">=5.0.4 <7",
60
- "oxlint-plugin-react-doctor": "0.2.5"
60
+ "oxlint-plugin-react-doctor": "0.2.6"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/prompts": "^2.4.9",
64
64
  "commander": "^14.0.3",
65
65
  "eslint-plugin-react-hooks": "^7.1.1",
66
66
  "ora": "^9.4.0",
67
- "@react-doctor/api": "0.2.5",
68
- "@react-doctor/core": "0.2.5"
67
+ "@react-doctor/api": "0.2.6",
68
+ "@react-doctor/core": "0.2.6"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "eslint-plugin-react-hooks": "^6 || ^7"