react-doctor 0.5.8-dev.7f9e7f4 → 0.5.8-dev.970babc

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
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0b6893a8-f482-50d5-ad21-45ebd17e98a0")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a201a872-19de-5f85-bec4-8ecc9f0ae2a8")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -26,7 +26,7 @@ import tty from "node:tty";
26
26
  import { codeFrameColumns } from "@babel/code-frame";
27
27
  import Conf from "conf";
28
28
  import basePrompts from "prompts";
29
- import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
29
+ import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource, isSkillAgentType } from "agent-install";
30
30
  import { generateCode, loadFile, writeFile } from "magicast";
31
31
  import { getConfigFromVariableDeclaration, getDefaultExportOptions } from "magicast/helpers";
32
32
  //#region \0rolldown/runtime.js
@@ -36862,6 +36862,7 @@ const DOCS_URL = "https://react.doctor/docs";
36862
36862
  const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
36863
36863
  const FETCH_TIMEOUT_MS = 1e4;
36864
36864
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
36865
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
36865
36866
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
36866
36867
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
36867
36868
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -37679,7 +37680,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
37679
37680
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
37680
37681
  }
37681
37682
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
37682
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
37683
+ const canonicalizeRuleKey = (ruleKey) => {
37684
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
37685
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
37686
+ };
37683
37687
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
37684
37688
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
37685
37689
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -38765,6 +38769,12 @@ const BOOLEAN_FIELD_NAMES = [
38765
38769
  "adoptExistingLintConfig"
38766
38770
  ];
38767
38771
  const STRING_FIELD_NAMES = ["rootDir"];
