react-doctor 0.9.7-dev.13138a4 → 0.9.7

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.
@@ -1,1154 +0,0 @@
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]="37d51c00-658e-5212-9cfb-51a98a4a4ded")}catch(e){}}();
3
- import { N as CliInputError, _ as CI_TRUST_COMPANIES, en as highlighter, et as GITHUB_ACTIONS_SETUP_URL, nn as isErrnoException, t as recordCount, vt as SKILL_NAME, y as METRIC } from "./record-metric-CtlQma6P.js";
4
- import { A as packageHasNestedRecordKey, D as getPackageJsonPath, E as ensureTrailingNewline, I as writeJsonFile, L as cliLogger, M as readPackageJson, N as resolveGitPath, O as isRecord, P as runGit, S as REACT_DOCTOR_COMMAND, T as VITE_PLUS_HOOKS_PATH, _ as LEFTHOOK_CONFIG_FILES, a as installReactDoctorScriptStep, b as OVERCOMMIT_CONFIG_FILE, c as spinner, g as HUSKY_HOOKS_PATH, h as HOOK_RELATIVE_PATH, j as packageHasRecordKey, k as packageHasDependency, m as HOOK_FILE_NAME, n as findNearestPackageDirectory, r as hasDoctorDependency, t as DOCTOR_PACKAGE_NAME, v as LEGACY_HOOK_RUNNER_RELATIVE_PATH, x as PRE_COMMIT_CONFIG_FILE, y as NON_BLOCKING_REACT_DOCTOR_COMMAND } from "./install-doctor-script-DxrteuEw.js";
5
- import { a as readReactDoctorWorkflow, c as workflowUsesV1Action, d as detectDefaultBranch, l as prompts, n as getReactDoctorWorkflowPath, o as upgradeReactDoctorWorkflowInPlace, r as installReactDoctorWorkflow } from "./install-github-workflow-BP6t3YBA.js";
6
- import { t as shouldSkipPrompts } from "./should-skip-prompts-CbIIaU8b.js";
7
- import { a as recordGate, c as ACTION_UPGRADE_EVENT, d as INSTALL_AGENTS_PREFERENCE_ID, h as detectAvailableAgents, i as readPreference, l as CI_PITCH_EVENT, m as computeDefaultSelectedAgents, n as openUrl, r as isGatePending, s as writePreference, t as reportWorkflowResult } from "./report-workflow-result-D3GCowCN.js";
8
- import { execFile } from "node:child_process";
9
- import * as path$1 from "node:path";
10
- import * as fs$1 from "node:fs";
11
- import { fileURLToPath } from "node:url";
12
- import { promisify } from "node:util";
13
- import { SKILL_MANIFEST_FILE, getSkillAgentConfig, installSkillsFromSource, isSkillAgentType } from "agent-install";
14
- //#region src/cli/utils/action-upgrade-prompt.ts
15
- const ACTION_UPGRADE_GATE = {
16
- id: ACTION_UPGRADE_EVENT,
17
- scope: "project"
18
- };
19
- const hasHandledActionUpgrade = (projectRoot, options = {}) => !isGatePending(ACTION_UPGRADE_GATE, { projectRoot }, options);
20
- const recordActionUpgradeDecision = (projectRoot, outcome, options = {}) => recordGate(ACTION_UPGRADE_GATE, {
21
- projectRoot,
22
- outcome
23
- }, options);
24
- //#endregion
25
- //#region src/cli/utils/ci-prompt-decision.ts
26
- const CI_PITCH_GATE = {
27
- id: CI_PITCH_EVENT,
28
- scope: "project"
29
- };
30
- const hasHandledCiPrompt = (projectRoot, options = {}) => !isGatePending(CI_PITCH_GATE, { projectRoot }, options);
31
- const recordCiPromptDecision = (projectRoot, outcome, options = {}) => recordGate(CI_PITCH_GATE, {
32
- projectRoot,
33
- outcome
34
- }, options);
35
- //#endregion
36
- //#region src/cli/utils/ask-add-to-github-actions.ts
37
- const CI_YES_CHOICE = "ci-yes";
38
- const CI_LEARN_MORE_CHOICE = "ci-learn-more";
39
- const CI_NO_CHOICE = "ci-no";
40
- const SGR_BOLD_OFF = "\x1B[22m";
41
- const ciQuestionMessage = [
42
- "Add React Doctor to GitHub Actions?",
43
- `${SGR_BOLD_OFF} ${highlighter.dim("Scan every pull request to prevent new React issues while you fix the backlog.")}`,
44
- `${SGR_BOLD_OFF} ${highlighter.dim(`Used by teams at ${CI_TRUST_COMPANIES}.`)}`
45
- ].join("\n");
46
- const askAddToGitHubActions = async (prompt = prompts) => {
47
- while (true) {
48
- const { ciChoice } = await prompt({
49
- type: "select",
50
- name: "ciChoice",
51
- message: ciQuestionMessage,
52
- hint: " ",
53
- choices: [
54
- {
55
- title: "Yes",
56
- description: "Adds the workflow file and a doctor package script",
57
- value: CI_YES_CHOICE
58
- },
59
- {
60
- title: "Learn more",
61
- description: "Opens the docs page in your browser",
62
- value: CI_LEARN_MORE_CHOICE
63
- },
64
- {
65
- title: "No",
66
- description: "Skip for now",
67
- value: CI_NO_CHOICE
68
- }
69
- ],
70
- initial: 0
71
- }, { onCancel: () => true });
72
- if (ciChoice === void 0) return "cancel";
73
- if (ciChoice === CI_YES_CHOICE) return "yes";
74
- if (ciChoice === CI_NO_CHOICE) return "no";
75
- const opened = await openUrl(GITHUB_ACTIONS_SETUP_URL);
76
- cliLogger.log(opened ? `Opened ${highlighter.info(GITHUB_ACTIONS_SETUP_URL)} in your browser.` : `Visit ${highlighter.info(GITHUB_ACTIONS_SETUP_URL)} to learn more.`);
77
- cliLogger.break();
78
- }
79
- };
80
- //#endregion
81
- //#region src/cli/utils/ask-upgrade-action-version.ts
82
- const UPGRADE_YES_CHOICE = "upgrade-yes";
83
- const UPGRADE_NO_CHOICE = "upgrade-no";
84
- const askUpgradeActionVersion = async (prompt = prompts) => {
85
- const { upgradeChoice } = await prompt({
86
- type: "select",
87
- name: "upgradeChoice",
88
- message: "A new major of the React Doctor Action (v2) is out. Upgrade this repo's workflow?",
89
- hint: " ",
90
- choices: [{
91
- title: "Yes (recommended)",
92
- description: "Bump the workflow to millionco/react-doctor@v2",
93
- value: UPGRADE_YES_CHOICE
94
- }, {
95
- title: "No, thanks",
96
- description: "Keep @v1 — won't ask again for this repo",
97
- value: UPGRADE_NO_CHOICE
98
- }],
99
- initial: 0
100
- }, { onCancel: () => true });
101
- if (upgradeChoice === void 0) return "cancel";
102
- return upgradeChoice === UPGRADE_YES_CHOICE ? "yes" : "no";
103
- };
104
- //#endregion
105
- //#region src/cli/utils/install-agents-preference.ts
106
- const INSTALL_AGENTS_PREFERENCE = {
107
- id: INSTALL_AGENTS_PREFERENCE_ID,
108
- scope: "global"
109
- };
110
- const PREFERENCE_SEPARATOR = ",";
111
- const readInstallAgents = (options = {}) => {
112
- const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
113
- if (stored === null) return [];
114
- return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
115
- };
116
- const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
117
- //#endregion
118
- //#region src/cli/utils/install-agent-hooks.ts
119
- const CLAUDE_AGENT = "claude-code";
120
- const CURSOR_AGENT = "cursor";
121
- const CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
122
- const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.mjs";
123
- const CLAUDE_HOOK_COMMAND = "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.mjs\"";
124
- const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
125
- const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
126
- const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
127
- const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
128
- const CURSOR_HOOKS_SCHEMA_VERSION = 1;
129
- const LEGACY_HOOK_SCRIPT_PATHS = [".claude/hooks/react-doctor.sh", ".cursor/hooks/react-doctor.sh"];
130
- const isLegacyHookCommand = (command) => typeof command === "string" && LEGACY_HOOK_SCRIPT_PATHS.some((legacyPath) => command.includes(legacyPath));
131
- const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
132
- const readJsonFile = (filePath, fallback) => {
133
- if (!fs$1.existsSync(filePath)) return fallback;
134
- const content = fs$1.readFileSync(filePath, "utf8").trim();
135
- if (content.length === 0) return fallback;
136
- try {
137
- return JSON.parse(content);
138
- } catch (error) {
139
- 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.`);
140
- throw error;
141
- }
142
- };
143
- const readJsonFileSafely = (filePath, fallback) => {
144
- try {
145
- return readJsonFile(filePath, fallback);
146
- } catch {
147
- return fallback;
148
- }
149
- };
150
- const findAgentsWithLegacyShellHooks = (projectRoot) => {
151
- const agents = [];
152
- if ((readJsonFileSafely(path$1.join(projectRoot, CLAUDE_SETTINGS_RELATIVE_PATH), {}).hooks?.PostToolBatch ?? []).some((group) => (group.hooks ?? []).some((hook) => isLegacyHookCommand(hook.command)))) agents.push(CLAUDE_AGENT);
153
- if ((readJsonFileSafely(path$1.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH), {}).hooks?.postToolUse ?? []).some((handler) => isLegacyHookCommand(handler.command))) agents.push(CURSOR_AGENT);
154
- return agents;
155
- };
156
- const ensureDirectoryExists = (directoryPath) => {
157
- try {
158
- fs$1.mkdirSync(directoryPath, { recursive: true });
159
- } catch (error) {
160
- const code = isErrnoException(error) ? error.code : void 0;
161
- 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.`);
162
- 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.`);
163
- throw error;
164
- }
165
- };
166
- const writeJsonFileWithDirectoryCheck = (filePath, value) => {
167
- ensureDirectoryExists(path$1.dirname(filePath));
168
- writeJsonFile(filePath, value);
169
- };
170
- const writeHookScript = (filePath) => {
171
- ensureDirectoryExists(path$1.dirname(filePath));
172
- fs$1.writeFileSync(filePath, buildAgentHookScript());
173
- try {
174
- fs$1.rmSync(filePath.replace(/\.mjs$/, ".sh"), { force: true });
175
- } catch {}
176
- };
177
- const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
178
- const installClaudeHook = (projectRoot) => {
179
- const settingsPath = path$1.join(projectRoot, CLAUDE_SETTINGS_RELATIVE_PATH);
180
- const hookPath = path$1.join(projectRoot, CLAUDE_HOOK_RELATIVE_PATH);
181
- const settings = readJsonFile(settingsPath, {});
182
- const hooks = { ...settings.hooks ?? {} };
183
- const postToolBatchHooks = (hooks.PostToolBatch ?? []).flatMap((group) => {
184
- const groupHooks = group.hooks ?? [];
185
- const keptHooks = groupHooks.filter((hook) => !isLegacyHookCommand(hook.command));
186
- if (keptHooks.length === groupHooks.length) return [group];
187
- return keptHooks.length > 0 ? [{
188
- ...group,
189
- hooks: keptHooks
190
- }] : [];
191
- });
192
- if (!hasClaudeHookCommand(postToolBatchHooks)) postToolBatchHooks.push({ hooks: [{
193
- type: "command",
194
- command: CLAUDE_HOOK_COMMAND
195
- }] });
196
- hooks.PostToolBatch = postToolBatchHooks;
197
- writeJsonFileWithDirectoryCheck(settingsPath, {
198
- ...settings,
199
- hooks
200
- });
201
- writeHookScript(hookPath);
202
- return [settingsPath, hookPath];
203
- };
204
- const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command === CURSOR_HOOK_COMMAND);
205
- const installCursorHook = (projectRoot) => {
206
- const configPath = path$1.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH);
207
- const hookPath = path$1.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
208
- const config = readJsonFile(configPath, {});
209
- const hooks = { ...config.hooks ?? {} };
210
- const postToolUseHooks = (hooks.postToolUse ?? []).filter((handler) => !isLegacyHookCommand(handler.command));
211
- if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
212
- command: CURSOR_HOOK_COMMAND,
213
- matcher: CURSOR_HOOK_MATCHER,
214
- timeout: 120
215
- });
216
- hooks.postToolUse = postToolUseHooks;
217
- writeJsonFileWithDirectoryCheck(configPath, {
218
- ...config,
219
- version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
220
- hooks
221
- });
222
- writeHookScript(hookPath);
223
- return [configPath, hookPath];
224
- };
225
- const buildAgentHookScript = () => [
226
- "import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
227
- "import { tmpdir } from 'node:os';",
228
- "import { join, dirname } from 'node:path';",
229
- "import { fileURLToPath } from 'node:url';",
230
- "import { spawnSync } from 'node:child_process';",
231
- "",
232
- "const __filename = fileURLToPath(import.meta.url);",
233
- "const __dirname = dirname(__filename);",
234
- "",
235
- "// --verbose scans on large diffs can exceed spawnSync's 1 MiB default.",
236
- "const SPAWN_MAX_BUFFER_BYTES = 16 * 1024 * 1024;",
237
- "",
238
- "const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
239
- "",
240
- "const readFileOrEmpty = (source) => {",
241
- " try {",
242
- " return readFileSync(source, 'utf8');",
243
- " } catch {",
244
- " return '';",
245
- " }",
246
- "};",
247
- "",
248
- "const shouldScan = (input) => {",
249
- " const eventName = input.hook_event_name || input.eventName || input.event_name;",
250
- " if (eventName === 'PostToolBatch') {",
251
- " const toolCalls = Array.isArray(input.tool_calls) ? input.tool_calls : [];",
252
- " return toolCalls.some((toolCall) => EDIT_TOOL_NAMES.has(toolCall.tool_name));",
253
- " }",
254
- " const toolName = input.tool_name || input.toolName || input.tool;",
255
- " return !toolName || EDIT_TOOL_NAMES.has(toolName);",
256
- "};",
257
- "",
258
- "const runReactDoctor = (outputPath) => {",
259
- " // Each candidate is a single shell command string (not an args array):",
260
- " // `shell: true` is required to run the Windows `.cmd` shims, and an args",
261
- " // array with `shell: true` trips Node's DEP0190. A missing command exits",
262
- " // 127 via a POSIX shell (no ENOENT error) and 9009 via cmd.exe, so fall",
263
- " // through on those. The local bin is probed with existsSync (its `./`",
264
- " // prefix form is not runnable by cmd.exe at all). With no runner found,",
265
- " // exit 0 silently — stdout is parsed as the hook's JSON.",
266
- " const localBin = process.platform === 'win32'",
267
- " ? 'node_modules\\\\.bin\\\\react-doctor.cmd'",
268
- " : './node_modules/.bin/react-doctor';",
269
- " const commands = [",
270
- " ...(existsSync(localBin)",
271
- " ? [localBin + ' --verbose --scope changed --blocking warning --no-score']",
272
- " : []),",
273
- " 'react-doctor --verbose --scope changed --blocking warning --no-score',",
274
- " 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
275
- " 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
276
- " ];",
277
- "",
278
- " for (const command of commands) {",
279
- " const result = spawnSync(command, { encoding: 'utf8', shell: true, maxBuffer: SPAWN_MAX_BUFFER_BYTES });",
280
- " if (result.error?.code === 'ENOENT' || result.status === 127 || result.status === 9009) continue;",
281
- " try {",
282
- " writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
283
- " } catch {}",
284
- " return result.status;",
285
- " }",
286
- "",
287
- " return 0;",
288
- "};",
289
- "",
290
- "const cleanup = (...paths) => {",
291
- " for (const path of paths) {",
292
- " try { unlinkSync(path); } catch {}",
293
- " }",
294
- "};",
295
- "",
296
- "const main = () => {",
297
- " let input;",
298
- " try {",
299
- " input = JSON.parse(readFileOrEmpty(0) || '{}');",
300
- " } catch {",
301
- " input = {};",
302
- " }",
303
- "",
304
- " if (!shouldScan(input)) {",
305
- " process.exit(0);",
306
- " }",
307
- "",
308
- " const projectRoot = process.env.CLAUDE_PROJECT_DIR || join(__dirname, '../..');",
309
- " const outputPath = join(tmpdir(), `react-doctor-agent-hook-output-${process.pid}.txt`);",
310
- "",
311
- " try {",
312
- " process.chdir(projectRoot);",
313
- " } catch {",
314
- " process.exit(0);",
315
- " }",
316
- "",
317
- " const scanResult = runReactDoctor(outputPath);",
318
- " if (scanResult === 0) {",
319
- " cleanup(outputPath);",
320
- " process.exit(0);",
321
- " }",
322
- "",
323
- " // The write above is best-effort (unwritable tmpdir), so the read is too",
324
- " // — a hook must never crash the agent loop with a stack trace.",
325
- " const scanOutput = readFileOrEmpty(outputPath).trim();",
326
- " cleanup(outputPath);",
327
- "",
328
- " if (!scanOutput) {",
329
- " process.exit(0);",
330
- " }",
331
- "",
332
- " const message = `React Doctor found issues in the changed files. Review this output and fix the regressions before finishing. For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.\\n\\n${scanOutput}`;",
333
- "",
334
- " if (input.hook_event_name === 'PostToolBatch') {",
335
- " console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
336
- " } else {",
337
- " console.log(JSON.stringify({ additional_context: message }));",
338
- " }",
339
- "};",
340
- "",
341
- "main();"
342
- ].join("\n");
343
- const installReactDoctorAgentHooks = (options) => {
344
- const installedAgents = [];
345
- const files = [];
346
- const requestedAgents = options.agents.filter(isSupportedAgent);
347
- for (const agent of requestedAgents) {
348
- if (agent === CLAUDE_AGENT) {
349
- files.push(...installClaudeHook(options.projectRoot));
350
- installedAgents.push(agent);
351
- }
352
- if (agent === CURSOR_AGENT) {
353
- files.push(...installCursorHook(options.projectRoot));
354
- installedAgents.push(agent);
355
- }
356
- }
357
- return {
358
- installedAgents,
359
- files
360
- };
361
- };
362
- //#endregion
363
- //#region src/cli/utils/install-git-hook-file.ts
364
- const REACT_DOCTOR_BLOCK_START = "# react-doctor hook start";
365
- const REACT_DOCTOR_BLOCK_END = "# react-doctor hook end";
366
- const REACT_DOCTOR_BLOCK_PATTERN_SOURCE = `(?:${REACT_DOCTOR_BLOCK_START}[\\s\\S]*?${REACT_DOCTOR_BLOCK_END}\\n?|# react-doctor hook launcher start[\\s\\S]*?# react-doctor hook launcher end\\n?)`;
367
- const REACT_DOCTOR_BLOCK_PATTERN = new RegExp(REACT_DOCTOR_BLOCK_PATTERN_SOURCE);
368
- const ALL_REACT_DOCTOR_BLOCKS_PATTERN = new RegExp(REACT_DOCTOR_BLOCK_PATTERN_SOURCE, "g");
369
- const SHEBANG = "#!/bin/sh";
370
- const SHEBANG_PREFIX = "#!";
371
- const LOCAL_REACT_DOCTOR_BIN = "./node_modules/.bin/react-doctor";
372
- const PNPM_REACT_DOCTOR_COMMAND = "pnpm dlx react-doctor@latest --staged --blocking warning";
373
- const NPX_REACT_DOCTOR_COMMAND = "npx --yes react-doctor@latest --staged --blocking warning";
374
- const buildReactDoctorHookBlock = () => [
375
- REACT_DOCTOR_BLOCK_START,
376
- "react_doctor_scan_staged_files() {",
377
- ` if [ -x "${LOCAL_REACT_DOCTOR_BIN}" ]; then`,
378
- ` "${LOCAL_REACT_DOCTOR_BIN}" ${REACT_DOCTOR_COMMAND.replace("react-doctor ", "")}`,
379
- " return",
380
- " fi",
381
- "",
382
- " if command -v react-doctor >/dev/null 2>&1; then",
383
- ` ${REACT_DOCTOR_COMMAND}`,
384
- " return",
385
- " fi",
386
- "",
387
- " if command -v pnpm >/dev/null 2>&1; then",
388
- ` ${PNPM_REACT_DOCTOR_COMMAND}`,
389
- " return",
390
- " fi",
391
- "",
392
- " if command -v npx >/dev/null 2>&1; then",
393
- ` ${NPX_REACT_DOCTOR_COMMAND}`,
394
- " return",
395
- " fi",
396
- "",
397
- " printf '%s\\n' \"react-doctor: command not found; skipping staged scan.\"",
398
- "}",
399
- "",
400
- "react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\")",
401
- "if react_doctor_scan_staged_files > \"$react_doctor_output\" 2>&1; then",
402
- " rm -f \"$react_doctor_output\"",
403
- "else",
404
- " cat \"$react_doctor_output\" >&2",
405
- " rm -f \"$react_doctor_output\"",
406
- ` printf '%s\\n' "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2`,
407
- "fi",
408
- REACT_DOCTOR_BLOCK_END
409
- ].join("\n");
410
- const mergeHookContent = (existingContent) => {
411
- const hookBlock = `${buildReactDoctorHookBlock()}\n`;
412
- if (REACT_DOCTOR_BLOCK_PATTERN.test(existingContent)) {
413
- let isFirstManagedBlock = true;
414
- return ensureTrailingNewline(existingContent.replace(ALL_REACT_DOCTOR_BLOCKS_PATTERN, () => {
415
- if (isFirstManagedBlock) {
416
- isFirstManagedBlock = false;
417
- return hookBlock;
418
- }
419
- return "";
420
- }));
421
- }
422
- if (existingContent.length === 0) return `${SHEBANG}\n\n${hookBlock}`;
423
- const normalizedExistingContent = ensureTrailingNewline(existingContent);
424
- if (normalizedExistingContent.startsWith(SHEBANG_PREFIX)) {
425
- const [shebangLine, ...remainingLines] = normalizedExistingContent.split("\n");
426
- return [
427
- shebangLine,
428
- "",
429
- hookBlock.trimEnd(),
430
- ...remainingLines
431
- ].join("\n");
432
- }
433
- return `${SHEBANG}\n\n${hookBlock}${normalizedExistingContent}`;
434
- };
435
- const removeLegacyManagedRunner = (projectRoot) => {
436
- const runnerPath = path$1.join(projectRoot, LEGACY_HOOK_RUNNER_RELATIVE_PATH);
437
- fs$1.rmSync(runnerPath, { force: true });
438
- for (const directory of [path$1.dirname(runnerPath), path$1.join(projectRoot, ".react-doctor")]) try {
439
- fs$1.rmdirSync(directory);
440
- } catch {}
441
- };
442
- const installDirectGitHook = (options) => {
443
- const didHookExist = fs$1.existsSync(options.hookPath);
444
- const nextContent = mergeHookContent(didHookExist ? fs$1.readFileSync(options.hookPath, "utf8") : "");
445
- if (options.hooksPathConfig !== void 0) runGit(options.projectRoot, [
446
- "config",
447
- "core.hooksPath",
448
- options.hooksPathConfig
449
- ]);
450
- fs$1.mkdirSync(path$1.dirname(options.hookPath), { recursive: true });
451
- fs$1.writeFileSync(options.hookPath, nextContent);
452
- fs$1.chmodSync(options.hookPath, 493);
453
- removeLegacyManagedRunner(options.projectRoot);
454
- return {
455
- hookPath: options.hookPath,
456
- kind: options.kind ?? "git",
457
- status: didHookExist ? "updated" : "created"
458
- };
459
- };
460
- //#endregion
461
- //#region src/cli/utils/install-git-hook-config-managers.ts
462
- const appendStringCommand = (existingCommand) => {
463
- const existingCommandText = typeof existingCommand === "string" ? existingCommand : Array.isArray(existingCommand) ? existingCommand.filter((entry) => typeof entry === "string").join("\n") : "";
464
- return existingCommandText.includes("react-doctor --staged --blocking warning") ? existingCommandText : [existingCommandText, NON_BLOCKING_REACT_DOCTOR_COMMAND].filter(Boolean).join("\n");
465
- };
466
- const appendArrayCommand = (existingCommands) => {
467
- const commands = Array.isArray(existingCommands) ? existingCommands.filter((entry) => typeof entry === "string") : typeof existingCommands === "string" ? [existingCommands] : [];
468
- return commands.some((command) => command.includes("react-doctor --staged --blocking warning")) ? commands : [...commands, NON_BLOCKING_REACT_DOCTOR_COMMAND];
469
- };
470
- const installPackageJsonHook = (options, strategy) => {
471
- const packageJsonPath = getPackageJsonPath(options.projectRoot);
472
- const didHookExist = fs$1.existsSync(packageJsonPath);
473
- const packageJson = readPackageJson(options.projectRoot);
474
- const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
475
- const parentKeys = strategy.path.slice(0, -1);
476
- const leafKey = strategy.path[strategy.path.length - 1];
477
- let parent = nextPackageJson;
478
- for (const key of parentKeys) {
479
- const existing = parent[key];
480
- const cloned = isRecord(existing) ? { ...existing } : {};
481
- parent[key] = cloned;
482
- parent = cloned;
483
- }
484
- parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
485
- writeJsonFile(packageJsonPath, nextPackageJson);
486
- removeLegacyManagedRunner(options.projectRoot);
487
- return {
488
- hookPath: packageJsonPath,
489
- kind: strategy.kind,
490
- status: didHookExist ? "updated" : "created"
491
- };
492
- };
493
- const installSimpleGitHooks = (options) => installPackageJsonHook(options, {
494
- kind: "simple-git-hooks",
495
- path: ["simple-git-hooks", "pre-commit"],
496
- leafShape: "string"
497
- });
498
- const installGhooks = (options) => installPackageJsonHook(options, {
499
- kind: "ghooks",
500
- path: [
501
- "config",
502
- "ghooks",
503
- "pre-commit"
504
- ],
505
- leafShape: "string"
506
- });
507
- const installPreCommitNpm = (options) => installPackageJsonHook(options, {
508
- kind: "pre-commit-npm",
509
- path: ["pre-commit"],
510
- leafShape: "array"
511
- });
512
- const installPrettyQuick = (options) => installPackageJsonHook(options, {
513
- kind: "pretty-quick",
514
- path: ["gitHooks", "pre-commit"],
515
- leafShape: "string"
516
- });
517
- const installYorkie = (options) => installPackageJsonHook(options, {
518
- kind: "yorkie",
519
- path: ["gitHooks", "pre-commit"],
520
- leafShape: "string"
521
- });
522
- const installGitHooksJs = (options) => installPackageJsonHook(options, {
523
- kind: "git-hooks-js",
524
- path: ["git-hooks", "pre-commit"],
525
- leafShape: "string"
526
- });
527
- const appendIndentedBlockToTopLevelSection = (content, sectionName, block) => {
528
- const normalizedContent = ensureTrailingNewline(content);
529
- const match = new RegExp(`^${sectionName}:\\s*$`, "m").exec(normalizedContent);
530
- if (match === null) return ensureTrailingNewline([
531
- normalizedContent.trimEnd(),
532
- "",
533
- `${sectionName}:`,
534
- ...block,
535
- ""
536
- ].filter((line, index) => index > 0 || line.length > 0).join("\n"));
537
- const sectionStartIndex = match.index;
538
- const nextSectionPattern = /^[A-Za-z0-9_-]+:\s*$/gm;
539
- nextSectionPattern.lastIndex = sectionStartIndex + match[0].length;
540
- let nextSectionMatch = nextSectionPattern.exec(normalizedContent);
541
- while (nextSectionMatch !== null && nextSectionMatch.index === sectionStartIndex) nextSectionMatch = nextSectionPattern.exec(normalizedContent);
542
- const insertIndex = nextSectionMatch?.index ?? normalizedContent.length;
543
- const prefix = normalizedContent.slice(0, insertIndex).trimEnd();
544
- const suffix = normalizedContent.slice(insertIndex);
545
- return ensureTrailingNewline([
546
- prefix,
547
- ...block,
548
- suffix.trimStart()
549
- ].join("\n"));
550
- };
551
- const findTopLevelSectionRange = (content, sectionName) => {
552
- const match = new RegExp(`^${sectionName}:\\s*$`, "m").exec(content);
553
- if (match === null) return null;
554
- const headerLineEndIndex = content.indexOf("\n", match.index);
555
- const headerEndIndex = headerLineEndIndex === -1 ? match.index + match[0].length : headerLineEndIndex + 1;
556
- const nextSectionPattern = /^[A-Za-z0-9_-]+:\s*$/gm;
557
- nextSectionPattern.lastIndex = headerEndIndex;
558
- return {
559
- headerEndIndex,
560
- contentEndIndex: nextSectionPattern.exec(content)?.index ?? content.length
561
- };
562
- };
563
- const appendLefthookCommand = (content) => {
564
- const normalizedContent = ensureTrailingNewline(content);
565
- const sectionRange = findTopLevelSectionRange(normalizedContent, "pre-commit");
566
- const reactDoctorCommandBlock = [
567
- " react-doctor:",
568
- ` run: ${NON_BLOCKING_REACT_DOCTOR_COMMAND}`,
569
- ""
570
- ];
571
- if (sectionRange === null) return ensureTrailingNewline([
572
- normalizedContent.trimEnd(),
573
- "",
574
- "pre-commit:",
575
- " commands:",
576
- ...reactDoctorCommandBlock
577
- ].filter((line, index) => index > 0 || line.length > 0).join("\n"));
578
- const sectionContent = normalizedContent.slice(sectionRange.headerEndIndex, sectionRange.contentEndIndex);
579
- const commandsMatch = /^ commands:\s*$/m.exec(sectionContent);
580
- const insertIndex = commandsMatch === null ? sectionRange.headerEndIndex : sectionRange.headerEndIndex + sectionContent.indexOf(commandsMatch[0]) + commandsMatch[0].length + 1;
581
- const insertBlock = commandsMatch === null ? [" commands:", ...reactDoctorCommandBlock].join("\n") : reactDoctorCommandBlock.join("\n");
582
- return ensureTrailingNewline(`${normalizedContent.slice(0, insertIndex)}${insertBlock}${normalizedContent.slice(insertIndex)}`);
583
- };
584
- const installLefthook = (options) => {
585
- const didHookExist = fs$1.existsSync(options.hookPath);
586
- const existingContent = didHookExist ? fs$1.readFileSync(options.hookPath, "utf8") : "";
587
- if (!existingContent.includes("react-doctor")) {
588
- const nextContent = appendLefthookCommand(existingContent);
589
- fs$1.mkdirSync(path$1.dirname(options.hookPath), { recursive: true });
590
- fs$1.writeFileSync(options.hookPath, nextContent);
591
- }
592
- removeLegacyManagedRunner(options.projectRoot);
593
- return {
594
- hookPath: options.hookPath,
595
- kind: "lefthook",
596
- status: didHookExist ? "updated" : "created"
597
- };
598
- };
599
- const installPreCommit = (options) => {
600
- const didHookExist = fs$1.existsSync(options.hookPath);
601
- const existingContent = didHookExist ? fs$1.readFileSync(options.hookPath, "utf8") : "";
602
- if (!existingContent.includes("id: react-doctor")) {
603
- const hasReposKey = /^repos:\s*$/m.test(existingContent);
604
- const localHookBlock = [
605
- " - repo: local",
606
- " hooks:",
607
- " - id: react-doctor",
608
- " name: react-doctor",
609
- ` entry: sh -c '${NON_BLOCKING_REACT_DOCTOR_COMMAND}'`,
610
- " language: system",
611
- " pass_filenames: false",
612
- ""
613
- ].join("\n");
614
- const nextContent = hasReposKey ? `${ensureTrailingNewline(existingContent)}${localHookBlock}` : `repos:\n${localHookBlock}`;
615
- fs$1.mkdirSync(path$1.dirname(options.hookPath), { recursive: true });
616
- fs$1.writeFileSync(options.hookPath, nextContent);
617
- }
618
- removeLegacyManagedRunner(options.projectRoot);
619
- return {
620
- hookPath: options.hookPath,
621
- kind: "pre-commit",
622
- status: didHookExist ? "updated" : "created"
623
- };
624
- };
625
- const installOvercommit = (options) => {
626
- const didHookExist = fs$1.existsSync(options.hookPath);
627
- const existingContent = didHookExist ? fs$1.readFileSync(options.hookPath, "utf8") : "";
628
- if (!existingContent.includes("ReactDoctor")) {
629
- const nextContent = appendIndentedBlockToTopLevelSection(existingContent, "PreCommit", [
630
- " ReactDoctor:",
631
- " enabled: true",
632
- ` command: ['sh', '-c', '${NON_BLOCKING_REACT_DOCTOR_COMMAND}']`,
633
- ""
634
- ]);
635
- fs$1.mkdirSync(path$1.dirname(options.hookPath), { recursive: true });
636
- fs$1.writeFileSync(options.hookPath, nextContent);
637
- }
638
- removeLegacyManagedRunner(options.projectRoot);
639
- return {
640
- hookPath: options.hookPath,
641
- kind: "overcommit",
642
- status: didHookExist ? "updated" : "created"
643
- };
644
- };
645
- //#endregion
646
- //#region src/cli/utils/install-git-hook.ts
647
- const isHuskyProject = (projectRoot) => fs$1.existsSync(path$1.join(projectRoot, ".husky")) || packageHasDependency(projectRoot, "husky");
648
- const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
649
- const isSimpleGitHooksProject = (projectRoot) => {
650
- const packageJson = readPackageJson(projectRoot);
651
- return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || fs$1.existsSync(path$1.join(projectRoot, ".simple-git-hooks.cjs"));
652
- };
653
- const getLefthookConfigPath = (projectRoot) => {
654
- for (const fileName of LEFTHOOK_CONFIG_FILES) {
655
- const filePath = path$1.join(projectRoot, fileName);
656
- if (fs$1.existsSync(filePath)) return filePath;
657
- }
658
- return packageHasDependency(projectRoot, "lefthook") ? path$1.join(projectRoot, LEFTHOOK_CONFIG_FILES[0] ?? "lefthook.yml") : null;
659
- };
660
- const isPreCommitProject = (projectRoot) => fs$1.existsSync(path$1.join(projectRoot, PRE_COMMIT_CONFIG_FILE));
661
- const isOvercommitProject = (projectRoot) => fs$1.existsSync(path$1.join(projectRoot, ".overcommit.yml")) || packageHasDependency(projectRoot, "overcommit");
662
- const isYorkieProject = (projectRoot) => packageHasRecordKey(projectRoot, "gitHooks") || packageHasDependency(projectRoot, "yorkie");
663
- const isGhooksProject = (projectRoot) => packageHasDependency(projectRoot, "ghooks") || packageHasNestedRecordKey(projectRoot, "config", "ghooks");
664
- const isGitHooksJsProject = (projectRoot) => packageHasRecordKey(projectRoot, "git-hooks") || packageHasDependency(projectRoot, "git-hooks-js");
665
- const isPreCommitNpmProject = (projectRoot) => packageHasDependency(projectRoot, "pre-commit");
666
- const isPrettyQuickProject = (projectRoot) => packageHasDependency(projectRoot, "pretty-quick");
667
- const detectGitHookTarget = (projectRoot) => {
668
- if (runGit(projectRoot, ["rev-parse", "--is-inside-work-tree"]) !== "true") return null;
669
- const topLevel = runGit(projectRoot, ["rev-parse", "--show-toplevel"]) ?? projectRoot;
670
- const configuredHooksPath = runGit(projectRoot, [
671
- "config",
672
- "--path",
673
- "--get",
674
- "core.hooksPath"
675
- ]);
676
- if (configuredHooksPath !== null && configuredHooksPath.length > 0) return {
677
- hookPath: path$1.join(resolveGitPath(topLevel, configuredHooksPath), HOOK_FILE_NAME),
678
- runnerRoot: topLevel,
679
- kind: "configured"
680
- };
681
- if (isHuskyProject(topLevel)) return {
682
- hookPath: path$1.join(topLevel, HUSKY_HOOKS_PATH, HOOK_FILE_NAME),
683
- runnerRoot: topLevel,
684
- kind: "husky",
685
- hooksPathConfig: HUSKY_HOOKS_PATH
686
- };
687
- if (isVitePlusProject(topLevel)) return {
688
- hookPath: path$1.join(topLevel, VITE_PLUS_HOOKS_PATH, HOOK_FILE_NAME),
689
- runnerRoot: topLevel,
690
- kind: "vite-plus",
691
- hooksPathConfig: VITE_PLUS_HOOKS_PATH
692
- };
693
- if (isSimpleGitHooksProject(topLevel)) return {
694
- hookPath: path$1.join(topLevel, "package.json"),
695
- runnerRoot: topLevel,
696
- kind: "simple-git-hooks"
697
- };
698
- const lefthookConfigPath = getLefthookConfigPath(topLevel);
699
- if (lefthookConfigPath !== null) return {
700
- hookPath: lefthookConfigPath,
701
- runnerRoot: topLevel,
702
- kind: "lefthook"
703
- };
704
- if (isPreCommitProject(topLevel)) return {
705
- hookPath: path$1.join(topLevel, PRE_COMMIT_CONFIG_FILE),
706
- runnerRoot: topLevel,
707
- kind: "pre-commit"
708
- };
709
- if (isOvercommitProject(topLevel)) return {
710
- hookPath: path$1.join(topLevel, OVERCOMMIT_CONFIG_FILE),
711
- runnerRoot: topLevel,
712
- kind: "overcommit"
713
- };
714
- if (isYorkieProject(topLevel)) return {
715
- hookPath: getPackageJsonPath(topLevel),
716
- runnerRoot: topLevel,
717
- kind: "yorkie"
718
- };
719
- if (isGhooksProject(topLevel)) return {
720
- hookPath: getPackageJsonPath(topLevel),
721
- runnerRoot: topLevel,
722
- kind: "ghooks"
723
- };
724
- if (isGitHooksJsProject(topLevel)) return {
725
- hookPath: getPackageJsonPath(topLevel),
726
- runnerRoot: topLevel,
727
- kind: "git-hooks-js"
728
- };
729
- if (isPreCommitNpmProject(topLevel)) return {
730
- hookPath: getPackageJsonPath(topLevel),
731
- runnerRoot: topLevel,
732
- kind: "pre-commit-npm"
733
- };
734
- if (isPrettyQuickProject(topLevel)) return {
735
- hookPath: getPackageJsonPath(topLevel),
736
- runnerRoot: topLevel,
737
- kind: "pretty-quick"
738
- };
739
- const hookPath = runGit(projectRoot, [
740
- "rev-parse",
741
- "--git-path",
742
- HOOK_RELATIVE_PATH
743
- ]);
744
- if (hookPath === null || hookPath.length === 0) return null;
745
- return {
746
- hookPath: resolveGitPath(projectRoot, hookPath),
747
- runnerRoot: topLevel,
748
- kind: "git"
749
- };
750
- };
751
- const installReactDoctorGitHook = (options) => {
752
- if (options.kind === "simple-git-hooks") return installSimpleGitHooks(options);
753
- if (options.kind === "lefthook") return installLefthook(options);
754
- if (options.kind === "pre-commit") return installPreCommit(options);
755
- if (options.kind === "overcommit") return installOvercommit(options);
756
- if (options.kind === "yorkie") return installYorkie(options);
757
- if (options.kind === "ghooks") return installGhooks(options);
758
- if (options.kind === "git-hooks-js") return installGitHooksJs(options);
759
- if (options.kind === "pre-commit-npm") return installPreCommitNpm(options);
760
- if (options.kind === "pretty-quick") return installPrettyQuick(options);
761
- return installDirectGitHook(options);
762
- };
763
- //#endregion
764
- //#region src/cli/utils/install-react-doctor.ts
765
- const SETUP_OPTION_GIT_HOOK = "git-hook";
766
- const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
767
- const SETUP_OPTION_SKIP = "skip";
768
- const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
769
- "ghooks",
770
- "git-hooks-js",
771
- "lefthook",
772
- "overcommit",
773
- "pre-commit",
774
- "pre-commit-npm",
775
- "pretty-quick",
776
- "simple-git-hooks",
777
- "yorkie"
778
- ]);
779
- const PACKAGE_MANAGER_LOCKFILES = [
780
- {
781
- packageManager: "pnpm",
782
- fileName: "pnpm-lock.yaml"
783
- },
784
- {
785
- packageManager: "yarn",
786
- fileName: "yarn.lock"
787
- },
788
- {
789
- packageManager: "bun",
790
- fileName: "bun.lockb"
791
- },
792
- {
793
- packageManager: "bun",
794
- fileName: "bun.lock"
795
- },
796
- {
797
- packageManager: "npm",
798
- fileName: "package-lock.json"
799
- }
800
- ];
801
- const findNearestFileDirectory = (startDirectory, fileNames) => {
802
- let currentDirectory = path$1.resolve(startDirectory);
803
- while (true) {
804
- if (fileNames.some((fileName) => fs$1.existsSync(path$1.join(currentDirectory, fileName)))) return currentDirectory;
805
- const parentDirectory = path$1.dirname(currentDirectory);
806
- if (parentDirectory === currentDirectory) return null;
807
- currentDirectory = parentDirectory;
808
- }
809
- };
810
- const detectPackageManager = (projectRoot) => {
811
- let currentDirectory = path$1.resolve(projectRoot);
812
- while (true) {
813
- const packageJson = readPackageJson(currentDirectory);
814
- if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
815
- const packageManagerName = packageJson.packageManager.split("@")[0];
816
- if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
817
- }
818
- const parentDirectory = path$1.dirname(currentDirectory);
819
- if (parentDirectory === currentDirectory) break;
820
- currentDirectory = parentDirectory;
821
- }
822
- const lockfileDirectory = findNearestFileDirectory(projectRoot, PACKAGE_MANAGER_LOCKFILES.map((lockfile) => lockfile.fileName));
823
- return PACKAGE_MANAGER_LOCKFILES.find((lockfile) => lockfileDirectory !== null && fs$1.existsSync(path$1.join(lockfileDirectory, lockfile.fileName)))?.packageManager ?? "npm";
824
- };
825
- const packageManagerNeedsWorkspaceFlag = (projectRoot) => fs$1.existsSync(path$1.join(projectRoot, "pnpm-workspace.yaml")) || findNearestFileDirectory(projectRoot, ["pnpm-workspace.yaml"]) !== null;
826
- const buildInstallCommand = (projectRoot) => {
827
- const packageManager = detectPackageManager(projectRoot);
828
- const packageSpecifier = `${DOCTOR_PACKAGE_NAME}@latest`;
829
- if (packageManager === "npm") return {
830
- command: "npm",
831
- args: [
832
- "install",
833
- "--save-dev",
834
- packageSpecifier
835
- ],
836
- cwd: projectRoot
837
- };
838
- if (packageManager === "yarn") return {
839
- command: "yarn",
840
- args: [
841
- "add",
842
- "--dev",
843
- packageSpecifier
844
- ],
845
- cwd: projectRoot
846
- };
847
- if (packageManager === "bun") return {
848
- command: "bun",
849
- args: [
850
- "add",
851
- "--dev",
852
- packageSpecifier
853
- ],
854
- cwd: projectRoot
855
- };
856
- return {
857
- command: "pnpm",
858
- args: [
859
- "add",
860
- "--save-dev",
861
- ...packageManagerNeedsWorkspaceFlag(projectRoot) ? ["-w"] : [],
862
- packageSpecifier
863
- ],
864
- cwd: projectRoot
865
- };
866
- };
867
- const execFileAsync = promisify(execFile);
868
- const defaultInstallDependencyRunner = async (input) => {
869
- await execFileAsync(input.command, [...input.args], {
870
- cwd: input.cwd,
871
- env: {
872
- ...process.env,
873
- REACT_DOCTOR_INSTALL: "1"
874
- },
875
- shell: process.platform === "win32"
876
- });
877
- };
878
- const isSupplyChainTrustError = (error) => {
879
- const candidate = error;
880
- const haystack = [
881
- candidate?.stderr,
882
- candidate?.stdout,
883
- candidate?.message
884
- ].map((part) => String(part ?? "")).join("\n");
885
- return /ERR_PNPM_TRUST_DOWNGRADE|trust downgrade/i.test(haystack);
886
- };
887
- const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
888
- const installReactDoctorDependency = async (options) => {
889
- const packageJson = readPackageJson(options.projectRoot);
890
- if (!isRecord(packageJson)) return {
891
- dependencyStatus: "skipped",
892
- dependencyReason: "missing-or-invalid-package-json"
893
- };
894
- if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
895
- if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
896
- dependencyStatus: "skipped",
897
- dependencyReason: "invalid-dev-dependencies"
898
- };
899
- const runnerInput = buildInstallCommand(options.projectRoot);
900
- try {
901
- await (options.runner ?? defaultInstallDependencyRunner)(runnerInput);
902
- } catch (error) {
903
- return {
904
- dependencyStatus: "skipped",
905
- dependencyReason: isSupplyChainTrustError(error) ? "trust-policy-blocked" : "install-command-failed",
906
- installCommand: formatInstallCommand(runnerInput)
907
- };
908
- }
909
- return { dependencyStatus: "created" };
910
- };
911
- const buildManualGitHookTarget = (hookPath, projectRoot) => ({
912
- hookPath,
913
- runnerRoot: projectRoot,
914
- kind: "git"
915
- });
916
- const formatGitHookInstallMessage = (hookResult) => {
917
- if (CONFIG_ONLY_GIT_HOOK_KINDS.has(hookResult.kind)) return `React Doctor pre-commit config ${hookResult.status} at ${hookResult.hookPath}. Run your hook manager's install command if hooks are not already installed.`;
918
- return `React Doctor pre-commit hook ${hookResult.status} at ${hookResult.hookPath}.`;
919
- };
920
- const formatDependencyInstallMessage = (result) => {
921
- if (result.dependencyStatus === "created") return "Installed dev dependency: react-doctor.";
922
- if (result.dependencyStatus === "existing") return "React Doctor dependency already exists.";
923
- if (result.dependencyReason === "invalid-dev-dependencies") return "Skipped dev dependency install: devDependencies field is not an object.";
924
- if (result.dependencyReason === "trust-policy-blocked") return "Local install skipped by your package manager's supply-chain trust policy (safe to ignore for pre-release packages).";
925
- if (result.dependencyReason === "install-command-failed") return "Local install failed: your package manager rejected the command.";
926
- return "Skipped dev dependency install: package.json missing or invalid.";
927
- };
928
- const buildDependencyFollowUp = (result) => {
929
- if (result.dependencyReason !== "trust-policy-blocked" && result.dependencyReason !== "install-command-failed") return;
930
- return ` React Doctor still works via \`npx react-doctor\`. To install locally: ${result.installCommand ?? `npm install --save-dev react-doctor@latest`}`;
931
- };
932
- const installReactDoctorPackageSetup = async (projectRoot, dependencyRunner) => {
933
- installReactDoctorScriptStep(projectRoot);
934
- const dependencySpinner = spinner("Installing React Doctor package...").start();
935
- try {
936
- const dependencyResult = await installReactDoctorDependency({
937
- projectRoot,
938
- runner: dependencyRunner
939
- });
940
- recordCount(METRIC.installDependency, 1, {
941
- status: dependencyResult.dependencyStatus,
942
- reason: dependencyResult.dependencyReason ?? null,
943
- packageManager: detectPackageManager(projectRoot)
944
- });
945
- if (dependencyResult.dependencyStatus === "skipped") {
946
- const message = formatDependencyInstallMessage(dependencyResult);
947
- if (dependencyResult.dependencyReason === "trust-policy-blocked") dependencySpinner.warn(message);
948
- else dependencySpinner.fail(message);
949
- const followUp = buildDependencyFollowUp(dependencyResult);
950
- if (followUp !== void 0) cliLogger.dim(followUp);
951
- return dependencyResult;
952
- }
953
- dependencySpinner.succeed(formatDependencyInstallMessage(dependencyResult));
954
- return dependencyResult;
955
- } catch (error) {
956
- dependencySpinner.fail("Failed to install React Doctor package.");
957
- throw error;
958
- }
959
- };
960
- const getSkillSourceDirectory = () => {
961
- const distDirectory = path$1.dirname(fileURLToPath(import.meta.url));
962
- return path$1.join(distDirectory, "skills", SKILL_NAME);
963
- };
964
- const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
965
- const installReactDoctorSkillStep = async (sourceDir, selectedAgents, projectRoot) => {
966
- const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
967
- try {
968
- const installResult = await installSkillsFromSource({
969
- source: sourceDir,
970
- agents: selectedAgents,
971
- cwd: projectRoot,
972
- mode: "copy"
973
- });
974
- if (installResult.skills.length === 0) throw new Error(`Could not parse ${SKILL_MANIFEST_FILE} for ${SKILL_NAME} (missing or invalid frontmatter).`);
975
- if (installResult.failed.length > 0) throw new Error(installResult.failed.map((failure) => `${getSkillAgentConfig(failure.agent).displayName}: ${failure.error}`).join("\n"));
976
- installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map((agent) => getSkillAgentConfig(agent).displayName).join(", ")}.`);
977
- } catch (error) {
978
- installSpinner.fail(`Failed to install ${SKILL_NAME} skill.`);
979
- throw error;
980
- }
981
- };
982
- const installReactDoctorGitHookStep = (gitHookTarget) => {
983
- const hookSpinner = spinner("Installing React Doctor pre-commit hook...").start();
984
- try {
985
- const hookResult = installReactDoctorGitHook({
986
- hookPath: gitHookTarget.hookPath,
987
- projectRoot: gitHookTarget.runnerRoot,
988
- kind: gitHookTarget.kind,
989
- hooksPathConfig: gitHookTarget.hooksPathConfig
990
- });
991
- hookSpinner.succeed(formatGitHookInstallMessage(hookResult));
992
- recordCount(METRIC.installGitHook, 1, { kind: hookResult.kind });
993
- } catch (error) {
994
- hookSpinner.fail("Failed to install React Doctor pre-commit hook.");
995
- throw error;
996
- }
997
- };
998
- const installReactDoctorAgentHooksStep = (projectRoot, selectedAgents) => {
999
- const hookSpinner = spinner("Installing React Doctor agent hooks...").start();
1000
- try {
1001
- const hookResult = installReactDoctorAgentHooks({
1002
- projectRoot,
1003
- agents: selectedAgents
1004
- });
1005
- if (hookResult.installedAgents.length === 0) hookSpinner.succeed("No supported native agent hook targets selected.");
1006
- else {
1007
- hookSpinner.succeed(`React Doctor agent hooks installed for ${hookResult.installedAgents.map((agent) => getSkillAgentConfig(agent).displayName).join(", ")}.`);
1008
- recordCount(METRIC.installAgentHooks, 1, { agentsCount: hookResult.installedAgents.length });
1009
- }
1010
- } catch (error) {
1011
- hookSpinner.fail("Failed to install React Doctor agent hooks.");
1012
- throw error;
1013
- }
1014
- };
1015
- const installReactDoctorWorkflowStep = async (projectRoot) => {
1016
- return reportWorkflowResult(spinner("Adding GitHub Actions workflow...").start(), installReactDoctorWorkflow(projectRoot, await detectDefaultBranch(projectRoot) ?? void 0), projectRoot);
1017
- };
1018
- const upgradeReactDoctorWorkflowStep = (projectRoot) => {
1019
- const workflowSpinner = spinner("Upgrading GitHub Actions workflow to v2...").start();
1020
- const upgradeResult = upgradeReactDoctorWorkflowInPlace(projectRoot);
1021
- if (upgradeResult.status === "failed") {
1022
- workflowSpinner.fail("Couldn't update the GitHub Actions workflow.");
1023
- return false;
1024
- }
1025
- if (upgradeResult.status === "not-needed") {
1026
- workflowSpinner.succeed("GitHub Actions workflow already up to date.");
1027
- return false;
1028
- }
1029
- workflowSpinner.succeed(`Upgraded the GitHub Actions workflow to v2 at ${path$1.relative(projectRoot, upgradeResult.workflowPath)}.`);
1030
- recordCount(METRIC.installWorkflow, 1, { kind: "upgrade" });
1031
- return true;
1032
- };
1033
- const runInstallReactDoctor = async (options = {}) => {
1034
- const requestedProjectRoot = options.projectRoot ?? process.cwd();
1035
- const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
1036
- const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
1037
- if (!fs$1.existsSync(path$1.join(sourceDir, SKILL_MANIFEST_FILE))) {
1038
- cliLogger.error(`Could not locate the ${SKILL_NAME} skill bundled with this package.`);
1039
- process.exitCode = 1;
1040
- return;
1041
- }
1042
- const detectedAgents = options.detectedAgents ?? await detectAvailableAgents();
1043
- if (detectedAgents.length === 0) {
1044
- cliLogger.error("No supported coding agents detected.");
1045
- cliLogger.dim(" Looked for binaries on PATH (claude, codex, cursor, droid, gemini, copilot, opencode, pi)");
1046
- cliLogger.dim(" and config dirs in $HOME (~/.claude, ~/.cursor, ~/.codex, ~/.gemini, ...).");
1047
- process.exitCode = 1;
1048
- return;
1049
- }
1050
- const skipPrompts = shouldSkipPrompts({ yes: options.yes });
1051
- const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
1052
- const gitHookPath = gitHookTarget?.hookPath;
1053
- const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
1054
- const prompt = options.prompt ?? prompts;
1055
- const workflowTargetPath = getReactDoctorWorkflowPath(projectRoot);
1056
- const existingWorkflow = readReactDoctorWorkflow(projectRoot);
1057
- const canInstallWorkflow = !fs$1.existsSync(workflowTargetPath);
1058
- const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
1059
- const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
1060
- const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
1061
- const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
1062
- const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
1063
- if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
1064
- if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
1065
- const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
1066
- const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
1067
- const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
1068
- const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
1069
- type: "multiselect",
1070
- name: "agents",
1071
- message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
1072
- choices: detectedAgents.map((agent) => ({
1073
- title: getSkillAgentConfig(agent).displayName,
1074
- value: agent,
1075
- selected: defaultSelectedAgents.includes(agent)
1076
- })),
1077
- instructions: false,
1078
- min: 1
1079
- }, promptOptions)).agents ?? [];
1080
- if (selectedAgents.length === 0) return;
1081
- if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
1082
- let dependencyResult;
1083
- if (!options.dryRun) {
1084
- await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
1085
- dependencyResult = await installReactDoctorPackageSetup(projectRoot, options.installDependencyRunner);
1086
- }
1087
- let didInstallWorkflow = false;
1088
- if (!options.dryRun && (shouldInstallWorkflow || shouldUpgradeWorkflow)) {
1089
- cliLogger.break();
1090
- if (shouldInstallWorkflow) didInstallWorkflow = await installReactDoctorWorkflowStep(projectRoot);
1091
- else if (upgradeReactDoctorWorkflowStep(projectRoot)) recordActionUpgradeDecision(projectRoot, "accepted");
1092
- }
1093
- const setupActionChoices = [...gitHookPath === null || gitHookPath === void 0 ? [] : [{
1094
- title: "Pre-commit hook",
1095
- description: "Check staged changes before each commit",
1096
- value: SETUP_OPTION_GIT_HOOK,
1097
- selected: true
1098
- }], ...canInstallNativeAgentHooks(selectedAgents) ? [{
1099
- title: "Agent hooks",
1100
- description: "Ask Claude Code or Cursor to scan after code edits",
1101
- value: SETUP_OPTION_AGENT_HOOKS,
1102
- selected: Boolean(options.agentHooks)
1103
- }] : []];
1104
- const setupChoices = setupActionChoices.length === 0 ? [] : [{
1105
- title: "Skip optional setup",
1106
- description: "Install only the agent skill and package setup",
1107
- value: SETUP_OPTION_SKIP,
1108
- selected: false
1109
- }, ...setupActionChoices];
1110
- if (setupChoices.length > 0 && !options.dryRun) cliLogger.break();
1111
- const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
1112
- type: "multiselect",
1113
- name: "setupOptions",
1114
- message: "Select additional React Doctor setup:",
1115
- choices: setupChoices,
1116
- instructions: false
1117
- }, promptOptions)).setupOptions ?? [];
1118
- const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
1119
- const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
1120
- const shouldInstallGitHook = gitHookPath != null && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
1121
- const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
1122
- if (!options.dryRun) {
1123
- if (shouldInstallGitHook && gitHookTarget != null) installReactDoctorGitHookStep(gitHookTarget);
1124
- if (shouldInstallAgentHooks) installReactDoctorAgentHooksStep(projectRoot, selectedAgents);
1125
- }
1126
- if (options.dryRun) {
1127
- cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
1128
- for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
1129
- cliLogger.dim(` Source: ${sourceDir}`);
1130
- cliLogger.dim(" Package script: doctor (or react-doctor if doctor exists)");
1131
- cliLogger.dim(" Dev dependency: react-doctor");
1132
- if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
1133
- if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
1134
- if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path$1.relative(projectRoot, workflowTargetPath)}`);
1135
- if (shouldUpgradeWorkflow) cliLogger.dim(` Upgrade GitHub Actions workflow to v2: ${path$1.relative(projectRoot, workflowTargetPath)}`);
1136
- return;
1137
- }
1138
- recordCount(METRIC.installCompleted, 1, {
1139
- agentsCount: selectedAgents.length,
1140
- agentsDetected: detectedAgents.length,
1141
- usedRememberedAgents,
1142
- gitHook: shouldInstallGitHook,
1143
- agentHooks: shouldInstallAgentHooks,
1144
- workflow: didInstallWorkflow,
1145
- dependencyStatus: dependencyResult?.dependencyStatus ?? "skipped",
1146
- packageManager: detectPackageManager(projectRoot)
1147
- });
1148
- for (const agent of selectedAgents) recordCount(METRIC.installAgent, 1, { agent });
1149
- };
1150
- //#endregion
1151
- export { askUpgradeActionVersion as a, recordCiPromptDecision as c, installReactDoctorAgentHooks as i, hasHandledActionUpgrade as l, runInstallReactDoctor as n, askAddToGitHubActions as o, findAgentsWithLegacyShellHooks as r, hasHandledCiPrompt as s, getSkillSourceDirectory as t, recordActionUpgradeDecision as u };
1152
-
1153
- //# sourceMappingURL=install-react-doctor-BhfvyNZB.js.map
1154
- //# debugId=37d51c00-658e-5212-9cfb-51a98a4a4ded