@ceraph/react-native-mcp 0.2.0

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,134 @@
1
+ /**
2
+ * Manages React Native / Expo child processes (build and Metro dev server).
3
+ * Auto-detects whether the project uses Expo or bare React Native
4
+ * and runs the appropriate commands.
5
+ * Captures stdout/stderr into rolling buffers and parses errors.
6
+ */
7
+ import { type Warning, type AllErrors } from "./error-parser.js";
8
+ export interface BuildResult {
9
+ success: boolean;
10
+ errors: Array<{
11
+ file: string;
12
+ line: number;
13
+ message: string;
14
+ type: "build" | "link" | "pod";
15
+ }>;
16
+ warnings: Warning[];
17
+ output: string;
18
+ }
19
+ interface RunBuildOptions {
20
+ clean?: boolean;
21
+ device?: string;
22
+ }
23
+ interface StartMetroOptions {
24
+ port?: number;
25
+ clear?: boolean;
26
+ }
27
+ interface ConsoleOptions {
28
+ lines?: number;
29
+ level?: "all" | "error" | "warn" | "log";
30
+ }
31
+ export declare class RNManager {
32
+ private buildProcess;
33
+ private metroProcess;
34
+ private buildKilled;
35
+ private metroKilled;
36
+ private buildOutput;
37
+ private metroOutput;
38
+ private buildErrors;
39
+ private runtimeErrors;
40
+ private buildWarnings;
41
+ private metroWarnings;
42
+ private metroParser;
43
+ private readonly MAX_BUILD_LINES;
44
+ private readonly MAX_METRO_LINES;
45
+ private readonly MAX_ERRORS;
46
+ /** The working directory for commands. */
47
+ private cwd;
48
+ /** Path to the error file that triggers the Claude Code hook. */
49
+ private errorFilePath;
50
+ constructor(cwd: string);
51
+ /**
52
+ * Detect whether this is an Expo or bare React Native project.
53
+ * Checks for app.json with expo config or expo package in dependencies.
54
+ */
55
+ detectProjectType(): Promise<boolean>;
56
+ /**
57
+ * Write errors to .rn-errors.json so the Claude Code hook can detect them.
58
+ * Overwrites the file each time — the hook fires on any write.
59
+ */
60
+ private writeErrorFile;
61
+ /**
62
+ * Clear the error file (e.g., on successful build or process start).
63
+ */
64
+ private clearErrorFile;
65
+ /**
66
+ * Append a line to a rolling buffer, evicting oldest entries when full.
67
+ */
68
+ private pushLine;
69
+ /**
70
+ * Cap an error array at MAX_ERRORS, dropping oldest entries.
71
+ */
72
+ private capErrors;
73
+ /**
74
+ * Kill a child process gracefully (SIGTERM, then SIGKILL after timeout).
75
+ *
76
+ * The promise resolves only when the process actually exits, not when
77
+ * SIGKILL is sent. Resolving on the SIGKILL timer was a race: callers
78
+ * (e.g. `runBuild`) would spawn a new build while the old Xcode process
79
+ * was still releasing DerivedData / build locks, causing cryptic
80
+ * SIGABRT or "file locked" errors on the new build.
81
+ *
82
+ * A hard upper-bound timer (2s after SIGKILL) guards against the
83
+ * pathological case where a process refuses to die at all, so callers
84
+ * never hang forever.
85
+ */
86
+ private killProcess;
87
+ /**
88
+ * Run `npx expo prebuild --clean` synchronously (waits for exit).
89
+ */
90
+ private runPrebuildClean;
91
+ /**
92
+ * Build and run the app on an iOS device or simulator.
93
+ * Auto-detects Expo vs bare React Native and uses the appropriate command:
94
+ * - Expo: `npx expo run:ios`
95
+ * - Bare RN: `npx react-native run-ios`
96
+ *
97
+ * Optionally runs `npx expo prebuild --clean` first (Expo only).
98
+ */
99
+ runBuild(options?: RunBuildOptions): Promise<BuildResult>;
100
+ /**
101
+ * Start the Metro dev server.
102
+ * Auto-detects Expo vs bare React Native:
103
+ * - Expo: `npx expo start --dev-client`
104
+ * - Bare RN: `npx react-native start`
105
+ *
106
+ * Spawns Metro in the background and continuously captures output.
107
+ * Returns quickly once Metro shows signs of being ready (or after a timeout).
108
+ */
109
+ startMetro(options?: StartMetroOptions): Promise<{
110
+ success: boolean;
111
+ message: string;
112
+ }>;
113
+ /**
114
+ * Return all captured errors from both build and runtime contexts.
115
+ */
116
+ getErrors(): AllErrors;
117
+ /**
118
+ * Return recent console output from Metro, optionally filtered by level.
119
+ */
120
+ getConsole(options?: ConsoleOptions): string[];
121
+ /**
122
+ * Stop all managed Expo processes.
123
+ */
124
+ stopAll(): Promise<string[]>;
125
+ /**
126
+ * Get the raw build output buffer (for diagnostics).
127
+ */
128
+ getBuildOutput(): string[];
129
+ /**
130
+ * Get the raw metro output buffer (for diagnostics).
131
+ */
132
+ getMetroOutput(): string[];
133
+ }
134
+ export {};
@@ -0,0 +1,561 @@
1
+ /**
2
+ * Manages React Native / Expo child processes (build and Metro dev server).
3
+ * Auto-detects whether the project uses Expo or bare React Native
4
+ * and runs the appropriate commands.
5
+ * Captures stdout/stderr into rolling buffers and parses errors.
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { writeFile, access } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { parseBuildOutput, parseMetroOutput, MetroErrorParser, classifyLogLevel, } from "./error-parser.js";
11
+ export class RNManager {
12
+ buildProcess = null;
13
+ metroProcess = null;
14
+ buildKilled = false;
15
+ metroKilled = false;
16
+ buildOutput = [];
17
+ metroOutput = [];
18
+ buildErrors = [];
19
+ runtimeErrors = [];
20
+ buildWarnings = [];
21
+ metroWarnings = [];
22
+ // Parser state must persist across `onData` chunks so that an error
23
+ // whose message and stack trace arrive in separate chunks is still
24
+ // assembled into a single RuntimeError. Resetting on every chunk
25
+ // (the previous bug) caused the .rn-errors.json hook to fire with
26
+ // the message but no stack, or to drop multi-line errors entirely.
27
+ metroParser = new MetroErrorParser();
28
+ MAX_BUILD_LINES = 1000;
29
+ MAX_METRO_LINES = 500;
30
+ MAX_ERRORS = 100;
31
+ /** The working directory for commands. */
32
+ cwd;
33
+ /** Path to the error file that triggers the Claude Code hook. */
34
+ errorFilePath;
35
+ constructor(cwd) {
36
+ this.cwd = cwd;
37
+ this.errorFilePath = join(cwd, ".rn-errors.json");
38
+ }
39
+ /**
40
+ * Detect whether this is an Expo or bare React Native project.
41
+ * Checks for app.json with expo config or expo package in dependencies.
42
+ */
43
+ async detectProjectType() {
44
+ try {
45
+ // Check for app.json with expo key
46
+ const appJsonPath = join(this.cwd, "app.json");
47
+ await access(appJsonPath);
48
+ const appJson = JSON.parse(await (await import("node:fs/promises")).readFile(appJsonPath, "utf-8"));
49
+ if (appJson.expo) {
50
+ return true;
51
+ }
52
+ }
53
+ catch {
54
+ // No app.json or no expo key
55
+ }
56
+ try {
57
+ // Check for expo in package.json dependencies
58
+ const pkgPath = join(this.cwd, "package.json");
59
+ const pkg = JSON.parse(await (await import("node:fs/promises")).readFile(pkgPath, "utf-8"));
60
+ if (pkg.dependencies?.expo) {
61
+ return true;
62
+ }
63
+ }
64
+ catch {
65
+ // No package.json
66
+ }
67
+ return false;
68
+ }
69
+ /**
70
+ * Write errors to .rn-errors.json so the Claude Code hook can detect them.
71
+ * Overwrites the file each time — the hook fires on any write.
72
+ */
73
+ async writeErrorFile(errors) {
74
+ try {
75
+ await writeFile(this.errorFilePath, JSON.stringify({
76
+ timestamp: new Date().toISOString(),
77
+ errors: errors.map((e) => ({
78
+ message: e.message,
79
+ stack: e.stack,
80
+ timestamp: e.timestamp,
81
+ })),
82
+ }, null, 2), "utf-8");
83
+ }
84
+ catch {
85
+ // Non-critical — don't break the process over a file write
86
+ }
87
+ }
88
+ /**
89
+ * Clear the error file (e.g., on successful build or process start).
90
+ */
91
+ async clearErrorFile() {
92
+ try {
93
+ await writeFile(this.errorFilePath, JSON.stringify({ timestamp: new Date().toISOString(), errors: [] }), "utf-8");
94
+ }
95
+ catch {
96
+ // Non-critical
97
+ }
98
+ }
99
+ /**
100
+ * Append a line to a rolling buffer, evicting oldest entries when full.
101
+ */
102
+ pushLine(buffer, line, max) {
103
+ buffer.push(line);
104
+ if (buffer.length > max) {
105
+ buffer.splice(0, buffer.length - max);
106
+ }
107
+ }
108
+ /**
109
+ * Cap an error array at MAX_ERRORS, dropping oldest entries.
110
+ */
111
+ capErrors(arr) {
112
+ if (arr.length > this.MAX_ERRORS) {
113
+ arr.splice(0, arr.length - this.MAX_ERRORS);
114
+ }
115
+ }
116
+ /**
117
+ * Kill a child process gracefully (SIGTERM, then SIGKILL after timeout).
118
+ *
119
+ * The promise resolves only when the process actually exits, not when
120
+ * SIGKILL is sent. Resolving on the SIGKILL timer was a race: callers
121
+ * (e.g. `runBuild`) would spawn a new build while the old Xcode process
122
+ * was still releasing DerivedData / build locks, causing cryptic
123
+ * SIGABRT or "file locked" errors on the new build.
124
+ *
125
+ * A hard upper-bound timer (2s after SIGKILL) guards against the
126
+ * pathological case where a process refuses to die at all, so callers
127
+ * never hang forever.
128
+ */
129
+ async killProcess(proc) {
130
+ return new Promise((resolve) => {
131
+ if (!proc.pid || proc.exitCode !== null) {
132
+ resolve();
133
+ return;
134
+ }
135
+ let hardTimer = null;
136
+ const softTimer = setTimeout(() => {
137
+ try {
138
+ proc.kill("SIGKILL");
139
+ }
140
+ catch {
141
+ // Process may have already exited between checks.
142
+ }
143
+ // Wait for `exit` to fire after SIGKILL, but bound the wait so
144
+ // callers don't hang on a process that refuses to die.
145
+ hardTimer = setTimeout(() => {
146
+ proc.off("exit", onExit);
147
+ resolve();
148
+ }, 2000);
149
+ }, 5000);
150
+ const onExit = () => {
151
+ clearTimeout(softTimer);
152
+ if (hardTimer)
153
+ clearTimeout(hardTimer);
154
+ resolve();
155
+ };
156
+ proc.once("exit", onExit);
157
+ try {
158
+ proc.kill("SIGTERM");
159
+ }
160
+ catch {
161
+ // Process already exited or PID is gone — treat as done. Detach
162
+ // the exit listener so it doesn't fire stale on a stop/start cycle.
163
+ proc.off("exit", onExit);
164
+ clearTimeout(softTimer);
165
+ resolve();
166
+ return;
167
+ }
168
+ // TOCTOU recovery: if the process exited between the entry guard
169
+ // and the `once("exit")` registration above, `kill()` returns false
170
+ // (no throw) and our listener never fires for the already-emitted
171
+ // exit. Re-check `exitCode` here so we don't wait the full 7s on
172
+ // the hard timer.
173
+ if (proc.exitCode !== null) {
174
+ proc.off("exit", onExit);
175
+ clearTimeout(softTimer);
176
+ resolve();
177
+ }
178
+ });
179
+ }
180
+ /**
181
+ * Run `npx expo prebuild --clean` synchronously (waits for exit).
182
+ */
183
+ runPrebuildClean() {
184
+ return new Promise((resolve) => {
185
+ const lines = [];
186
+ const proc = spawn("npx", ["expo", "prebuild", "--clean"], {
187
+ cwd: this.cwd,
188
+ stdio: ["ignore", "pipe", "pipe"],
189
+ });
190
+ const onData = (data) => {
191
+ const text = data.toString();
192
+ for (const line of text.split("\n")) {
193
+ if (line.trim())
194
+ lines.push(line);
195
+ }
196
+ };
197
+ proc.stdout?.on("data", onData);
198
+ proc.stderr?.on("data", onData);
199
+ // Node fires both `error` and `exit` on a failed spawn. Guard so
200
+ // the second handler doesn't run a stale resolve / output dump.
201
+ let resolved = false;
202
+ proc.on("error", (err) => {
203
+ if (resolved)
204
+ return;
205
+ resolved = true;
206
+ resolve({
207
+ success: false,
208
+ output: `Failed to spawn prebuild: ${err.message}`,
209
+ });
210
+ });
211
+ proc.on("exit", (code) => {
212
+ if (resolved)
213
+ return;
214
+ resolved = true;
215
+ resolve({
216
+ success: code === 0,
217
+ output: lines.join("\n"),
218
+ });
219
+ });
220
+ });
221
+ }
222
+ /**
223
+ * Build and run the app on an iOS device or simulator.
224
+ * Auto-detects Expo vs bare React Native and uses the appropriate command:
225
+ * - Expo: `npx expo run:ios`
226
+ * - Bare RN: `npx react-native run-ios`
227
+ *
228
+ * Optionally runs `npx expo prebuild --clean` first (Expo only).
229
+ */
230
+ async runBuild(options = {}) {
231
+ const isExpo = await this.detectProjectType();
232
+ // Kill any existing build process
233
+ if (this.buildProcess) {
234
+ this.buildKilled = true;
235
+ await this.killProcess(this.buildProcess);
236
+ this.buildProcess = null;
237
+ }
238
+ // Reset state
239
+ this.buildKilled = false;
240
+ this.buildOutput = [];
241
+ this.buildErrors = [];
242
+ this.buildWarnings = [];
243
+ // Run prebuild --clean if requested (Expo only)
244
+ if (options.clean) {
245
+ if (!isExpo) {
246
+ return {
247
+ success: false,
248
+ errors: [
249
+ {
250
+ file: "",
251
+ line: 0,
252
+ message: "prebuild --clean is only available for Expo projects.",
253
+ type: "build",
254
+ },
255
+ ],
256
+ warnings: [],
257
+ output: "",
258
+ };
259
+ }
260
+ const prebuildResult = await this.runPrebuildClean();
261
+ if (!prebuildResult.success) {
262
+ return {
263
+ success: false,
264
+ errors: [
265
+ {
266
+ file: "",
267
+ line: 0,
268
+ message: `prebuild --clean failed:\n${prebuildResult.output}`,
269
+ type: "build",
270
+ },
271
+ ],
272
+ warnings: [],
273
+ output: prebuildResult.output,
274
+ };
275
+ }
276
+ // Add prebuild output to build output
277
+ for (const line of prebuildResult.output.split("\n")) {
278
+ this.pushLine(this.buildOutput, line, this.MAX_BUILD_LINES);
279
+ }
280
+ }
281
+ // Construct the build command based on project type
282
+ const args = isExpo ? ["expo", "run:ios"] : ["react-native", "run-ios"];
283
+ if (options.device) {
284
+ args.push(isExpo ? "--device" : "--udid", options.device);
285
+ }
286
+ return new Promise((resolve) => {
287
+ const proc = spawn("npx", args, {
288
+ cwd: this.cwd,
289
+ stdio: ["ignore", "pipe", "pipe"],
290
+ });
291
+ this.buildProcess = proc;
292
+ const onData = (data) => {
293
+ const text = data.toString();
294
+ for (const line of text.split("\n")) {
295
+ if (line.trim()) {
296
+ this.pushLine(this.buildOutput, line, this.MAX_BUILD_LINES);
297
+ }
298
+ }
299
+ };
300
+ proc.stdout?.on("data", onData);
301
+ proc.stderr?.on("data", onData);
302
+ // Node fires both `error` and `exit` on a spawn failure. Guard so
303
+ // the `exit` handler doesn't re-parse the (likely empty) output
304
+ // and overwrite `this.buildErrors` / `this.buildWarnings` after
305
+ // the caller has already received the failure result.
306
+ let resolved = false;
307
+ proc.on("error", (err) => {
308
+ if (resolved)
309
+ return;
310
+ resolved = true;
311
+ this.buildProcess = null;
312
+ resolve({
313
+ success: false,
314
+ errors: [
315
+ {
316
+ file: "",
317
+ line: 0,
318
+ message: `Failed to spawn ${isExpo ? "expo run:ios" : "react-native run-ios"}: ${err.message}`,
319
+ type: "build",
320
+ },
321
+ ],
322
+ warnings: [],
323
+ output: this.buildOutput.join("\n"),
324
+ });
325
+ });
326
+ proc.on("exit", (code) => {
327
+ if (resolved)
328
+ return;
329
+ resolved = true;
330
+ this.buildProcess = null;
331
+ if (this.buildKilled) {
332
+ resolve({
333
+ success: false,
334
+ errors: [],
335
+ warnings: [],
336
+ output: "Build was cancelled.",
337
+ });
338
+ return;
339
+ }
340
+ // Parse all captured output
341
+ const parsed = parseBuildOutput(this.buildOutput);
342
+ this.buildErrors = parsed.errors;
343
+ this.buildWarnings = parsed.warnings;
344
+ this.capErrors(this.buildErrors);
345
+ const success = code === 0 && !parsed.buildFailed;
346
+ resolve({
347
+ success,
348
+ errors: parsed.errors.map((e) => ({
349
+ file: e.file,
350
+ line: e.line,
351
+ message: e.message,
352
+ type: e.type,
353
+ })),
354
+ warnings: parsed.warnings,
355
+ output: this.buildOutput.slice(-100).join("\n"), // Last 100 lines
356
+ });
357
+ });
358
+ });
359
+ }
360
+ /**
361
+ * Start the Metro dev server.
362
+ * Auto-detects Expo vs bare React Native:
363
+ * - Expo: `npx expo start --dev-client`
364
+ * - Bare RN: `npx react-native start`
365
+ *
366
+ * Spawns Metro in the background and continuously captures output.
367
+ * Returns quickly once Metro shows signs of being ready (or after a timeout).
368
+ */
369
+ async startMetro(options = {}) {
370
+ const isExpo = await this.detectProjectType();
371
+ // Kill existing Metro process
372
+ if (this.metroProcess) {
373
+ this.metroKilled = true;
374
+ await this.killProcess(this.metroProcess);
375
+ this.metroProcess = null;
376
+ }
377
+ // Reset state
378
+ this.metroKilled = false;
379
+ this.metroOutput = [];
380
+ this.runtimeErrors = [];
381
+ this.metroWarnings = [];
382
+ this.metroParser = new MetroErrorParser();
383
+ const args = isExpo
384
+ ? ["expo", "start", "--dev-client"]
385
+ : ["react-native", "start"];
386
+ if (options.port) {
387
+ args.push("--port", String(options.port));
388
+ }
389
+ if (options.clear) {
390
+ args.push(isExpo ? "--clear" : "--reset-cache");
391
+ }
392
+ await this.clearErrorFile();
393
+ return new Promise((resolve) => {
394
+ const proc = spawn("npx", args, {
395
+ cwd: this.cwd,
396
+ stdio: ["ignore", "pipe", "pipe"],
397
+ });
398
+ this.metroProcess = proc;
399
+ let resolved = false;
400
+ const onData = (data) => {
401
+ const text = data.toString();
402
+ // Collect lines from this chunk and parse them as a batch.
403
+ // The parser is stateful, so an error whose message and stack
404
+ // straddle chunk boundaries is still assembled correctly.
405
+ const chunkLines = [];
406
+ for (const line of text.split("\n")) {
407
+ if (line.trim()) {
408
+ this.pushLine(this.metroOutput, line, this.MAX_METRO_LINES);
409
+ chunkLines.push(line);
410
+ }
411
+ }
412
+ if (chunkLines.length > 0) {
413
+ const parsed = this.metroParser.parse(chunkLines);
414
+ if (parsed.runtimeErrors.length > 0) {
415
+ this.runtimeErrors.push(...parsed.runtimeErrors);
416
+ this.capErrors(this.runtimeErrors);
417
+ this.writeErrorFile(this.runtimeErrors);
418
+ }
419
+ if (parsed.warnings.length > 0) {
420
+ this.metroWarnings.push(...parsed.warnings);
421
+ }
422
+ }
423
+ // Detect Metro ready
424
+ if (!resolved && text.includes("Metro waiting on")) {
425
+ resolved = true;
426
+ resolve({
427
+ success: true,
428
+ message: "Metro dev server started successfully.",
429
+ });
430
+ }
431
+ };
432
+ proc.stdout?.on("data", onData);
433
+ proc.stderr?.on("data", onData);
434
+ proc.on("error", (err) => {
435
+ this.metroProcess = null;
436
+ if (!resolved) {
437
+ resolved = true;
438
+ resolve({
439
+ success: false,
440
+ message: `Failed to start Metro: ${err.message}`,
441
+ });
442
+ }
443
+ });
444
+ proc.on("exit", (code) => {
445
+ this.metroProcess = null;
446
+ // Flush any error still being assembled in the streaming parser
447
+ // so it surfaces in `.rn-errors.json` for the Claude Code hook.
448
+ const flushed = this.metroParser.flush();
449
+ if (flushed.runtimeErrors.length > 0) {
450
+ this.runtimeErrors.push(...flushed.runtimeErrors);
451
+ this.capErrors(this.runtimeErrors);
452
+ this.writeErrorFile(this.runtimeErrors);
453
+ }
454
+ if (this.metroKilled) {
455
+ if (!resolved) {
456
+ resolved = true;
457
+ resolve({
458
+ success: false,
459
+ message: "Metro was cancelled.",
460
+ });
461
+ }
462
+ return;
463
+ }
464
+ if (!resolved) {
465
+ resolved = true;
466
+ resolve({
467
+ success: false,
468
+ message: `Metro exited unexpectedly with code ${code}.`,
469
+ });
470
+ }
471
+ });
472
+ // If Metro doesn't show "ready" within 30s, resolve anyway
473
+ // -- it may still be starting up (pod install, etc.)
474
+ setTimeout(() => {
475
+ if (!resolved) {
476
+ resolved = true;
477
+ resolve({
478
+ success: true,
479
+ message: "Metro process started but has not confirmed readiness yet. " +
480
+ "It may still be initializing. Use rn_get_console to check.",
481
+ });
482
+ }
483
+ }, 30_000);
484
+ });
485
+ }
486
+ /**
487
+ * Return all captured errors from both build and runtime contexts.
488
+ */
489
+ getErrors() {
490
+ // Only re-parse the buffer when Metro is NOT running. While the
491
+ // streaming parser holds in-progress state (header received, stack
492
+ // not yet), an independent re-parse would `flush()` a stackless
493
+ // copy of that same error and surface it as a duplicate-without-stack.
494
+ // Once Metro exits, the streaming parser has already been flushed
495
+ // in the `proc.on("exit")` handler, and re-parsing the buffer is a
496
+ // safe fallback for anything still missing.
497
+ const metroParsed = this.metroProcess === null
498
+ ? parseMetroOutput(this.metroOutput)
499
+ : { runtimeErrors: [], warnings: [] };
500
+ return {
501
+ buildErrors: this.buildErrors.map((e) => ({
502
+ file: e.file,
503
+ line: e.line,
504
+ column: e.column,
505
+ message: e.message,
506
+ severity: e.severity,
507
+ type: e.type,
508
+ })),
509
+ runtimeErrors: this.runtimeErrors.length > 0
510
+ ? this.runtimeErrors
511
+ : metroParsed.runtimeErrors,
512
+ warnings: [...this.buildWarnings, ...this.metroWarnings],
513
+ };
514
+ }
515
+ /**
516
+ * Return recent console output from Metro, optionally filtered by level.
517
+ */
518
+ getConsole(options = {}) {
519
+ const { lines = 50, level = "all" } = options;
520
+ let output = this.metroOutput;
521
+ if (level !== "all") {
522
+ output = output.filter((line) => {
523
+ const classified = classifyLogLevel(line);
524
+ return classified === level;
525
+ });
526
+ }
527
+ // Return the last N lines
528
+ return output.slice(-lines);
529
+ }
530
+ /**
531
+ * Stop all managed Expo processes.
532
+ */
533
+ async stopAll() {
534
+ const stopped = [];
535
+ if (this.buildProcess) {
536
+ this.buildKilled = true;
537
+ await this.killProcess(this.buildProcess);
538
+ this.buildProcess = null;
539
+ stopped.push("build");
540
+ }
541
+ if (this.metroProcess) {
542
+ this.metroKilled = true;
543
+ await this.killProcess(this.metroProcess);
544
+ this.metroProcess = null;
545
+ stopped.push("metro");
546
+ }
547
+ return stopped;
548
+ }
549
+ /**
550
+ * Get the raw build output buffer (for diagnostics).
551
+ */
552
+ getBuildOutput() {
553
+ return [...this.buildOutput];
554
+ }
555
+ /**
556
+ * Get the raw metro output buffer (for diagnostics).
557
+ */
558
+ getMetroOutput() {
559
+ return [...this.metroOutput];
560
+ }
561
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ceraph/react-native-mcp — MCP server for React Native / Expo development workflow.
4
+ *
5
+ * Auto-detects Expo vs bare React Native projects and uses the appropriate
6
+ * commands. Provides tools for building, running, error capture, screen
7
+ * interaction, and prebuild detection.
8
+ */
9
+ export {};