38772
+ const STRING_ARRAY_FIELD_NAMES = [
38773
+ "projects",
38774
+ "textComponents",
38775
+ "rawTextWrapperComponents",
38776
+ "serverAuthFunctionNames"
38777
+ ];
38768
38778
  const SURFACE_CONTROL_FIELD_NAMES = [
38769
38779
  "includeTags",
38770
38780
  "excludeTags",
@@ -38866,6 +38876,7 @@ const validateConfigTypes = (config) => {
38866
38876
  const validated = { ...config };
38867
38877
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
38868
38878
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
38879
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
38869
38880
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
38870
38881
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
38871
38882
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -41094,43 +41105,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
41094
41105
  * reason: GitInvocationFailed })` so the rest of the codebase
41095
41106
  * sees a single failure channel.
41096
41107
  */
41097
- const runCommand = (input) => scoped(gen(function* () {
41098
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
41099
- cwd: input.directory,
41100
- env: input.env,
41101
- extendEnv: true
41102
- }));
41103
- const maxStdoutBytes = input.maxStdoutBytes;
41104
- const stdoutByteCount = yield* make$13(0);
41105
- const [stdout, stderr, status] = yield* all([
41106
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
41107
- args: [...input.args],
41108
- directory: input.directory,
41109
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
41110
- }) })) : void_)))))),
41111
- mkString(decodeText(handle.stderr)),
41112
- handle.exitCode
41113
- ], { concurrency: 3 });
41114
- return {
41115
- status,
41116
- stdout,
41117
- stderr
41118
- };
41119
- })).pipe(catchTag$1("PlatformError", (cause) => {
41120
- if (input.command !== "git") return succeed$2({
41108
+ const runCommand = (input) => {
41109
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
41121
41110
  status: 127,
41122
41111
  stdout: "",
41123
41112
  stderr: String(cause)
41124
- });
41125
- return new ReactDoctorError({ reason: new GitInvocationFailed({
41113
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
41126
41114
  args: [...input.args],
41127
41115
  directory: input.directory,
41128
41116
  cause
41129
- }) });
41130
- }), withSpan("git.exec", { attributes: {
41131
- "git.command": input.command,
41132
- "git.subcommand": input.args[0] ?? ""
41133
- } }));
41117
+ }) }));
41118
+ return scoped(gen(function* () {
41119
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
41120
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
41121
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
41122
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
41123
+ cwd: input.directory,
41124
+ env: input.env,
41125
+ extendEnv: true
41126
+ }));
41127
+ const maxStdoutBytes = input.maxStdoutBytes;
41128
+ const stdoutByteCount = yield* make$13(0);
41129
+ const [stdout, stderr, status] = yield* all([
41130
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
41131
+ args: [...input.args],
41132
+ directory: input.directory,
41133
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
41134
+ }) })) : void_)))))),
41135
+ mkString(decodeText(handle.stderr)),
41136
+ handle.exitCode
41137
+ ], { concurrency: 3 });
41138
+ return {
41139
+ status,
41140
+ stdout,
41141
+ stderr
41142
+ };
41143
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
41144
+ "git.command": input.command,
41145
+ "git.subcommand": input.args[0] ?? ""
41146
+ } }));
41147
+ };
41134
41148
  const runGit = (directory, args) => runCommand({
41135
41149
  command: "git",
41136
41150
  args,
@@ -41163,7 +41177,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41163
41177
  "rev-parse",
41164
41178
  "--verify",
41165
41179
  branch
41166
- ]).pipe(map$3((result) => result.status === 0));
41180
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
41167
41181
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
41168
41182
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
41169
41183
  "merge-base",
@@ -41377,7 +41391,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41377
41391
  ]);
41378
41392
  if (result.status !== 0) return null;
41379
41393
  return parseChangedLineRanges(result.stdout);
41380
- }).pipe(withSpan("Git.changedLineRanges"))
41394
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
41381
41395
  });
41382
41396
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
41383
41397
  /**
@@ -44839,6 +44853,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
44839
44853
  const METRIC = {
44840
44854
  cliInvoked: "cli.invoked",
44841
44855
  cliError: "cli.error",
44856
+ cliEnvironmentError: "cli.env_error",
44842
44857
  projectDetected: "project.detected",
44843
44858
  projectPathSelected: "project.path_selected",
44844
44859
  projectConfigSelected: "project.config_selected",
@@ -44911,7 +44926,7 @@ const makeNoopConsole = () => ({
44911
44926
  });
44912
44927
  //#endregion
44913
44928
  //#region src/cli/utils/version.ts
44914
- const VERSION = "0.5.8-dev.7f9e7f4";
44929
+ const VERSION = "0.5.8-dev.970babc";
44915
44930
  //#endregion
44916
44931
  //#region src/cli/utils/json-mode.ts
44917
44932
  let context = null;
@@ -45275,13 +45290,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45275
45290
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45276
45291
  * standard `SENTRY_RELEASE` override.
45277
45292
  */
45278
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.7f9e7f4`;
45293
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.970babc`;
45279
45294
  /**
45280
45295
  * Deployment environment shown in Sentry's environment filter. Defaults to
45281
45296
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45282
45297
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45283
45298
  */
45284
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.7f9e7f4") ? "development" : "production");
45299
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.970babc") ? "development" : "production");
45285
45300
  /**
45286
45301
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45287
45302
  * (set to `0` to disable tracing) and falls back to
@@ -49690,6 +49705,7 @@ const CI_PITCH_EVENT = "ci-pitch";
49690
49705
  const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
49691
49706
  const SETUP_HINT_EVENT = "setup-hint";
49692
49707
  const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
49708
+ const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
49693
49709
  const foldLegacyDecisions = (projects, legacy, eventId) => {
49694
49710
  for (const [hash, record] of Object.entries(legacy ?? {})) {
49695
49711
  const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
@@ -50353,7 +50369,7 @@ const readPackageJson = (projectRoot) => {
50353
50369
  return null;
50354
50370
  }
50355
50371
  };
50356
- const writeJsonFile$1 = (filePath, value) => {
50372
+ const writeJsonFile = (filePath, value) => {
50357
50373
  NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
50358
50374
  };
50359
50375
  const packageHasDependency = (projectRoot, dependencyName) => {
@@ -51073,7 +51089,8 @@ const getStagedSourceFiles = async (directory) => {
51073
51089
  return [...await runPromise(gen(function* () {
51074
51090
  return yield* (yield* StagedFiles).discoverSourceFiles(directory);
51075
51091
  }).pipe(provide(stagedFilesLayer)))];
51076
- } catch {
51092
+ } catch (error) {
51093
+ cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
51077
51094
  return [];
51078
51095
  }
51079
51096
  };
@@ -51092,6 +51109,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
51092
51109
  };
51093
51110
  };
51094
51111
  //#endregion
51112
+ //#region src/cli/utils/is-environment-error.ts
51113
+ const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
51114
+ const ENVIRONMENT_ERROR_CODES = new Set([
51115
+ "ENOSPC",
51116
+ "EIO",
51117
+ "EROFS",
51118
+ "EACCES",
51119
+ "EPERM",
51120
+ "ENOTDIR"
51121
+ ]);
51122
+ const isEnvironmentError = (error) => {
51123
+ if (!isNodeSystemError(error)) return false;
51124
+ if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
51125
+ return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
51126
+ };
51127
+ const formatEnvironmentError = (error) => {
51128
+ if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
51129
+ switch (error.code) {
51130
+ case "ENOSPC": return "No space left on device. Free up disk space and try again.";
51131
+ case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
51132
+ case "EROFS": return "Read-only filesystem: cannot write to this location.";
51133
+ case "EACCES":
51134
+ case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
51135
+ case "ENOTDIR": return error.path ? `A file exists at ${error.path} or one of its parent paths where a directory was expected.` : "A file exists where a directory was expected.";
51136
+ case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
51137
+ default: return error.message;
51138
+ }
51139
+ };
51140
+ //#endregion
51095
51141
  //#region src/cli/utils/handle-error.ts
51096
51142
  const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
51097
51143
  const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
@@ -51174,15 +51220,19 @@ const handleError = (error, options = {}) => {
51174
51220
  process.exitCode = 1;
51175
51221
  };
51176
51222
  /**
51177
- * Renderer for expected, user-actionable failures — a bad `--diff` value or
51178
- * a base branch that isn't fetched. Prints just the (already human-readable)
51179
- * message no "Something went wrong", prefilled issue, Discord link, or
51180
- * Sentry reference because there is no bug to report.
51223
+ * Renderer for expected, user-actionable failures — a bad `--diff` value,
51224
+ * a base branch that isn't fetched, or environment errors like disk-full or
51225
+ * permission-denied. Prints just the (already human-readable) message no
51226
+ * "Something went wrong", prefilled issue, Discord link, or Sentry reference
51227
+ * — because there is no bug to report.
51181
51228
  */
51182
51229
  const handleUserError = (error, options = {}) => {
51230
+ const isEnvError = isEnvironmentError(error);
51231
+ if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
51232
+ const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
51183
51233
  runSync(gen(function* () {
51184
51234
  yield* error$1("");
51185
- yield* error$1(highlighter.error(formatErrorForReport(error)));
51235
+ yield* error$1(highlighter.error(message));
51186
51236
  yield* error$1("");
51187
51237
  }));
51188
51238
  if (options.shouldExit !== false) process.exit(1);
@@ -51197,7 +51247,7 @@ const handleUserError = (error, options = {}) => {
51197
51247
  * `handleUserError` (a plain message — no "Something went wrong", prefilled
51198
51248
  * issue, Discord link, or Sentry reference), since there is no bug to report.
51199
51249
  *
51200
- * Three distinct shapes reach the CLI's catch blocks:
51250
+ * Four distinct shapes reach the CLI's catch blocks:
51201
51251
  *
51202
51252
  * - **Project-discovery failures** (`NoReactDependencyError`,
51203
51253
  * `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
