react-doctor 0.5.8-dev.31c0657 → 0.5.8-dev.5774deb
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 +299 -91
- package/dist/index.js +179 -66
- package/dist/lsp.js +180 -66
- package/package.json +2 -2
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]="
|
|
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]="498477a7-1f41-5ecb-9999-c48cfef57259")}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
|
|
@@ -9041,7 +9041,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
|
|
|
9041
9041
|
* @since 2.0.0
|
|
9042
9042
|
*/
|
|
9043
9043
|
const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
|
|
9044
|
-
const setImmediate = "setImmediate" in globalThis ? (f) => {
|
|
9044
|
+
const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
|
|
9045
9045
|
const timer = globalThis.setImmediate(f);
|
|
9046
9046
|
return () => globalThis.clearImmediate(timer);
|
|
9047
9047
|
} : (f) => {
|
|
@@ -9085,7 +9085,7 @@ var PriorityBuckets = class {
|
|
|
9085
9085
|
var MixedScheduler = class {
|
|
9086
9086
|
executionMode;
|
|
9087
9087
|
setImmediate;
|
|
9088
|
-
constructor(executionMode = "async", setImmediateFn = setImmediate) {
|
|
9088
|
+
constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
|
|
9089
9089
|
this.executionMode = executionMode;
|
|
9090
9090
|
this.setImmediate = setImmediateFn;
|
|
9091
9091
|
}
|
|
@@ -9110,7 +9110,7 @@ var MixedSchedulerDispatcher = class {
|
|
|
9110
9110
|
tasks = /* @__PURE__ */ new PriorityBuckets();
|
|
9111
9111
|
running = void 0;
|
|
9112
9112
|
setImmediate;
|
|
9113
|
-
constructor(setImmediateFn = setImmediate) {
|
|
9113
|
+
constructor(setImmediateFn = setImmediate$1) {
|
|
9114
9114
|
this.setImmediate = setImmediateFn;
|
|
9115
9115
|
}
|
|
9116
9116
|
/**
|
|
@@ -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"];
|
|
@@ -36946,6 +36947,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
|
36946
36947
|
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36947
36948
|
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36948
36949
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36950
|
+
const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
36949
36951
|
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36950
36952
|
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36951
36953
|
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
@@ -37679,7 +37681,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37679
37681
|
NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
|
|
37680
37682
|
}
|
|
37681
37683
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37682
|
-
const canonicalizeRuleKey = (ruleKey) =>
|
|
37684
|
+
const canonicalizeRuleKey = (ruleKey) => {
|
|
37685
|
+
const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
|
|
37686
|
+
return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
|
|
37687
|
+
};
|
|
37683
37688
|
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37684
37689
|
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37685
37690
|
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
@@ -38553,7 +38558,7 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38553
38558
|
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38554
38559
|
return Math.min(Math.floor(requested), 32);
|
|
38555
38560
|
};
|
|
38556
|
-
const readSystemFacts = () => ({
|
|
38561
|
+
const readSystemFacts$1 = () => ({
|
|
38557
38562
|
availableCores: os.availableParallelism(),
|
|
38558
38563
|
totalMemoryBytes: os.totalmem(),
|
|
38559
38564
|
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
@@ -38574,7 +38579,7 @@ const readSystemFacts = () => ({
|
|
|
38574
38579
|
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38575
38580
|
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38576
38581
|
*/
|
|
38577
|
-
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38582
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
|
|
38578
38583
|
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38579
38584
|
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38580
38585
|
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
@@ -38765,6 +38770,12 @@ const BOOLEAN_FIELD_NAMES = [
|
|
|
38765
38770
|
"adoptExistingLintConfig"
|
|
38766
38771
|
];
|
|
38767
38772
|
const STRING_FIELD_NAMES = ["rootDir"];
|
|
38773
|
+
const STRING_ARRAY_FIELD_NAMES = [
|
|
38774
|
+
"projects",
|
|
38775
|
+
"textComponents",
|
|
38776
|
+
"rawTextWrapperComponents",
|
|
38777
|
+
"serverAuthFunctionNames"
|
|
38778
|
+
];
|
|
38768
38779
|
const SURFACE_CONTROL_FIELD_NAMES = [
|
|
38769
38780
|
"includeTags",
|
|
38770
38781
|
"excludeTags",
|
|
@@ -38866,6 +38877,7 @@ const validateConfigTypes = (config) => {
|
|
|
38866
38877
|
const validated = { ...config };
|
|
38867
38878
|
for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
|
|
38868
38879
|
for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
|
|
38880
|
+
for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
|
|
38869
38881
|
applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
|
|
38870
38882
|
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
|
|
38871
38883
|
applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
|
|
@@ -40128,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
|
|
|
40128
40140
|
}
|
|
40129
40141
|
return true;
|
|
40130
40142
|
};
|
|
40131
|
-
const
|
|
40143
|
+
const yieldToEventLoop = () => new Promise((resolve) => {
|
|
40144
|
+
setImmediate(resolve);
|
|
40145
|
+
});
|
|
40146
|
+
const createSecurityScanSession = (rootDirectory, options) => {
|
|
40132
40147
|
const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
|
|
40133
40148
|
const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
|
|
40134
40149
|
const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
|
|
@@ -40143,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40143
40158
|
committedFilesOnly: rule.committedFilesOnly === true
|
|
40144
40159
|
}];
|
|
40145
40160
|
});
|
|
40146
|
-
if (enabledScanRules.length === 0) return
|
|
40161
|
+
if (enabledScanRules.length === 0) return null;
|
|
40147
40162
|
const diagnostics = [];
|
|
40148
40163
|
const seen = /* @__PURE__ */ new Set();
|
|
40149
40164
|
const gitIgnoredCache = /* @__PURE__ */ new Map();
|
|
@@ -40155,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40155
40170
|
}
|
|
40156
40171
|
return status === true;
|
|
40157
40172
|
};
|
|
40158
|
-
|
|
40159
|
-
|
|
40160
|
-
|
|
40161
|
-
|
|
40162
|
-
|
|
40163
|
-
|
|
40164
|
-
|
|
40173
|
+
const scanFile = (file) => {
|
|
40174
|
+
for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
|
|
40175
|
+
if (committedFilesOnly && isFileGitIgnored(file)) continue;
|
|
40176
|
+
const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
|
|
40177
|
+
const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
|
|
40178
|
+
if (seen.has(key)) continue;
|
|
40179
|
+
seen.add(key);
|
|
40180
|
+
diagnostics.push(diagnostic);
|
|
40181
|
+
}
|
|
40182
|
+
};
|
|
40183
|
+
return {
|
|
40184
|
+
scanFile,
|
|
40185
|
+
diagnostics
|
|
40186
|
+
};
|
|
40187
|
+
};
|
|
40188
|
+
const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
|
|
40189
|
+
const session = createSecurityScanSession(rootDirectory, options);
|
|
40190
|
+
if (session === null) return [];
|
|
40191
|
+
let filesSinceYield = 0;
|
|
40192
|
+
for (const file of collectSecurityScanFiles(rootDirectory)) {
|
|
40193
|
+
session.scanFile(file);
|
|
40194
|
+
filesSinceYield += 1;
|
|
40195
|
+
if (filesSinceYield >= 16) {
|
|
40196
|
+
filesSinceYield = 0;
|
|
40197
|
+
await yieldToEventLoop();
|
|
40198
|
+
}
|
|
40165
40199
|
}
|
|
40166
|
-
return diagnostics;
|
|
40200
|
+
return session.diagnostics;
|
|
40167
40201
|
};
|
|
40168
40202
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
40169
40203
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
@@ -40396,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
40396
40430
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
40397
40431
|
};
|
|
40398
40432
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
40433
|
+
const readSystemFacts = () => ({
|
|
40434
|
+
availableCores: os.availableParallelism(),
|
|
40435
|
+
totalMemoryBytes: os.totalmem(),
|
|
40436
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
40437
|
+
});
|
|
40438
|
+
/**
|
|
40439
|
+
* How many real deslop dead-code child processes may run at once, across the
|
|
40440
|
+
* concurrent per-project `runInspect` fibers of one CLI run. The cap is the
|
|
40441
|
+
* smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
|
|
40442
|
+
* workers that fit in available memory, floored at 1.
|
|
40443
|
+
*
|
|
40444
|
+
* On a roomy dev box / CI runner this resolves high enough that every
|
|
40445
|
+
* concurrently-scanned project still spawns its own worker (no serialization vs
|
|
40446
|
+
* the prior uncapped behavior); on a memory-constrained runner it collapses
|
|
40447
|
+
* toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
|
|
40448
|
+
* instead of oversubscribing memory with N simultaneous children — the global
|
|
40449
|
+
* cap the per-project spawn path lacked.
|
|
40450
|
+
*
|
|
40451
|
+
* Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
|
|
40452
|
+
* heavier dead-code worker. `facts` is injectable for tests.
|
|
40453
|
+
*/
|
|
40454
|
+
const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
|
|
40455
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
40456
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
|
|
40457
|
+
return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
40458
|
+
};
|
|
40459
|
+
let availableSlots = -1;
|
|
40460
|
+
const waiters = [];
|
|
40461
|
+
const releaseSlot = () => {
|
|
40462
|
+
const nextWaiter = waiters.shift();
|
|
40463
|
+
if (nextWaiter !== void 0) nextWaiter();
|
|
40464
|
+
else availableSlots += 1;
|
|
40465
|
+
};
|
|
40466
|
+
/**
|
|
40467
|
+
* Runs `task` once a dead-code worker slot is free, releasing the slot when the
|
|
40468
|
+
* task settles (success or failure). With a high cap (roomy machine) every
|
|
40469
|
+
* caller proceeds immediately; with a low cap (constrained runner) callers
|
|
40470
|
+
* queue and run as slots free.
|
|
40471
|
+
*
|
|
40472
|
+
* `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
|
|
40473
|
+
* this caller is queued, the call rejects without acquiring a slot or running
|
|
40474
|
+
* `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
|
|
40475
|
+
* then spawn a child only to tear it down. A queued caller that aborts removes
|
|
40476
|
+
* its own waiter so a later release never hands a slot to a dead request.
|
|
40477
|
+
*/
|
|
40478
|
+
const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
40479
|
+
if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
|
|
40480
|
+
if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
|
|
40481
|
+
if (availableSlots > 0) availableSlots -= 1;
|
|
40482
|
+
else await new Promise((resolve, reject) => {
|
|
40483
|
+
const waiter = () => {
|
|
40484
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40485
|
+
resolve();
|
|
40486
|
+
};
|
|
40487
|
+
const onAbort = () => {
|
|
40488
|
+
const queuedIndex = waiters.indexOf(waiter);
|
|
40489
|
+
if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
|
|
40490
|
+
reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
|
|
40491
|
+
};
|
|
40492
|
+
waiters.push(waiter);
|
|
40493
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40494
|
+
});
|
|
40495
|
+
try {
|
|
40496
|
+
return await task();
|
|
40497
|
+
} finally {
|
|
40498
|
+
releaseSlot();
|
|
40499
|
+
}
|
|
40500
|
+
};
|
|
40399
40501
|
/**
|
|
40400
40502
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
40401
40503
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -40686,14 +40788,17 @@ const checkDeadCode = async (options) => {
|
|
|
40686
40788
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40687
40789
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40688
40790
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40689
|
-
const
|
|
40690
|
-
|
|
40691
|
-
|
|
40692
|
-
|
|
40693
|
-
|
|
40694
|
-
|
|
40695
|
-
|
|
40696
|
-
|
|
40791
|
+
const spawnAndRun = () => {
|
|
40792
|
+
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40793
|
+
rootDirectory,
|
|
40794
|
+
entryPatterns,
|
|
40795
|
+
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40796
|
+
ignorePatterns,
|
|
40797
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40798
|
+
parseConcurrency: options.parseConcurrency
|
|
40799
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
40800
|
+
};
|
|
40801
|
+
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
40697
40802
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40698
40803
|
const diagnostics = [];
|
|
40699
40804
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -41094,43 +41199,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41094
41199
|
* reason: GitInvocationFailed })` so the rest of the codebase
|
|
41095
41200
|
* sees a single failure channel.
|
|
41096
41201
|
*/
|
|
41097
|
-
const runCommand = (input) =>
|
|
41098
|
-
const
|
|
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({
|
|
41202
|
+
const runCommand = (input) => {
|
|
41203
|
+
const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
|
|
41121
41204
|
status: 127,
|
|
41122
41205
|
stdout: "",
|
|
41123
41206
|
stderr: String(cause)
|
|
41124
|
-
})
|
|
41125
|
-
return new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41207
|
+
}) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41126
41208
|
args: [...input.args],
|
|
41127
41209
|
directory: input.directory,
|
|
41128
41210
|
cause
|
|
41129
|
-
}) });
|
|
41130
|
-
|
|
41131
|
-
|
|
41132
|
-
|
|
41133
|
-
|
|
41211
|
+
}) }));
|
|
41212
|
+
return scoped(gen(function* () {
|
|
41213
|
+
if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
|
|
41214
|
+
const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
|
|
41215
|
+
if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
|
|
41216
|
+
const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
|
|
41217
|
+
cwd: input.directory,
|
|
41218
|
+
env: input.env,
|
|
41219
|
+
extendEnv: true
|
|
41220
|
+
}));
|
|
41221
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41222
|
+
const stdoutByteCount = yield* make$13(0);
|
|
41223
|
+
const [stdout, stderr, status] = yield* all([
|
|
41224
|
+
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({
|
|
41225
|
+
args: [...input.args],
|
|
41226
|
+
directory: input.directory,
|
|
41227
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41228
|
+
}) })) : void_)))))),
|
|
41229
|
+
mkString(decodeText(handle.stderr)),
|
|
41230
|
+
handle.exitCode
|
|
41231
|
+
], { concurrency: 3 });
|
|
41232
|
+
return {
|
|
41233
|
+
status,
|
|
41234
|
+
stdout,
|
|
41235
|
+
stderr
|
|
41236
|
+
};
|
|
41237
|
+
})).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
|
|
41238
|
+
"git.command": input.command,
|
|
41239
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41240
|
+
} }));
|
|
41241
|
+
};
|
|
41134
41242
|
const runGit = (directory, args) => runCommand({
|
|
41135
41243
|
command: "git",
|
|
41136
41244
|
args,
|
|
@@ -41163,7 +41271,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41163
41271
|
"rev-parse",
|
|
41164
41272
|
"--verify",
|
|
41165
41273
|
branch
|
|
41166
|
-
]).pipe(map$3((result) => result.status === 0));
|
|
41274
|
+
]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
|
|
41167
41275
|
const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
|
|
41168
41276
|
const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
|
|
41169
41277
|
"merge-base",
|
|
@@ -41377,7 +41485,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41377
41485
|
]);
|
|
41378
41486
|
if (result.status !== 0) return null;
|
|
41379
41487
|
return parseChangedLineRanges(result.stdout);
|
|
41380
|
-
}).pipe(withSpan("Git.changedLineRanges"))
|
|
41488
|
+
}).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
|
|
41381
41489
|
});
|
|
41382
41490
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
41383
41491
|
/**
|
|
@@ -43854,7 +43962,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43854
43962
|
* diagnostics).
|
|
43855
43963
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43856
43964
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43857
|
-
* expo/react-native
|
|
43965
|
+
* expo/react-native), collected synchronously. The heavier
|
|
43966
|
+
* content-regex security scan is forked instead (like supply-chain
|
|
43967
|
+
* below) and joined before the concat, so its CPU overlaps lint
|
|
43968
|
+
* rather than blocking the event loop before it.
|
|
43858
43969
|
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43859
43970
|
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43860
43971
|
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
@@ -43874,7 +43985,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43874
43985
|
* order, so terminal output is identical either way; supply-chain
|
|
43875
43986
|
* rides alongside without a spinner.
|
|
43876
43987
|
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43877
|
-
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
43988
|
+
* FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
|
|
43878
43989
|
* byte-identical regardless of which fiber settled first. The
|
|
43879
43990
|
* viewer-permission fiber is joined later, during score-metadata
|
|
43880
43991
|
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
@@ -43935,12 +44046,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43935
44046
|
...checkPnpmHardening(scanDirectory),
|
|
43936
44047
|
...checkReactServerComponentsAdvisory(scanDirectory, project),
|
|
43937
44048
|
...checkExpoProject(scanDirectory, project),
|
|
43938
|
-
...checkReactNativeProject(scanDirectory, project)
|
|
43939
|
-
...checkSecurityScan(scanDirectory, {
|
|
43940
|
-
project,
|
|
43941
|
-
ignoredTags: input.ignoredTags
|
|
43942
|
-
})
|
|
44049
|
+
...checkReactNativeProject(scanDirectory, project)
|
|
43943
44050
|
])));
|
|
44051
|
+
const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
|
|
44052
|
+
project,
|
|
44053
|
+
ignoredTags: input.ignoredTags
|
|
44054
|
+
})).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
|
|
43944
44055
|
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43945
44056
|
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43946
44057
|
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
@@ -44076,9 +44187,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44076
44187
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44077
44188
|
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44078
44189
|
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
44190
|
+
const securityScanCollected = yield* join(securityScanFiber);
|
|
44079
44191
|
yield* reporterService.finalize;
|
|
44080
44192
|
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
44081
44193
|
...envCollected,
|
|
44194
|
+
...securityScanCollected,
|
|
44082
44195
|
...supplyChainCollected,
|
|
44083
44196
|
...lintCollected,
|
|
44084
44197
|
...deadCodeCollected
|
|
@@ -44839,6 +44952,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
|
|
|
44839
44952
|
const METRIC = {
|
|
44840
44953
|
cliInvoked: "cli.invoked",
|
|
44841
44954
|
cliError: "cli.error",
|
|
44955
|
+
cliEnvironmentError: "cli.env_error",
|
|
44842
44956
|
projectDetected: "project.detected",
|
|
44843
44957
|
projectPathSelected: "project.path_selected",
|
|
44844
44958
|
projectConfigSelected: "project.config_selected",
|
|
@@ -44911,7 +45025,7 @@ const makeNoopConsole = () => ({
|
|
|
44911
45025
|
});
|
|
44912
45026
|
//#endregion
|
|
44913
45027
|
//#region src/cli/utils/version.ts
|
|
44914
|
-
const VERSION = "0.5.8-dev.
|
|
45028
|
+
const VERSION = "0.5.8-dev.5774deb";
|
|
44915
45029
|
//#endregion
|
|
44916
45030
|
//#region src/cli/utils/json-mode.ts
|
|
44917
45031
|
let context = null;
|
|
@@ -45275,13 +45389,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45275
45389
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45276
45390
|
* standard `SENTRY_RELEASE` override.
|
|
45277
45391
|
*/
|
|
45278
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.
|
|
45392
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.5774deb`;
|
|
45279
45393
|
/**
|
|
45280
45394
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45281
45395
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45282
45396
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45283
45397
|
*/
|
|
45284
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.
|
|
45398
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.5774deb") ? "development" : "production");
|
|
45285
45399
|
/**
|
|
45286
45400
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45287
45401
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -49690,6 +49804,7 @@ const CI_PITCH_EVENT = "ci-pitch";
|
|
|
49690
49804
|
const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
|
|
49691
49805
|
const SETUP_HINT_EVENT = "setup-hint";
|
|
49692
49806
|
const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
|
|
49807
|
+
const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
|
|
49693
49808
|
const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
49694
49809
|
for (const [hash, record] of Object.entries(legacy ?? {})) {
|
|
49695
49810
|
const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
|
|
@@ -50353,7 +50468,7 @@ const readPackageJson = (projectRoot) => {
|
|
|
50353
50468
|
return null;
|
|
50354
50469
|
}
|
|
50355
50470
|
};
|
|
50356
|
-
const writeJsonFile
|
|
50471
|
+
const writeJsonFile = (filePath, value) => {
|
|
50357
50472
|
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50358
50473
|
};
|
|
50359
50474
|
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
@@ -51073,7 +51188,8 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
51073
51188
|
return [...await runPromise(gen(function* () {
|
|
51074
51189
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
51075
51190
|
}).pipe(provide(stagedFilesLayer)))];
|
|
51076
|
-
} catch {
|
|
51191
|
+
} catch (error) {
|
|
51192
|
+
cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
|
|
51077
51193
|
return [];
|
|
51078
51194
|
}
|
|
51079
51195
|
};
|
|
@@ -51092,6 +51208,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
|
|
|
51092
51208
|
};
|
|
51093
51209
|
};
|
|
51094
51210
|
//#endregion
|
|
51211
|
+
//#region src/cli/utils/is-environment-error.ts
|
|
51212
|
+
const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
|
|
51213
|
+
const ENVIRONMENT_ERROR_CODES = new Set([
|
|
51214
|
+
"ENOSPC",
|
|
51215
|
+
"EIO",
|
|
51216
|
+
"EROFS",
|
|
51217
|
+
"EACCES",
|
|
51218
|
+
"EPERM",
|
|
51219
|
+
"ENOTDIR"
|
|
51220
|
+
]);
|
|
51221
|
+
const isEnvironmentError = (error) => {
|
|
51222
|
+
if (!isNodeSystemError(error)) return false;
|
|
51223
|
+
if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
|
|
51224
|
+
return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
|
|
51225
|
+
};
|
|
51226
|
+
const formatEnvironmentError = (error) => {
|
|
51227
|
+
if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
|
|
51228
|
+
switch (error.code) {
|
|
51229
|
+
case "ENOSPC": return "No space left on device. Free up disk space and try again.";
|
|
51230
|
+
case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
|
|
51231
|
+
case "EROFS": return "Read-only filesystem: cannot write to this location.";
|
|
51232
|
+
case "EACCES":
|
|
51233
|
+
case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
|
|
51234
|
+
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.";
|
|
51235
|
+
case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
|
|
51236
|
+
default: return error.message;
|
|
51237
|
+
}
|
|
51238
|
+
};
|
|
51239
|
+
//#endregion
|
|
51095
51240
|
//#region src/cli/utils/handle-error.ts
|
|
51096
51241
|
const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
|
|
51097
51242
|
const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
|
|
@@ -51174,15 +51319,19 @@ const handleError = (error, options = {}) => {
|
|
|
51174
51319
|
process.exitCode = 1;
|
|
51175
51320
|
};
|
|
51176
51321
|
/**
|
|
51177
|
-
* Renderer for expected, user-actionable failures — a bad `--diff` value
|
|
51178
|
-
* a base branch that isn't fetched
|
|
51179
|
-
*
|
|
51180
|
-
*
|
|
51322
|
+
* Renderer for expected, user-actionable failures — a bad `--diff` value,
|
|
51323
|
+
* a base branch that isn't fetched, or environment errors like disk-full or
|
|
51324
|
+
* permission-denied. Prints just the (already human-readable) message — no
|
|
51325
|
+
* "Something went wrong", prefilled issue, Discord link, or Sentry reference
|
|
51326
|
+
* — because there is no bug to report.
|
|
51181
51327
|
*/
|
|
51182
51328
|
const handleUserError = (error, options = {}) => {
|
|
51329
|
+
const isEnvError = isEnvironmentError(error);
|
|
51330
|
+
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
|
|
51331
|
+
const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
|
|
51183
51332
|
runSync(gen(function* () {
|
|
51184
51333
|
yield* error$1("");
|
|
51185
|
-
yield* error$1(highlighter.error(
|
|
51334
|
+
yield* error$1(highlighter.error(message));
|
|
51186
51335
|
yield* error$1("");
|
|
51187
51336
|
}));
|
|
51188
51337
|
if (options.shouldExit !== false) process.exit(1);
|
|
@@ -51197,7 +51346,7 @@ const handleUserError = (error, options = {}) => {
|
|
|
51197
51346
|
* `handleUserError` (a plain message — no "Something went wrong", prefilled
|
|
51198
51347
|
* issue, Discord link, or Sentry reference), since there is no bug to report.
|
|
51199
51348
|
*
|
|
51200
|
-
*
|
|
51349
|
+
* Four distinct shapes reach the CLI's catch blocks:
|
|
51201
51350
|
*
|
|
51202
51351
|
* - **Project-discovery failures** (`NoReactDependencyError`,
|
|
51203
51352
|
* `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
|
|
@@ -51210,12 +51359,19 @@ const handleUserError = (error, options = {}) => {
|
|
|
51210
51359
|
* `--project` name.
|
|
51211
51360
|
* - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
|
|
51212
51361
|
* stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
|
|
51362
|
+
* - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
|
|
51363
|
+
* `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
|
|
51364
|
+
* full / failing / read-only, permission denied, or a path blocked by a
|
|
51365
|
+
* file. React Doctor cannot fix the user's environment; exit cleanly with an
|
|
51366
|
+
* actionable message instead of crashing. See `is-environment-error.ts` for
|
|
51367
|
+
* why the set stays narrow (codes that usually mean our bug keep reaching
|
|
51368
|
+
* Sentry).
|
|
51213
51369
|
*
|
|
51214
51370
|
* This composes the existing core narrowers rather than introducing a new
|
|
51215
51371
|
* error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
|
|
51216
51372
|
* knowledge of the `ReactDoctorError` shape.
|
|
51217
51373
|
*/
|
|
51218
|
-
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51374
|
+
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51219
51375
|
//#endregion
|
|
51220
51376
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
51221
51377
|
const buildHandoffPayload = (input) => {
|
|
@@ -51296,6 +51452,20 @@ const detectAvailableAgents = async () => {
|
|
|
51296
51452
|
const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
|
|
51297
51453
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
51298
51454
|
};
|
|
51455
|
+
const DEFAULT_INSTALL_AGENTS = [
|
|
51456
|
+
"claude-code",
|
|
51457
|
+
"cursor",
|
|
51458
|
+
"codex",
|
|
51459
|
+
"opencode"
|
|
51460
|
+
];
|
|
51461
|
+
const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
|
|
51462
|
+
const detected = new Set(detectedAgents);
|
|
51463
|
+
const remembered = rememberedAgents.filter((agent) => detected.has(agent));
|
|
51464
|
+
if (remembered.length > 0) return remembered;
|
|
51465
|
+
const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
|
|
51466
|
+
if (defaults.length > 0) return defaults;
|
|
51467
|
+
return detectedAgents.length === 1 ? [...detectedAgents] : [];
|
|
51468
|
+
};
|
|
51299
51469
|
//#endregion
|
|
51300
51470
|
//#region src/cli/utils/install-doctor-script.ts
|
|
51301
51471
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
@@ -51377,7 +51547,7 @@ const installDoctorScript = (options) => {
|
|
|
51377
51547
|
};
|
|
51378
51548
|
})();
|
|
51379
51549
|
const scriptStatus = scriptTarget.status;
|
|
51380
|
-
if (scriptStatus === "created") writeJsonFile
|
|
51550
|
+
if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
|
|
51381
51551
|
...packageJson,
|
|
51382
51552
|
scripts: {
|
|
51383
51553
|
...isRecord$1(scripts) ? scripts : {},
|
|
@@ -51948,6 +52118,19 @@ const setUpGitHubActions = async (options) => {
|
|
|
51948
52118
|
return didCreateWorkflow;
|
|
51949
52119
|
};
|
|
51950
52120
|
//#endregion
|
|
52121
|
+
//#region src/cli/utils/install-agents-preference.ts
|
|
52122
|
+
const INSTALL_AGENTS_PREFERENCE = {
|
|
52123
|
+
id: INSTALL_AGENTS_PREFERENCE_ID,
|
|
52124
|
+
scope: "global"
|
|
52125
|
+
};
|
|
52126
|
+
const PREFERENCE_SEPARATOR = ",";
|
|
52127
|
+
const readInstallAgents = (options = {}) => {
|
|
52128
|
+
const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
|
|
52129
|
+
if (stored === null) return [];
|
|
52130
|
+
return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
|
|
52131
|
+
};
|
|
52132
|
+
const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
|
|
52133
|
+
//#endregion
|
|
51951
52134
|
//#region src/cli/utils/install-agent-hooks.ts
|
|
51952
52135
|
const CLAUDE_AGENT = "claude-code";
|
|
51953
52136
|
const CURSOR_AGENT = "cursor";
|
|
@@ -51958,20 +52141,34 @@ const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
|
|
|
51958
52141
|
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.sh";
|
|
51959
52142
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
51960
52143
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
51961
|
-
const JSON_INDENT_SPACES$1 = 2;
|
|
51962
52144
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
51963
52145
|
const readJsonFile = (filePath, fallback) => {
|
|
51964
52146
|
if (!NFS.existsSync(filePath)) return fallback;
|
|
51965
52147
|
const content = NFS.readFileSync(filePath, "utf8").trim();
|
|
51966
52148
|
if (content.length === 0) return fallback;
|
|
51967
|
-
|
|
52149
|
+
try {
|
|
52150
|
+
return JSON.parse(content);
|
|
52151
|
+
} catch (error) {
|
|
52152
|
+
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.`);
|
|
52153
|
+
throw error;
|
|
52154
|
+
}
|
|
51968
52155
|
};
|
|
51969
|
-
const
|
|
51970
|
-
|
|
51971
|
-
|
|
52156
|
+
const ensureDirectoryExists = (directoryPath) => {
|
|
52157
|
+
try {
|
|
52158
|
+
NFS.mkdirSync(directoryPath, { recursive: true });
|
|
52159
|
+
} catch (error) {
|
|
52160
|
+
const code = error.code;
|
|
52161
|
+
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.`);
|
|
52162
|
+
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.`);
|
|
52163
|
+
throw error;
|
|
52164
|
+
}
|
|
52165
|
+
};
|
|
52166
|
+
const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
52167
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
52168
|
+
writeJsonFile(filePath, value);
|
|
51972
52169
|
};
|
|
51973
52170
|
const writeHookScript = (filePath) => {
|
|
51974
|
-
|
|
52171
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
51975
52172
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
51976
52173
|
NFS.chmodSync(filePath, 493);
|
|
51977
52174
|
};
|
|
@@ -51987,7 +52184,7 @@ const installClaudeHook = (projectRoot) => {
|
|
|
51987
52184
|
command: CLAUDE_HOOK_COMMAND
|
|
51988
52185
|
}] });
|
|
51989
52186
|
hooks.PostToolBatch = postToolBatchHooks;
|
|
51990
|
-
|
|
52187
|
+
writeJsonFileWithDirectoryCheck(settingsPath, {
|
|
51991
52188
|
...settings,
|
|
51992
52189
|
hooks
|
|
51993
52190
|
});
|
|
@@ -52007,7 +52204,7 @@ const installCursorHook = (projectRoot) => {
|
|
|
52007
52204
|
timeout: 120
|
|
52008
52205
|
});
|
|
52009
52206
|
hooks.postToolUse = postToolUseHooks;
|
|
52010
|
-
|
|
52207
|
+
writeJsonFileWithDirectoryCheck(configPath, {
|
|
52011
52208
|
...config,
|
|
52012
52209
|
version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
|
|
52013
52210
|
hooks
|
|
@@ -52243,7 +52440,7 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
52243
52440
|
parent = cloned;
|
|
52244
52441
|
}
|
|
52245
52442
|
parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
|
|
52246
|
-
writeJsonFile
|
|
52443
|
+
writeJsonFile(packageJsonPath, nextPackageJson);
|
|
52247
52444
|
removeLegacyManagedRunner(options.projectRoot);
|
|
52248
52445
|
return {
|
|
52249
52446
|
hookPath: packageJsonPath,
|
|
@@ -52826,6 +53023,9 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
52826
53023
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
52827
53024
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
52828
53025
|
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
53026
|
+
const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
|
|
53027
|
+
const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
|
|
53028
|
+
const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
|
|
52829
53029
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
52830
53030
|
type: "multiselect",
|
|
52831
53031
|
name: "agents",
|
|
@@ -52833,12 +53033,13 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
52833
53033
|
choices: detectedAgents.map((agent) => ({
|
|
52834
53034
|
title: getSkillAgentConfig(agent).displayName,
|
|
52835
53035
|
value: agent,
|
|
52836
|
-
selected:
|
|
53036
|
+
selected: defaultSelectedAgents.includes(agent)
|
|
52837
53037
|
})),
|
|
52838
53038
|
instructions: false,
|
|
52839
53039
|
min: 1
|
|
52840
53040
|
}, promptOptions)).agents ?? [];
|
|
52841
53041
|
if (selectedAgents.length === 0) return;
|
|
53042
|
+
if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
|
|
52842
53043
|
let dependencyResult;
|
|
52843
53044
|
if (!options.dryRun) {
|
|
52844
53045
|
await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
|
|
@@ -52897,6 +53098,8 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
52897
53098
|
}
|
|
52898
53099
|
recordCount(METRIC.installCompleted, 1, {
|
|
52899
53100
|
agentsCount: selectedAgents.length,
|
|
53101
|
+
agentsDetected: detectedAgents.length,
|
|
53102
|
+
usedRememberedAgents,
|
|
52900
53103
|
gitHook: shouldInstallGitHook,
|
|
52901
53104
|
agentHooks: shouldInstallAgentHooks,
|
|
52902
53105
|
workflow: didInstallWorkflow,
|
|
@@ -53676,7 +53879,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
|
|
|
53676
53879
|
};
|
|
53677
53880
|
const warnDiffUnavailable = (requested, isQuiet) => {
|
|
53678
53881
|
if (isQuiet) return;
|
|
53679
|
-
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (
|
|
53882
|
+
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
53883
|
else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
|
|
53681
53884
|
cliLogger.break();
|
|
53682
53885
|
};
|
|
@@ -53873,6 +54076,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
|
|
|
53873
54076
|
return requestedNames.map((requestedName) => {
|
|
53874
54077
|
const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
|
|
53875
54078
|
if (matched) return matched.directory;
|
|
54079
|
+
if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
|
|
53876
54080
|
const candidateDirectory = Path.resolve(rootDirectory, requestedName);
|
|
53877
54081
|
if (isDirectory(candidateDirectory)) {
|
|
53878
54082
|
recordCount(METRIC.projectPathSelected);
|
|
@@ -54367,6 +54571,10 @@ const installAction = async (options, command) => {
|
|
|
54367
54571
|
projectRoot: options.cwd ?? process.cwd()
|
|
54368
54572
|
});
|
|
54369
54573
|
} catch (error) {
|
|
54574
|
+
if (isExpectedUserError(error)) {
|
|
54575
|
+
handleUserError(error);
|
|
54576
|
+
return;
|
|
54577
|
+
}
|
|
54370
54578
|
handleError(error, { sentryEventId: await reportErrorToSentry(error) });
|
|
54371
54579
|
}
|
|
54372
54580
|
};
|
|
@@ -55374,4 +55582,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
55374
55582
|
export {};
|
|
55375
55583
|
|
|
55376
55584
|
//# sourceMappingURL=cli.js.map
|
|
55377
|
-
//# debugId=
|
|
55585
|
+
//# debugId=498477a7-1f41-5ecb-9999-c48cfef57259
|