react-doctor 0.9.7 → 0.9.8-dev.7f028ea

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.
@@ -0,0 +1,158 @@
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]="98355477-37ef-5b82-8b69-b28370c58639")}catch(e){}}();
3
+ import { D as SENTRY_FLUSH_TIMEOUT_MS, H as CANONICAL_DISCORD_URL, Hn as gen, Ht as formatErrorChain, In as error, U as CANONICAL_GITHUB_URL, Ut as formatReactDoctorError, Yn as runSync, cn as isReactDoctorError, en as highlighter, i as buildSentryScope, m as VERSION, nn as isErrnoException, t as recordCount, y as METRIC } from "./record-metric-BGxpNcR0.js";
4
+ import { c as isEnvironmentError, i as recordRunTraceId, n as getActiveRunTrace, o as isExpectedUserError, s as formatEnvironmentError } from "./unref-stdin-noiOzC5n.js";
5
+ import * as Sentry from "@sentry/node";
6
+ //#region src/cli/utils/handle-error.ts
7
+ const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
8
+ const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
9
+ const formatErrorForReport = (error) => isReactDoctorError(error) ? formatReactDoctorError(error) : formatErrorChain(error);
10
+ const formatSingleLine = (text) => text.replaceAll(/\s+/g, " ").trim();
11
+ const getErrorReportContext = () => ({
12
+ cwd: process.cwd(),
13
+ command: process.argv.join(" "),
14
+ nodeVersion: process.version,
15
+ platform: process.platform,
16
+ architecture: process.arch,
17
+ isOtlpEndpointConfigured: Boolean(process.env[OTLP_ENDPOINT_ENVIRONMENT_VARIABLE]),
18
+ isOtlpAuthHeaderConfigured: Boolean(process.env[OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE])
19
+ });
20
+ const formatConfiguredState = (isConfigured) => isConfigured ? "yes" : "no";
21
+ const buildErrorIssueBody = (error, context, sentryEventId) => {
22
+ const formattedError = formatErrorForReport(error) || "(empty error)";
23
+ const isOtlpExporterEnabled = context.isOtlpEndpointConfigured && context.isOtlpAuthHeaderConfigured;
24
+ return [
25
+ "## Error",
26
+ "",
27
+ "```text",
28
+ formattedError,
29
+ "```",
30
+ "",
31
+ "## Runtime",
32
+ "",
33
+ `- react-doctor version: ${VERSION}`,
34
+ `- node: ${context.nodeVersion}`,
35
+ `- platform: ${context.platform} ${context.architecture}`,
36
+ `- cwd: ${context.cwd}`,
37
+ `- command: ${context.command}`,
38
+ ...sentryEventId ? [`- Sentry reference: ${sentryEventId}`] : [],
39
+ "",
40
+ "## OpenTelemetry",
41
+ "",
42
+ `- ${OTLP_ENDPOINT_ENVIRONMENT_VARIABLE} configured: ${formatConfiguredState(context.isOtlpEndpointConfigured)}`,
43
+ `- ${OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE} configured: ${formatConfiguredState(context.isOtlpAuthHeaderConfigured)} (value redacted)`,
44
+ `- OTLP exporter enabled: ${formatConfiguredState(isOtlpExporterEnabled)}`,
45
+ "- trace/span link, if exported: ",
46
+ "",
47
+ "## Notes",
48
+ "",
49
+ "Please add reproduction steps and any relevant repository details."
50
+ ].join("\n");
51
+ };
52
+ const buildErrorIssueUrl = (error, sentryEventId) => {
53
+ const formattedError = formatSingleLine(formatErrorForReport(error));
54
+ const issueUrl = new URL(`${CANONICAL_GITHUB_URL}/issues/new`);
55
+ issueUrl.searchParams.set("title", formattedError ? `CLI error: ${formattedError}` : "CLI error");
56
+ issueUrl.searchParams.set("labels", "bug");
57
+ issueUrl.searchParams.set("body", buildErrorIssueBody(error, getErrorReportContext(), sentryEventId));
58
+ return issueUrl.toString();
59
+ };
60
+ /**
61
+ * Effect-typed renderer: every message routes through `Console.error`
62
+ * so test runs can swap `Console` to a capture sink and the output
63
+ * appears in the right stream (stderr) in production. Lines stay
64
+ * red-highlighted (matches the historical `consoleLogger.error`
65
+ * contract) so the user sees a clearly distinguished error block.
66
+ */
67
+ const handleErrorEffect = (error$2, sentryEventId) => gen(function* () {
68
+ yield* error("");
69
+ yield* error(highlighter.error("Something went wrong. Please check the error below for more details."));
70
+ yield* error(highlighter.error(`If the problem persists, please open this prefilled issue: ${buildErrorIssueUrl(error$2, sentryEventId)}`));
71
+ yield* error(highlighter.error(`You can also ask for help in Discord: ${CANONICAL_DISCORD_URL}`));
72
+ if (sentryEventId) yield* error(highlighter.error(`Reference (mention this when reporting): ${sentryEventId}`));
73
+ yield* error("");
74
+ yield* error(highlighter.error(formatErrorForReport(error$2)));
75
+ yield* error("");
76
+ });
77
+ /**
78
+ * Sync façade for legacy callers (top-level CLI command bodies that
79
+ * aren't yet Effect-typed). Bridges via `Effect.runSync` so the
80
+ * underlying Console writes happen exactly like the Effect path.
81
+ */
82
+ const handleError = (error, options = {}) => {
83
+ runSync(handleErrorEffect(error, options.sentryEventId));
84
+ if (options.shouldExit !== false) process.exit(1);
85
+ process.exitCode = 1;
86
+ };
87
+ /**
88
+ * Renderer for expected, user-actionable failures — a bad `--diff` value,
89
+ * a base branch that isn't fetched, or environment errors like disk-full or
90
+ * permission-denied. Prints just the (already human-readable) message — no
91
+ * "Something went wrong", prefilled issue, Discord link, or Sentry reference
92
+ * — because there is no bug to report.
93
+ */
94
+ const handleUserError = (error$1, options = {}) => {
95
+ const isEnvError = isEnvironmentError(error$1);
96
+ if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: (isErrnoException(error$1) ? error$1.code : void 0) ?? "unknown" });
97
+ const message = isEnvError ? formatEnvironmentError(error$1) : formatErrorForReport(error$1);
98
+ runSync(gen(function* () {
99
+ yield* error("");
100
+ yield* error(highlighter.error(message));
101
+ yield* error("");
102
+ }));
103
+ if (options.shouldExit !== false) process.exit(1);
104
+ process.exitCode = 1;
105
+ };
106
+ //#endregion
107
+ //#region src/cli/utils/report-error.ts
108
+ /**
109
+ * Sends an error to Sentry — enriched with a fresh snapshot of the current run
110
+ * (version, platform, CI/agent, invocation, scanned project) and, when a run
111
+ * transaction is in flight, linked to its trace via the scope's propagation
112
+ * context so the crash and its transaction share a `trace_id` — then waits for
113
+ * delivery before the caller exits. The CLI tears down synchronously after
114
+ * rendering an error, so the awaited `flush` is what actually gets the event
115
+ * (and any in-flight transaction) off the machine.
116
+ *
117
+ * Returns the Sentry event id so the caller can surface it as a reference the
118
+ * user can quote when reporting the bug; returns `undefined` when Sentry was
119
+ * never initialized (`--no-score`, tests, or a missing DSN) or delivery failed.
120
+ * Swallows any transport failure so telemetry can never mask the user's
121
+ * original error.
122
+ */
123
+ const reportErrorToSentry = async (error) => {
124
+ if (!Sentry.isInitialized()) return void 0;
125
+ if (isExpectedUserError(error)) return void 0;
126
+ try {
127
+ const { tags, contexts } = buildSentryScope();
128
+ let reason = "unknown";
129
+ if (isReactDoctorError(error)) reason = error.reason._tag;
130
+ else if (error instanceof Error) reason = error.name;
131
+ recordCount(METRIC.cliError, 1, {
132
+ command: typeof tags.command === "string" ? tags.command : void 0,
133
+ reason
134
+ });
135
+ const runTrace = getActiveRunTrace();
136
+ const eventId = Sentry.withScope((scope) => {
137
+ for (const [name, context] of Object.entries(contexts)) scope.setContext(name, context);
138
+ scope.setTags(tags);
139
+ if (runTrace) scope.setPropagationContext({
140
+ traceId: runTrace.traceId,
141
+ parentSpanId: runTrace.spanId,
142
+ sampled: runTrace.sampled,
143
+ sampleRand: Math.random()
144
+ });
145
+ recordRunTraceId(scope.getPropagationContext().traceId);
146
+ return Sentry.captureException(error);
147
+ });
148
+ await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
149
+ return eventId;
150
+ } catch {
151
+ return;
152
+ }
153
+ };
154
+ //#endregion
155
+ export { handleError as n, handleUserError as r, reportErrorToSentry as t };
156
+
157
+ //# sourceMappingURL=report-error-Wat6_MDs.js.map
158
+ //# debugId=98355477-37ef-5b82-8b69-b28370c58639
@@ -0,0 +1,279 @@
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]="2c4a070e-23f8-5a31-a4af-18688e5c19c0")}catch(e){}}();
3
+ import { S as REACT_DOCTOR_CONFIG_PROJECT_NAME, t as recordCount, y as METRIC } from "./record-metric-BGxpNcR0.js";
4
+ import { u as isCommandAvailable } from "./install-github-workflow-CAo3dNh1.js";
5
+ import { spawn } from "node:child_process";
6
+ import * as path$1 from "node:path";
7
+ import process$1 from "node:process";
8
+ import { createHash } from "node:crypto";
9
+ import { detectInstalledSkillAgents, getSkillAgentTypes } from "agent-install";
10
+ import Conf from "conf";
11
+ //#region src/cli/utils/detect-agents.ts
12
+ const PATH_BINARIES = {
13
+ "claude-code": ["claude"],
14
+ codex: ["codex"],
15
+ cursor: ["cursor", "agent"],
16
+ droid: ["droid"],
17
+ "gemini-cli": ["gemini"],
18
+ "github-copilot": ["copilot"],
19
+ opencode: ["opencode"],
20
+ pi: ["pi", "omegon"]
21
+ };
22
+ const detectPathAvailableAgents = () => {
23
+ const detected = [];
24
+ for (const [agent, binaries] of Object.entries(PATH_BINARIES)) if (binaries.some(isCommandAvailable)) detected.push(agent);
25
+ return detected;
26
+ };
27
+ const detectAvailableAgents = async () => {
28
+ const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
29
+ return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
30
+ };
31
+ const DEFAULT_INSTALL_AGENTS = [
32
+ "claude-code",
33
+ "cursor",
34
+ "codex",
35
+ "opencode"
36
+ ];
37
+ const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
38
+ const detected = new Set(detectedAgents);
39
+ const remembered = rememberedAgents.filter((agent) => detected.has(agent));
40
+ if (remembered.length > 0) return remembered;
41
+ const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
42
+ if (defaults.length > 0) return defaults;
43
+ return detectedAgents.length === 1 ? [...detectedAgents] : [];
44
+ };
45
+ //#endregion
46
+ //#region src/cli/utils/now-iso.ts
47
+ const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
48
+ const ONBOARDING_EVENT = "onboarding";
49
+ const CI_PITCH_EVENT = "ci-pitch";
50
+ const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
51
+ const SETUP_HINT_EVENT = "setup-hint";
52
+ const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
53
+ const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
54
+ const foldLegacyDecisions = (projects, legacy, eventId) => {
55
+ for (const [hash, record] of Object.entries(legacy ?? {})) {
56
+ const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
57
+ projects[hash] = {
58
+ ...existing,
59
+ events: {
60
+ ...existing.events,
61
+ [eventId]: {
62
+ firedAt: record.at ?? nowIso(),
63
+ version: 1,
64
+ ...record.outcome ? { outcome: record.outcome } : {}
65
+ }
66
+ }
67
+ };
68
+ }
69
+ };
70
+ const migrateCliState = (state) => {
71
+ if (typeof state.schemaVersion === "number" && state.schemaVersion >= 2) return state;
72
+ const projects = {};
73
+ for (const [hash, record] of Object.entries(state.projects ?? {})) {
74
+ const carried = {
75
+ rootDirectory: record.rootDirectory,
76
+ ...record.events ? { events: record.events } : {},
77
+ ...record.migrations ? { migrations: record.migrations } : {}
78
+ };
79
+ projects[hash] = record.setupPrompt === false ? {
80
+ ...carried,
81
+ events: {
82
+ ...carried.events,
83
+ [SETUP_HINT_EVENT]: {
84
+ firedAt: nowIso(),
85
+ version: 1,
86
+ outcome: "declined"
87
+ }
88
+ }
89
+ } : carried;
90
+ }
91
+ foldLegacyDecisions(projects, state.ciPrompts, CI_PITCH_EVENT);
92
+ foldLegacyDecisions(projects, state.actionUpgrades, ACTION_UPGRADE_EVENT);
93
+ return {
94
+ schemaVersion: 2,
95
+ global: typeof state.onboardedAt === "string" ? { events: { [ONBOARDING_EVENT]: {
96
+ firedAt: state.onboardedAt,
97
+ version: 1
98
+ } } } : {},
99
+ projects
100
+ };
101
+ };
102
+ const resolveConfigDir = (options) => options.cwd ?? (process.env["REACT_DOCTOR_CONFIG_DIR"] || void 0);
103
+ const openStore = (options = {}) => new Conf({
104
+ projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
105
+ cwd: resolveConfigDir(options)
106
+ });
107
+ const openMigratedStore = (options) => {
108
+ const store = openStore(options);
109
+ const state = store.store;
110
+ const migrated = migrateCliState(state);
111
+ if (migrated !== state) store.store = migrated;
112
+ return store;
113
+ };
114
+ const readCliState = (select, fallback, options = {}) => {
115
+ try {
116
+ return select(openMigratedStore(options).store);
117
+ } catch {
118
+ return fallback;
119
+ }
120
+ };
121
+ const updateCliState = (update, options = {}) => {
122
+ try {
123
+ const store = openMigratedStore(options);
124
+ store.store = update(store.store);
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
130
+ //#endregion
131
+ //#region src/cli/utils/hash-project-root.ts
132
+ const hashProjectRoot = (projectRoot) => createHash("sha256").update(path$1.resolve(projectRoot)).digest("hex");
133
+ //#endregion
134
+ //#region src/cli/utils/cli-lifecycle.ts
135
+ const versionOf = (item) => item.version ?? 1;
136
+ const selectScope = (state, scoped, projectRoot) => scoped.scope === "global" ? state.global : projectRoot === void 0 ? void 0 : state.projects?.[hashProjectRoot(projectRoot)];
137
+ const updateScope = (state, scoped, projectRoot, updateScopeState) => {
138
+ if (scoped.scope === "global") return {
139
+ ...state,
140
+ global: updateScopeState(state.global ?? {})
141
+ };
142
+ if (projectRoot === void 0) return state;
143
+ const projectKey = hashProjectRoot(projectRoot);
144
+ const base = state.projects?.[projectKey] ?? { rootDirectory: path$1.resolve(projectRoot) };
145
+ return {
146
+ ...state,
147
+ projects: {
148
+ ...state.projects,
149
+ [projectKey]: {
150
+ ...base,
151
+ ...updateScopeState(base)
152
+ }
153
+ }
154
+ };
155
+ };
156
+ const isGatePending = (gate, target = {}, options = {}) => {
157
+ if (gate.scope === "project" && target.projectRoot === void 0) return false;
158
+ return readCliState((state) => {
159
+ const record = selectScope(state, gate, target.projectRoot)?.events?.[gate.id];
160
+ return !record || record.version < versionOf(gate);
161
+ }, gate.fireWhenUnknown ?? false, options);
162
+ };
163
+ const recordGate = (gate, target = {}, options = {}) => updateCliState((state) => updateScope(state, gate, target.projectRoot, (scope) => ({
164
+ ...scope,
165
+ events: {
166
+ ...scope.events,
167
+ [gate.id]: {
168
+ firedAt: nowIso(),
169
+ version: versionOf(gate),
170
+ ...target.outcome ? { outcome: target.outcome } : {}
171
+ }
172
+ }
173
+ })), options);
174
+ const readPreference = (preference, target = {}, options = {}) => readCliState((state) => selectScope(state, preference, target.projectRoot)?.preferences?.[preference.id] ?? null, null, options);
175
+ const writePreference = (preference, value, target = {}, options = {}) => updateCliState((state) => updateScope(state, preference, target.projectRoot, (scope) => ({
176
+ ...scope,
177
+ preferences: {
178
+ ...scope.preferences,
179
+ [preference.id]: value
180
+ }
181
+ })), options);
182
+ const isMigrationPending = (migration, target = {}, options = {}) => {
183
+ if (migration.scope === "project" && target.projectRoot === void 0) return false;
184
+ return readCliState((state) => {
185
+ const record = selectScope(state, migration, target.projectRoot)?.migrations?.[migration.id];
186
+ return !record || record.version < versionOf(migration);
187
+ }, false, options);
188
+ };
189
+ const recordMigration = (migration, projectRoot, record, options) => updateCliState((state) => updateScope(state, migration, projectRoot, (scope) => ({
190
+ ...scope,
191
+ migrations: {
192
+ ...scope.migrations,
193
+ [migration.id]: record
194
+ }
195
+ })), options);
196
+ const runMigrations = async (migrations, target = {}, options = {}) => {
197
+ const results = [];
198
+ for (const migration of migrations) {
199
+ if (!isMigrationPending(migration, target, options)) {
200
+ results.push({
201
+ id: migration.id,
202
+ ran: false,
203
+ applied: true
204
+ });
205
+ continue;
206
+ }
207
+ let applied = false;
208
+ try {
209
+ applied = await migration.run({ projectRoot: target.projectRoot });
210
+ } catch {
211
+ applied = false;
212
+ }
213
+ if (applied) recordMigration(migration, target.projectRoot, {
214
+ ranAt: nowIso(),
215
+ version: versionOf(migration)
216
+ }, options);
217
+ results.push({
218
+ id: migration.id,
219
+ ran: true,
220
+ applied
221
+ });
222
+ }
223
+ return results;
224
+ };
225
+ //#endregion
226
+ //#region src/cli/utils/open-url.ts
227
+ const resolveOpenCommand = (url) => {
228
+ if (process$1.platform === "darwin") return {
229
+ command: "open",
230
+ args: [url]
231
+ };
232
+ if (process$1.platform === "win32") return {
233
+ command: "cmd",
234
+ args: [
235
+ "/c",
236
+ "start",
237
+ "",
238
+ url
239
+ ]
240
+ };
241
+ return {
242
+ command: "xdg-open",
243
+ args: [url]
244
+ };
245
+ };
246
+ const openUrl = (url) => new Promise((resolve) => {
247
+ try {
248
+ const { command, args } = resolveOpenCommand(url);
249
+ const child = spawn(command, args, {
250
+ detached: true,
251
+ stdio: "ignore"
252
+ });
253
+ child.once("spawn", () => resolve(true));
254
+ child.once("error", () => resolve(false));
255
+ child.unref();
256
+ } catch {
257
+ resolve(false);
258
+ }
259
+ });
260
+ //#endregion
261
+ //#region src/cli/utils/report-workflow-result.ts
262
+ const reportWorkflowResult = (workflowSpinner, result, projectRoot) => {
263
+ if (result.status === "failed") {
264
+ workflowSpinner.fail("Couldn't write the GitHub Actions workflow.");
265
+ return false;
266
+ }
267
+ if (result.status === "exists") {
268
+ workflowSpinner.succeed("GitHub Actions workflow already configured.");
269
+ return false;
270
+ }
271
+ workflowSpinner.succeed(`GitHub Actions workflow added at ${path$1.relative(projectRoot, result.workflowPath)}.`);
272
+ recordCount(METRIC.installWorkflow, 1);
273
+ return true;
274
+ };
275
+ //#endregion
276
+ export { recordGate as a, ACTION_UPGRADE_EVENT as c, INSTALL_AGENTS_PREFERENCE_ID as d, ONBOARDING_EVENT as f, detectAvailableAgents as h, readPreference as i, CI_PITCH_EVENT as l, computeDefaultSelectedAgents as m, openUrl as n, runMigrations as o, SETUP_HINT_EVENT as p, isGatePending as r, writePreference as s, reportWorkflowResult as t, HANDOFF_TARGET_PREFERENCE_ID as u };
277
+
278
+ //# sourceMappingURL=report-workflow-result-CULZXneJ.js.map
279
+ //# debugId=2c4a070e-23f8-5a31-a4af-18688e5c19c0