@@ -51210,12 +51260,19 @@ const handleUserError = (error, options = {}) => {
51210
51260
  * `--project` name.
51211
51261
  * - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
51212
51262
  * stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
51263
+ * - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
51264
+ * `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
51265
+ * full / failing / read-only, permission denied, or a path blocked by a
51266
+ * file. React Doctor cannot fix the user's environment; exit cleanly with an
51267
+ * actionable message instead of crashing. See `is-environment-error.ts` for
51268
+ * why the set stays narrow (codes that usually mean our bug keep reaching
51269
+ * Sentry).
51213
51270
  *
51214
51271
  * This composes the existing core narrowers rather than introducing a new
51215
51272
  * error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
51216
51273
  * knowledge of the `ReactDoctorError` shape.
51217
51274
  */
51218
- const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
51275
+ const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
51219
51276
  //#endregion
51220
51277
  //#region src/cli/utils/build-handoff-payload.ts
51221
51278
  const buildHandoffPayload = (input) => {
@@ -51296,6 +51353,20 @@ const detectAvailableAgents = async () => {
51296
51353
  const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
51297
51354
  return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
51298
51355
  };
51356
+ const DEFAULT_INSTALL_AGENTS = [
51357
+ "claude-code",
51358
+ "cursor",
51359
+ "codex",
51360
+ "opencode"
51361
+ ];
51362
+ const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
51363
+ const detected = new Set(detectedAgents);
51364
+ const remembered = rememberedAgents.filter((agent) => detected.has(agent));
51365
+ if (remembered.length > 0) return remembered;
51366
+ const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
51367
+ if (defaults.length > 0) return defaults;
51368
+ return detectedAgents.length === 1 ? [...detectedAgents] : [];
51369
+ };
51299
51370
  //#endregion
51300
51371
  //#region src/cli/utils/install-doctor-script.ts
51301
51372
  const DOCTOR_SCRIPT_NAME = "doctor";
@@ -51377,7 +51448,7 @@ const installDoctorScript = (options) => {
51377
51448
  };
51378
51449
  })();
51379
51450
  const scriptStatus = scriptTarget.status;
51380
- if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
51451
+ if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
51381
51452
  ...packageJson,
51382
51453
  scripts: {
51383
51454
  ...isRecord$1(scripts) ? scripts : {},
@@ -51948,6 +52019,19 @@ const setUpGitHubActions = async (options) => {
51948
52019
  return didCreateWorkflow;
51949
52020
  };
51950
52021
  //#endregion
52022
+ //#region src/cli/utils/install-agents-preference.ts
52023
+ const INSTALL_AGENTS_PREFERENCE = {
52024
+ id: INSTALL_AGENTS_PREFERENCE_ID,
52025
+ scope: "global"
52026
+ };
52027
+ const PREFERENCE_SEPARATOR = ",";
52028
+ const readInstallAgents = (options = {}) => {
52029
+ const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
52030
+ if (stored === null) return [];
52031
+ return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
52032
+ };
52033
+ const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
52034
+ //#endregion
51951
52035
  //#region src/cli/utils/install-agent-hooks.ts
51952
52036
  const CLAUDE_AGENT = "claude-code";
51953
52037
  const CURSOR_AGENT = "cursor";
@@ -51958,20 +52042,34 @@ const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
51958
52042
  const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.sh";
51959
52043
  const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
51960
52044
  const CURSOR_HOOKS_SCHEMA_VERSION = 1;
51961
- const JSON_INDENT_SPACES$1 = 2;
51962
52045
  const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
51963
52046
  const readJsonFile = (filePath, fallback) => {
51964
52047
  if (!NFS.existsSync(filePath)) return fallback;
51965
52048
  const content = NFS.readFileSync(filePath, "utf8").trim();
51966
52049
  if (content.length === 0) return fallback;
51967
- return JSON.parse(content);
52050
+ try {
52051
+ return JSON.parse(content);
52052
+ } catch (error) {
52053
+ if (error instanceof SyntaxError) throw new CliInputError(`Could not parse ${filePath}: the file contains invalid JSON. Fix the syntax errors in this file and re-run the install command.`);
52054
+ throw error;
52055
+ }
51968
52056
  };
51969
- const writeJsonFile = (filePath, value) => {
51970
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
51971
- NFS.writeFileSync(filePath, `${JSON.stringify(value, null, JSON_INDENT_SPACES$1)}\n`);
52057
+ const ensureDirectoryExists = (directoryPath) => {
52058
+ try {
52059
+ NFS.mkdirSync(directoryPath, { recursive: true });
52060
+ } catch (error) {
52061
+ const code = error.code;
52062
+ if (code === "EACCES" || code === "EPERM") throw new CliInputError(`Could not create directory ${directoryPath}: permission denied. Ensure you have write permissions for this location and re-run the install command.`);
52063
+ if (code === "ENOTDIR" || code === "EEXIST") throw new CliInputError(`Could not create directory ${directoryPath}: a file exists at this path or one of its parent paths. Remove the conflicting file and re-run the install command.`);
52064
+ throw error;
52065
+ }
52066
+ };
52067
+ const writeJsonFileWithDirectoryCheck = (filePath, value) => {
52068
+ ensureDirectoryExists(Path.dirname(filePath));
52069
+ writeJsonFile(filePath, value);
51972
52070
  };
51973
52071
  const writeHookScript = (filePath) => {
51974
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
52072
+ ensureDirectoryExists(Path.dirname(filePath));
51975
52073
  NFS.writeFileSync(filePath, buildAgentHookScript());
51976
52074
  NFS.chmodSync(filePath, 493);
51977
52075
  };
@@ -51987,7 +52085,7 @@ const installClaudeHook = (projectRoot) => {
51987
52085
  command: CLAUDE_HOOK_COMMAND
51988
52086
  }] });
51989
52087
  hooks.PostToolBatch = postToolBatchHooks;
51990
- writeJsonFile(settingsPath, {
52088
+ writeJsonFileWithDirectoryCheck(settingsPath, {
51991
52089
  ...settings,
51992
52090
  hooks
51993
52091
  });
@@ -52007,7 +52105,7 @@ const installCursorHook = (projectRoot) => {
52007
52105
  timeout: 120
52008
52106
  });
52009
52107
  hooks.postToolUse = postToolUseHooks;
52010
- writeJsonFile(configPath, {
52108
+ writeJsonFileWithDirectoryCheck(configPath, {
52011
52109
  ...config,
52012
52110
  version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
52013
52111
  hooks
@@ -52243,7 +52341,7 @@ const installPackageJsonHook = (options, strategy) => {
52243
52341
  parent = cloned;
52244
52342
  }
52245
52343
  parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
52246
- writeJsonFile$1(packageJsonPath, nextPackageJson);
52344
+ writeJsonFile(packageJsonPath, nextPackageJson);
52247
52345
  removeLegacyManagedRunner(options.projectRoot);
52248
52346
  return {
52249
52347
  hookPath: packageJsonPath,
@@ -52826,6 +52924,9 @@ const runInstallReactDoctor = async (options = {}) => {
52826
52924
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
52827
52925
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
52828
52926
  if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
52927
+ const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
52928
+ const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
52929
+ const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
52829
52930
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
52830
52931
  type: "multiselect",
52831
52932
  name: "agents",
@@ -52833,12 +52934,13 @@ const runInstallReactDoctor = async (options = {}) => {
52833
52934
  choices: detectedAgents.map((agent) => ({
52834
52935
  title: getSkillAgentConfig(agent).displayName,
52835
52936
  value: agent,
52836
- selected: true
52937
+ selected: defaultSelectedAgents.includes(agent)
52837
52938
  })),
52838
52939
  instructions: false,
52839
52940
  min: 1
52840
52941
  }, promptOptions)).agents ?? [];
52841
52942
  if (selectedAgents.length === 0) return;
52943
+ if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
52842
52944
  let dependencyResult;
52843
52945
  if (!options.dryRun) {
52844
52946
  await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
@@ -52897,6 +52999,8 @@ const runInstallReactDoctor = async (options = {}) => {
52897
52999
  }
52898
53000
  recordCount(METRIC.installCompleted, 1, {
52899
53001
  agentsCount: selectedAgents.length,
53002
+ agentsDetected: detectedAgents.length,
53003
+ usedRememberedAgents,
52900
53004
  gitHook: shouldInstallGitHook,
52901
53005
  agentHooks: shouldInstallAgentHooks,
52902
53006
  workflow: didInstallWorkflow,
@@ -53676,7 +53780,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
53676
53780
  };
53677
53781
  const warnDiffUnavailable = (requested, isQuiet) => {
53678
53782
  if (isQuiet) return;
53679
- if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (merge-base failed or HEAD has no history). Running full scan.`);
53783
+ if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (git unavailable, ref not found, or merge-base failed). Running full scan.`);
53680
53784
  else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
53681
53785
  cliLogger.break();
53682
53786
  };
@@ -53873,6 +53977,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
53873
53977
  return requestedNames.map((requestedName) => {
53874
53978
  const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
53875
53979
  if (matched) return matched.directory;
53980
+ if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
53876
53981
  const candidateDirectory = Path.resolve(rootDirectory, requestedName);
53877
53982
  if (isDirectory(candidateDirectory)) {
53878
53983
  recordCount(METRIC.projectPathSelected);
@@ -54367,6 +54472,10 @@ const installAction = async (options, command) => {
54367
54472
  projectRoot: options.cwd ?? process.cwd()
54368
54473
  });
54369
54474
  } catch (error) {
54475
+ if (isExpectedUserError(error)) {
54476
+ handleUserError(error);
54477
+ return;
54478
+ }
54370
54479
  handleError(error, { sentryEventId: await reportErrorToSentry(error) });
54371
54480
  }
54372
54481
  };
@@ -55374,4 +55483,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
55374
55483
  export {};
55375
55484
 
55376
55485
  //# sourceMappingURL=cli.js.map
55377
- //# debugId=0b6893a8-f482-50d5-ad21-45ebd17e98a0
55486
+ //# debugId=a201a872-19de-5f85-bec4-8ecc9f0ae2a8
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="33508ee6-c977-5b5f-8585-9939928ce74d")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="dccb6d63-63d4-5e91-af21-2756a08fe93a")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import * as NFS from "node:fs";
@@ -33694,6 +33694,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33694
33694
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33695
33695
  const FETCH_TIMEOUT_MS = 1e4;
33696
33696
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33697
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33697
33698
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33698
33699
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33699
33700
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -34487,7 +34488,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34487
34488
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34488
34489
  }
34489
34490
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34490
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34491
+ const canonicalizeRuleKey = (ruleKey) => {
34492
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34493
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34494
+ };
34491
34495
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34492
34496
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34493
34497
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -35577,6 +35581,12 @@ const BOOLEAN_FIELD_NAMES = [
35577
35581
  "adoptExistingLintConfig"
35578
35582
  ];
35579
35583
  const STRING_FIELD_NAMES = ["rootDir"];
35584
+ const STRING_ARRAY_FIELD_NAMES = [
35585
+ "projects",
35586
+ "textComponents",
35587
+ "rawTextWrapperComponents",
35588
+ "serverAuthFunctionNames"
35589
+ ];
35580
35590
  const SURFACE_CONTROL_FIELD_NAMES = [
35581
35591
  "includeTags",
35582
35592
  "excludeTags",
@@ -35678,6 +35688,7 @@ const validateConfigTypes = (config) => {
35678
35688
  const validated = { ...config };
35679
35689
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35680
35690
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35691
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35681
35692
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35682
35693
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35683
35694
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -37862,43 +37873,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37862
37873
  * reason: GitInvocationFailed })` so the rest of the codebase
37863
37874
  * sees a single failure channel.
37864
37875
  */
37865
- const runCommand = (input) => scoped(gen(function* () {
37866
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37867
- cwd: input.directory,
37868
- env: input.env,
37869
- extendEnv: true
37870
- }));
37871
- const maxStdoutBytes = input.maxStdoutBytes;
37872
- const stdoutByteCount = yield* make$13(0);
37873
- const [stdout, stderr, status] = yield* all([
37874
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37875
- args: [...input.args],
37876
- directory: input.directory,
37877
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37878
- }) })) : void_)))))),
37879
- mkString(decodeText(handle.stderr)),
37880
- handle.exitCode
37881
- ], { concurrency: 3 });
37882
- return {
37883
- status,
37884
- stdout,
37885
- stderr
37886
- };
37887
- })).pipe(catchTag$1("PlatformError", (cause) => {
37888
- if (input.command !== "git") return succeed$2({
37876
+ const runCommand = (input) => {
37877
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37889
37878
  status: 127,
37890
37879
  stdout: "",
37891
37880
  stderr: String(cause)
37892
- });
37893
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37881
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37894
37882
  args: [...input.args],
37895
37883
  directory: input.directory,
37896
37884
  cause
37897
- }) });
37898
- }), withSpan("git.exec", { attributes: {
37899
- "git.command": input.command,
37900
- "git.subcommand": input.args[0] ?? ""
37901
- } }));
37885
+ }) }));
37886
+ return scoped(gen(function* () {
37887
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37888
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37889
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
37890
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37891
+ cwd: input.directory,
37892
+ env: input.env,
37893
+ extendEnv: true
37894
+ }));
37895
+ const maxStdoutBytes = input.maxStdoutBytes;
37896
+ const stdoutByteCount = yield* make$13(0);
37897
+ const [stdout, stderr, status] = yield* all([
37898
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37899
+ args: [...input.args],
37900
+ directory: input.directory,
37901
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37902
+ }) })) : void_)))))),
37903
+ mkString(decodeText(handle.stderr)),
37904
+ handle.exitCode
37905
+ ], { concurrency: 3 });
37906
+ return {
37907
+ status,
37908
+ stdout,
37909
+ stderr
37910
+ };
37911
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
37912
+ "git.command": input.command,
37913
+ "git.subcommand": input.args[0] ?? ""
37914
+ } }));
37915
+ };
37902
37916
  const runGit = (directory, args) => runCommand({
37903
37917
  command: "git",
37904
37918
  args,
@@ -37931,7 +37945,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37931
37945
  "rev-parse",
37932
37946
  "--verify",
37933
37947
  branch
37934
- ]).pipe(map$3((result) => result.status === 0));
37948
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37935
37949
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37936
37950
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37937
37951
  "merge-base",
@@ -38145,7 +38159,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38145
38159
  ]);
38146
38160
  if (result.status !== 0) return null;
38147
38161
  return parseChangedLineRanges(result.stdout);
38148
- }).pipe(withSpan("Git.changedLineRanges"))
38162
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38149
38163
  });
38150
38164
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38151
38165
  /**
@@ -41469,4 +41483,4 @@ const toJsonReport = (result, options) => buildJsonReport({
41469
41483
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
41470
41484
 
41471
41485
  //# sourceMappingURL=index.js.map
41472
- //# debugId=33508ee6-c977-5b5f-8585-9939928ce74d
41486
+ //# debugId=dccb6d63-63d4-5e91-af21-2756a08fe93a
package/dist/lsp.js CHANGED
@@ -33730,6 +33730,7 @@ const MILLISECONDS_PER_SECOND = 1e3;
33730
33730
  const SCORE_API_URL = "https://www.react.doctor/api/score";
33731
33731
  const FETCH_TIMEOUT_MS = 1e4;
33732
33732
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
33733
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
33733
33734
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
33734
33735
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
33735
33736
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -34546,7 +34547,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
34546
34547
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
34547
34548
  }
34548
34549
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
34549
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
34550
+ const canonicalizeRuleKey = (ruleKey) => {
34551
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
34552
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
34553
+ };
34550
34554
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
34551
34555
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
34552
34556
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -35610,6 +35614,12 @@ const BOOLEAN_FIELD_NAMES = [
35610
35614
  "adoptExistingLintConfig"
35611
35615
  ];
35612
35616
  const STRING_FIELD_NAMES = ["rootDir"];
35617
+ const STRING_ARRAY_FIELD_NAMES = [
35618
+ "projects",
35619
+ "textComponents",
35620
+ "rawTextWrapperComponents",
35621
+ "serverAuthFunctionNames"
35622
+ ];
35613
35623
  const SURFACE_CONTROL_FIELD_NAMES = [
35614
35624
  "includeTags",
35615
35625
  "excludeTags",
@@ -35711,6 +35721,7 @@ const validateConfigTypes = (config) => {
35711
35721
  const validated = { ...config };
35712
35722
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
35713
35723
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
35724
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
35714
35725
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
35715
35726
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
35716
35727
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -37847,43 +37858,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
37847
37858
  * reason: GitInvocationFailed })` so the rest of the codebase
37848
37859
  * sees a single failure channel.
37849
37860
  */
37850
- const runCommand = (input) => scoped(gen(function* () {
37851
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37852
- cwd: input.directory,
37853
- env: input.env,
37854
- extendEnv: true
37855
- }));
37856
- const maxStdoutBytes = input.maxStdoutBytes;
37857
- const stdoutByteCount = yield* make$13(0);
37858
- const [stdout, stderr, status] = yield* all([
37859
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37860
- args: [...input.args],
37861
- directory: input.directory,
37862
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37863
- }) })) : void_)))))),
37864
- mkString(decodeText(handle.stderr)),
37865
- handle.exitCode
37866
- ], { concurrency: 3 });
37867
- return {
37868
- status,
37869
- stdout,
37870
- stderr
37871
- };
37872
- })).pipe(catchTag$1("PlatformError", (cause) => {
37873
- if (input.command !== "git") return succeed$2({
37861
+ const runCommand = (input) => {
37862
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
37874
37863
  status: 127,
37875
37864
  stdout: "",
37876
37865
  stderr: String(cause)
37877
- });
37878
- return new ReactDoctorError({ reason: new GitInvocationFailed({
37866
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37879
37867
  args: [...input.args],
37880
37868
  directory: input.directory,
37881
37869
  cause
37882
- }) });
37883
- }), withSpan("git.exec", { attributes: {
37884
- "git.command": input.command,
37885
- "git.subcommand": input.args[0] ?? ""
37886
- } }));
37870
+ }) }));
37871
+ return scoped(gen(function* () {
37872
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
37873
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
37874
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
37875
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
37876
+ cwd: input.directory,
37877
+ env: input.env,
37878
+ extendEnv: true
37879
+ }));
37880
+ const maxStdoutBytes = input.maxStdoutBytes;
37881
+ const stdoutByteCount = yield* make$13(0);
37882
+ const [stdout, stderr, status] = yield* all([
37883
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
37884
+ args: [...input.args],
37885
+ directory: input.directory,
37886
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
37887
+ }) })) : void_)))))),
37888
+ mkString(decodeText(handle.stderr)),
37889
+ handle.exitCode
37890
+ ], { concurrency: 3 });
37891
+ return {
37892
+ status,
37893
+ stdout,
37894
+ stderr
37895
+ };
37896
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
37897
+ "git.command": input.command,
37898
+ "git.subcommand": input.args[0] ?? ""
37899
+ } }));
37900
+ };
37887
37901
  const runGit = (directory, args) => runCommand({
37888
37902
  command: "git",
37889
37903
  args,
@@ -37916,7 +37930,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
37916
37930
  "rev-parse",
37917
37931
  "--verify",
37918
37932
  branch
37919
- ]).pipe(map$3((result) => result.status === 0));
37933
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
37920
37934
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
37921
37935
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
37922
37936
  "merge-base",
@@ -38130,7 +38144,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38130
38144
  ]);
38131
38145
  if (result.status !== 0) return null;
38132
38146
  return parseChangedLineRanges(result.stdout);
38133
- }).pipe(withSpan("Git.changedLineRanges"))
38147
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38134
38148
  });
38135
38149
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
38136
38150
  /**
@@ -42903,6 +42917,7 @@ const SENTRY_FLUSH_TIMEOUT_MS = 2e3;
42903
42917
  const METRIC = {
42904
42918
  cliInvoked: "cli.invoked",
42905
42919
  cliError: "cli.error",
42920
+ cliEnvironmentError: "cli.env_error",
42906
42921
  projectDetected: "project.detected",
42907
42922
  projectPathSelected: "project.path_selected",
42908
42923
  projectConfigSelected: "project.config_selected",
@@ -43249,5 +43264,5 @@ const startLanguageServer = () => {
43249
43264
  };
43250
43265
  //#endregion
43251
43266
  export { startLanguageServer };
43252
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9ee72ccc-703c-588f-b7e4-c92657144721")}catch(e){}}();
43253
- //# debugId=9ee72ccc-703c-588f-b7e4-c92657144721
43267
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d12f3c5d-14fd-59ba-9d8a-7e3f589c88ad")}catch(e){}}();
43268
+ //# debugId=d12f3c5d-14fd-59ba-9d8a-7e3f589c88ad
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.5.8-dev.7f9e7f4",
3
+ "version": "0.5.8-dev.970babc",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -64,7 +64,7 @@
64
64
  "vscode-languageserver-textdocument": "^1.0.12",
65
65
  "vscode-uri": "^3.1.0",
66
66
  "deslop-js": "0.5.8",
67
- "oxlint-plugin-react-doctor": "0.5.8-dev.7f9e7f4"
67
+ "oxlint-plugin-react-doctor": "0.5.8-dev.970babc"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/babel__code-frame": "^7.27.0",
@@ -73,8 +73,8 @@
73
73
  "commander": "^14.0.3",
74
74
  "ora": "^9.4.0",
75
75
  "@react-doctor/core": "0.5.8",
76
- "@react-doctor/api": "0.5.8",
77
- "@react-doctor/language-server": "0.5.8"
76
+ "@react-doctor/language-server": "0.5.8",
77
+ "@react-doctor/api": "0.5.8"
78
78
  },
79
79
  "engines": {
80
80
  "node": "^20.19.0 || >=22.13.0"