@ceraph/react-native-mcp 0.2.2 → 0.3.2

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.
Files changed (124) hide show
  1. package/LICENSE +116 -15
  2. package/README.md +79 -77
  3. package/assets/default.png +0 -0
  4. package/dist/app-lifecycle.d.ts +50 -0
  5. package/dist/app-lifecycle.js +487 -0
  6. package/dist/camera-image-writer.d.ts +43 -0
  7. package/dist/camera-image-writer.js +280 -0
  8. package/dist/camera-registry-sync.d.ts +18 -0
  9. package/dist/camera-registry-sync.js +117 -0
  10. package/dist/cli.d.ts +0 -7
  11. package/dist/cli.js +41 -9
  12. package/dist/device-autonomy.d.ts +30 -0
  13. package/dist/device-autonomy.js +117 -0
  14. package/dist/error-parser.d.ts +6 -26
  15. package/dist/error-parser.js +4 -74
  16. package/dist/expo-manager.d.ts +2 -74
  17. package/dist/expo-manager.js +11 -125
  18. package/dist/index.d.ts +0 -7
  19. package/dist/index.js +1266 -56
  20. package/dist/init/ast-camera.d.ts +29 -0
  21. package/dist/init/ast-camera.js +267 -0
  22. package/dist/init/ast-layout.d.ts +15 -0
  23. package/dist/init/ast-layout.js +167 -0
  24. package/dist/init/claude-hook-constants.d.ts +9 -0
  25. package/dist/init/claude-hook-constants.js +91 -0
  26. package/dist/init/lan-ip.d.ts +11 -0
  27. package/dist/init/lan-ip.js +51 -0
  28. package/dist/init/monorepo.d.ts +13 -0
  29. package/dist/init/monorepo.js +185 -0
  30. package/dist/init/oauth.d.ts +52 -0
  31. package/dist/init/oauth.js +220 -0
  32. package/dist/init/package-manager.d.ts +11 -0
  33. package/dist/init/package-manager.js +60 -0
  34. package/dist/init/prompt.d.ts +12 -0
  35. package/dist/init/prompt.js +68 -0
  36. package/dist/init/shell-profile.d.ts +22 -0
  37. package/dist/init/shell-profile.js +85 -0
  38. package/dist/init/steps.d.ts +135 -0
  39. package/dist/init/steps.js +399 -0
  40. package/dist/init/url-scheme.d.ts +42 -0
  41. package/dist/init/url-scheme.js +187 -0
  42. package/dist/init/walkthrough.d.ts +76 -0
  43. package/dist/init/walkthrough.js +340 -0
  44. package/dist/init.d.ts +7 -7
  45. package/dist/init.js +280 -120
  46. package/dist/iproxy-manager.d.ts +32 -0
  47. package/dist/iproxy-manager.js +216 -0
  48. package/dist/mac-caffeinate.d.ts +10 -0
  49. package/dist/mac-caffeinate.js +56 -0
  50. package/dist/permission-interceptor.d.ts +29 -0
  51. package/dist/permission-interceptor.js +185 -0
  52. package/dist/prebuild-detector.d.ts +0 -30
  53. package/dist/prebuild-detector.js +1 -42
  54. package/dist/preflight.d.ts +34 -0
  55. package/dist/preflight.js +847 -0
  56. package/dist/screen.d.ts +132 -43
  57. package/dist/screen.js +668 -94
  58. package/dist/shim/boot.d.ts +41 -0
  59. package/dist/shim/boot.js +141 -0
  60. package/dist/shim/camera.d.ts +22 -0
  61. package/dist/shim/camera.js +62 -0
  62. package/dist/shim/config.d.ts +6 -0
  63. package/dist/shim/config.js +56 -0
  64. package/dist/shim/deep-link.d.ts +1 -0
  65. package/dist/shim/deep-link.js +25 -0
  66. package/dist/shim/dev-guard.d.ts +1 -0
  67. package/dist/shim/dev-guard.js +3 -0
  68. package/dist/shim/error-handler.d.ts +20 -0
  69. package/dist/shim/error-handler.js +66 -0
  70. package/dist/shim/fetch-interceptor.d.ts +13 -0
  71. package/dist/shim/fetch-interceptor.js +93 -0
  72. package/dist/shim/index.d.ts +6 -0
  73. package/dist/shim/index.js +6 -0
  74. package/dist/shim/keep-awake.d.ts +13 -0
  75. package/dist/shim/keep-awake.js +118 -0
  76. package/dist/shim/reload.d.ts +23 -0
  77. package/dist/shim/reload.js +76 -0
  78. package/dist/shim/signal-capture.d.ts +11 -0
  79. package/dist/shim/signal-capture.js +15 -0
  80. package/dist/shim/signal-transport.d.ts +17 -0
  81. package/dist/shim/signal-transport.js +43 -0
  82. package/dist/signal-listener.d.ts +27 -0
  83. package/dist/signal-listener.js +135 -0
  84. package/dist/simulator-boot.d.ts +52 -0
  85. package/dist/simulator-boot.js +227 -0
  86. package/dist/target.d.ts +48 -0
  87. package/dist/target.js +267 -0
  88. package/dist/uninstall/cli-runner.d.ts +32 -0
  89. package/dist/uninstall/cli-runner.js +223 -0
  90. package/dist/uninstall/footprint.d.ts +40 -0
  91. package/dist/uninstall/footprint.js +288 -0
  92. package/dist/uninstall/mcp-tools.d.ts +14 -0
  93. package/dist/uninstall/mcp-tools.js +175 -0
  94. package/dist/uninstall/revert-auth.d.ts +22 -0
  95. package/dist/uninstall/revert-auth.js +31 -0
  96. package/dist/uninstall/revert-boot.d.ts +24 -0
  97. package/dist/uninstall/revert-boot.js +242 -0
  98. package/dist/uninstall/revert-camera.d.ts +12 -0
  99. package/dist/uninstall/revert-camera.js +199 -0
  100. package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
  101. package/dist/uninstall/revert-ceraph-dir.js +38 -0
  102. package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
  103. package/dist/uninstall/revert-claude-hooks.js +191 -0
  104. package/dist/uninstall/revert-gitignore.d.ts +17 -0
  105. package/dist/uninstall/revert-gitignore.js +43 -0
  106. package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
  107. package/dist/uninstall/revert-mcp-clients.js +194 -0
  108. package/dist/uninstall/revert-package.d.ts +34 -0
  109. package/dist/uninstall/revert-package.js +98 -0
  110. package/dist/uninstall/revert-scheme.d.ts +36 -0
  111. package/dist/uninstall/revert-scheme.js +139 -0
  112. package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
  113. package/dist/uninstall/revert-signal-host-env.js +61 -0
  114. package/dist/uninstall/walkthrough.d.ts +80 -0
  115. package/dist/uninstall/walkthrough.js +1244 -0
  116. package/dist/utils/atomic-write.d.ts +1 -0
  117. package/dist/utils/atomic-write.js +30 -0
  118. package/dist/wait-for-device.d.ts +68 -0
  119. package/dist/wait-for-device.js +368 -0
  120. package/dist/wda-manager.d.ts +38 -0
  121. package/dist/wda-manager.js +186 -0
  122. package/dist/wda-simulator.d.ts +28 -0
  123. package/dist/wda-simulator.js +257 -0
  124. package/package.json +59 -5
@@ -0,0 +1 @@
1
+ export declare function writeFileAtomic(path: string, contents: string | Buffer): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { chmod, rename, stat, unlink, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ export async function writeFileAtomic(path, contents) {
4
+ const dir = dirname(path);
5
+ const tmp = join(dir, `${path.slice(dir.length + 1)}.tmp.${process.pid}.${Math.random()
6
+ .toString(36)
7
+ .slice(2)}`);
8
+ let existingMode;
9
+ try {
10
+ const st = await stat(path);
11
+ existingMode = st.mode & 0o777;
12
+ }
13
+ catch {
14
+ }
15
+ try {
16
+ await writeFile(tmp, contents, "utf-8");
17
+ if (existingMode !== undefined) {
18
+ await chmod(tmp, existingMode);
19
+ }
20
+ await rename(tmp, path);
21
+ }
22
+ catch (err) {
23
+ try {
24
+ await unlink(tmp);
25
+ }
26
+ catch {
27
+ }
28
+ throw err;
29
+ }
30
+ }
@@ -0,0 +1,68 @@
1
+ export interface WaitForDeviceOptions {
2
+ signal?: AbortSignal;
3
+ fallbackPollIntervalMs?: number;
4
+ timeoutMs?: number;
5
+ }
6
+ export interface WaitForDeviceResult {
7
+ deviceFound: true;
8
+ udid: string;
9
+ name?: string;
10
+ waitedMs: number;
11
+ source: "usbmuxd" | "polling-fallback";
12
+ }
13
+ export type WaitForDeviceOpts = WaitForDeviceOptions;
14
+ export interface CommandResult {
15
+ code: number;
16
+ stdout: string;
17
+ stderr: string;
18
+ }
19
+ export interface UsbmuxdSocketLike {
20
+ write(buf: Buffer): boolean;
21
+ on(event: "data", cb: (chunk: Buffer) => void): this;
22
+ on(event: "error", cb: (err: Error) => void): this;
23
+ on(event: "close", cb: () => void): this;
24
+ destroy(err?: Error): void;
25
+ }
26
+ export interface WaitForDeviceDeps {
27
+ runner: (cmd: string, args: string[], timeoutMs?: number) => Promise<CommandResult>;
28
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
29
+ now?: () => number;
30
+ openSocket?: () => Promise<UsbmuxdSocketLike>;
31
+ }
32
+ export declare const defaultWaitForDeviceDeps: WaitForDeviceDeps;
33
+ export declare function buildListenRequest(tag?: number): Buffer;
34
+ export interface UsbmuxdMessage {
35
+ payload: Record<string, unknown>;
36
+ }
37
+ export declare class UsbmuxdFramer {
38
+ private buf;
39
+ feed(chunk: Buffer): UsbmuxdMessage[];
40
+ }
41
+ interface DevicectlDevice {
42
+ identifier?: string;
43
+ hardwareProperties?: {
44
+ platform?: string;
45
+ };
46
+ connectionProperties?: {
47
+ tunnelState?: string;
48
+ transportType?: string;
49
+ };
50
+ deviceProperties?: {
51
+ name?: string;
52
+ };
53
+ }
54
+ interface DevicectlListPayload {
55
+ result?: {
56
+ devices?: DevicectlDevice[];
57
+ };
58
+ }
59
+ export declare function pickFirstIosDevice(payload: DevicectlListPayload): {
60
+ udid: string;
61
+ name: string;
62
+ } | null;
63
+ export declare function extractAttachedDevice(payload: Record<string, unknown>): {
64
+ udid: string;
65
+ name?: string;
66
+ } | null;
67
+ export declare function waitForDevice(opts?: WaitForDeviceOptions, deps?: WaitForDeviceDeps): Promise<WaitForDeviceResult>;
68
+ export {};
@@ -0,0 +1,368 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as net from "node:net";
3
+ import * as plist from "plist";
4
+ async function defaultRunner(cmd, args, timeoutMs = 15_000) {
5
+ return new Promise((resolve) => {
6
+ let stdout = "";
7
+ let stderr = "";
8
+ let child;
9
+ try {
10
+ child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
11
+ }
12
+ catch (err) {
13
+ resolve({
14
+ code: 127,
15
+ stdout: "",
16
+ stderr: err instanceof Error ? err.message : String(err),
17
+ });
18
+ return;
19
+ }
20
+ const timer = setTimeout(() => {
21
+ try {
22
+ child.kill("SIGKILL");
23
+ }
24
+ catch {
25
+ }
26
+ }, timeoutMs);
27
+ child.stdout?.on("data", (d) => {
28
+ stdout += d.toString();
29
+ });
30
+ child.stderr?.on("data", (d) => {
31
+ stderr += d.toString();
32
+ });
33
+ let settled = false;
34
+ child.on("error", (err) => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ clearTimeout(timer);
39
+ resolve({
40
+ code: 127,
41
+ stdout,
42
+ stderr: stderr + (err.message ?? String(err)),
43
+ });
44
+ });
45
+ child.on("exit", (code) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ clearTimeout(timer);
50
+ resolve({ code: code ?? 1, stdout, stderr });
51
+ });
52
+ });
53
+ }
54
+ function defaultSleep(ms, signal) {
55
+ return new Promise((resolve, reject) => {
56
+ if (signal?.aborted) {
57
+ reject(new Error("aborted"));
58
+ return;
59
+ }
60
+ const timer = setTimeout(() => {
61
+ signal?.removeEventListener("abort", onAbort);
62
+ resolve();
63
+ }, ms);
64
+ const onAbort = () => {
65
+ clearTimeout(timer);
66
+ signal?.removeEventListener("abort", onAbort);
67
+ reject(new Error("aborted"));
68
+ };
69
+ signal?.addEventListener("abort", onAbort, { once: true });
70
+ });
71
+ }
72
+ function defaultOpenSocket() {
73
+ return new Promise((resolve, reject) => {
74
+ const socket = net.createConnection("/var/run/usbmuxd");
75
+ let settled = false;
76
+ socket.once("connect", () => {
77
+ if (settled)
78
+ return;
79
+ settled = true;
80
+ socket.removeListener("error", onError);
81
+ resolve(socket);
82
+ });
83
+ const onError = (err) => {
84
+ if (settled)
85
+ return;
86
+ settled = true;
87
+ try {
88
+ socket.destroy();
89
+ }
90
+ catch {
91
+ }
92
+ reject(err);
93
+ };
94
+ socket.once("error", onError);
95
+ });
96
+ }
97
+ export const defaultWaitForDeviceDeps = {
98
+ runner: defaultRunner,
99
+ sleep: defaultSleep,
100
+ now: () => Date.now(),
101
+ openSocket: defaultOpenSocket,
102
+ };
103
+ const USBMUXD_VERSION_PLIST = 1;
104
+ const USBMUXD_MESSAGE_PLIST = 8;
105
+ const USBMUXD_HEADER_BYTES = 16;
106
+ export function buildListenRequest(tag = 1) {
107
+ const payload = plist.build({
108
+ MessageType: "Listen",
109
+ ClientVersionString: "ceraph-react-native-mcp",
110
+ ProgName: "ceraph-react-native-mcp",
111
+ kLibUSBMuxVersion: 3,
112
+ });
113
+ const payloadBuf = Buffer.from(payload, "utf8");
114
+ const header = Buffer.alloc(USBMUXD_HEADER_BYTES);
115
+ header.writeUInt32LE(USBMUXD_HEADER_BYTES + payloadBuf.length, 0);
116
+ header.writeUInt32LE(USBMUXD_VERSION_PLIST, 4);
117
+ header.writeUInt32LE(USBMUXD_MESSAGE_PLIST, 8);
118
+ header.writeUInt32LE(tag, 12);
119
+ return Buffer.concat([header, payloadBuf]);
120
+ }
121
+ export class UsbmuxdFramer {
122
+ buf = Buffer.alloc(0);
123
+ feed(chunk) {
124
+ this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
125
+ const out = [];
126
+ while (this.buf.length >= USBMUXD_HEADER_BYTES) {
127
+ const length = this.buf.readUInt32LE(0);
128
+ if (length < USBMUXD_HEADER_BYTES || length > 1_048_576) {
129
+ this.buf = Buffer.alloc(0);
130
+ return out;
131
+ }
132
+ if (this.buf.length < length) {
133
+ return out;
134
+ }
135
+ const body = this.buf.subarray(USBMUXD_HEADER_BYTES, length);
136
+ this.buf = this.buf.subarray(length);
137
+ const parsed = parsePlistSafe(body);
138
+ if (parsed)
139
+ out.push({ payload: parsed });
140
+ }
141
+ return out;
142
+ }
143
+ }
144
+ function parsePlistSafe(body) {
145
+ try {
146
+ const text = body.toString("utf8");
147
+ const parsed = plist.parse(text);
148
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
149
+ return parsed;
150
+ }
151
+ return null;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ export function pickFirstIosDevice(payload) {
158
+ const devices = payload.result?.devices ?? [];
159
+ if (devices.length === 0)
160
+ return null;
161
+ const isIos = (d) => (d.hardwareProperties?.platform?.toLowerCase() ?? "").includes("ios");
162
+ const connected = devices.find((d) => {
163
+ const tunnel = d.connectionProperties?.tunnelState?.toLowerCase() ?? "";
164
+ return isIos(d) && tunnel === "connected";
165
+ });
166
+ const pick = connected ?? devices.find(isIos);
167
+ if (!pick?.identifier)
168
+ return null;
169
+ return {
170
+ udid: pick.identifier,
171
+ name: pick.deviceProperties?.name ?? pick.identifier,
172
+ };
173
+ }
174
+ async function probeOnce(deps) {
175
+ const res = await deps.runner("xcrun", ["devicectl", "list", "devices", "--json-output", "-"], 8_000);
176
+ if (res.code !== 0)
177
+ return null;
178
+ try {
179
+ const parsed = JSON.parse(res.stdout);
180
+ return pickFirstIosDevice(parsed);
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
186
+ export function extractAttachedDevice(payload) {
187
+ if (payload.MessageType !== "Attached")
188
+ return null;
189
+ const props = payload.Properties;
190
+ if (!props || typeof props !== "object")
191
+ return null;
192
+ const serial = props.SerialNumber;
193
+ if (typeof serial !== "string" || serial.length === 0)
194
+ return null;
195
+ const rawName = props.DeviceName;
196
+ const name = typeof rawName === "string" && rawName.length > 0 ? rawName : undefined;
197
+ return { udid: serial, name };
198
+ }
199
+ export async function waitForDevice(opts = {}, deps = defaultWaitForDeviceDeps) {
200
+ const signal = opts.signal;
201
+ const now = deps.now ?? (() => Date.now());
202
+ if (signal?.aborted)
203
+ throw new Error("aborted");
204
+ const start = now();
205
+ const initial = await probeOnce(deps);
206
+ if (initial) {
207
+ return {
208
+ deviceFound: true,
209
+ udid: initial.udid,
210
+ name: initial.name,
211
+ waitedMs: 0,
212
+ source: "usbmuxd",
213
+ };
214
+ }
215
+ if (signal?.aborted)
216
+ throw new Error("aborted");
217
+ const openSocket = deps.openSocket ?? defaultOpenSocket;
218
+ let socket;
219
+ try {
220
+ socket = await openSocket();
221
+ }
222
+ catch {
223
+ process.stderr.write("[ceraph][rn_wait_for_device] usbmuxd socket unavailable; " +
224
+ "falling back to polling. Install Xcode for sub-second device detection.\n");
225
+ return pollUntilDevicePresent(opts, deps, start);
226
+ }
227
+ return listenForAttached(socket, opts, deps, start);
228
+ }
229
+ function listenForAttached(socket, opts, deps, start) {
230
+ const signal = opts.signal;
231
+ const now = deps.now ?? (() => Date.now());
232
+ return new Promise((resolve, reject) => {
233
+ const framer = new UsbmuxdFramer();
234
+ let settled = false;
235
+ let timer;
236
+ const finalize = () => {
237
+ settled = true;
238
+ if (timer)
239
+ clearTimeout(timer);
240
+ if (signal)
241
+ signal.removeEventListener("abort", onAbort);
242
+ try {
243
+ socket.destroy();
244
+ }
245
+ catch {
246
+ }
247
+ };
248
+ const settleResolve = (v) => {
249
+ if (settled)
250
+ return;
251
+ finalize();
252
+ resolve(v);
253
+ };
254
+ const settleReject = (e) => {
255
+ if (settled)
256
+ return;
257
+ finalize();
258
+ reject(e);
259
+ };
260
+ const onAbort = () => settleReject(new Error("aborted"));
261
+ const onError = (_err) => {
262
+ if (settled)
263
+ return;
264
+ finalize();
265
+ pollUntilDevicePresent(opts, deps, start).then(resolve, reject);
266
+ };
267
+ const onClose = () => {
268
+ if (settled)
269
+ return;
270
+ finalize();
271
+ pollUntilDevicePresent(opts, deps, start).then(resolve, reject);
272
+ };
273
+ const onData = (chunk) => {
274
+ if (settled)
275
+ return;
276
+ const messages = framer.feed(chunk);
277
+ for (const msg of messages) {
278
+ const device = extractAttachedDevice(msg.payload);
279
+ if (device) {
280
+ settleResolve({
281
+ deviceFound: true,
282
+ udid: device.udid,
283
+ name: device.name,
284
+ waitedMs: now() - start,
285
+ source: "usbmuxd",
286
+ });
287
+ return;
288
+ }
289
+ }
290
+ };
291
+ socket.on("data", onData);
292
+ socket.on("error", onError);
293
+ socket.on("close", onClose);
294
+ if (signal) {
295
+ signal.addEventListener("abort", onAbort, { once: true });
296
+ }
297
+ if (typeof opts.timeoutMs === "number" && opts.timeoutMs > 0) {
298
+ const remaining = opts.timeoutMs - (now() - start);
299
+ const deadline = Math.max(remaining, 0);
300
+ timer = setTimeout(() => {
301
+ settleReject(new Error(formatTimeoutMessage(opts.timeoutMs)));
302
+ }, deadline);
303
+ }
304
+ try {
305
+ socket.write(buildListenRequest());
306
+ }
307
+ catch (err) {
308
+ if (settled)
309
+ return;
310
+ finalize();
311
+ pollUntilDevicePresent(opts, deps, start).then(resolve, reject);
312
+ }
313
+ });
314
+ }
315
+ function formatTimeoutMessage(timeoutMs) {
316
+ const minutes = Math.round(timeoutMs / 60_000);
317
+ const minutesLabel = minutes === 1 ? "1 minute" : `${minutes} minutes`;
318
+ return (`No device connected after ${minutesLabel}. Connect your iPhone ` +
319
+ "via USB-C and trust this Mac, then re-run the original tool.");
320
+ }
321
+ async function pollUntilDevicePresent(opts, deps, start) {
322
+ const pollIntervalMs = opts.fallbackPollIntervalMs ?? 10_000;
323
+ const signal = opts.signal;
324
+ const sleep = deps.sleep ?? defaultSleep;
325
+ const now = deps.now ?? (() => Date.now());
326
+ while (true) {
327
+ if (signal?.aborted)
328
+ throw new Error("aborted");
329
+ if (typeof opts.timeoutMs === "number" && opts.timeoutMs > 0) {
330
+ const elapsed = now() - start;
331
+ const remaining = opts.timeoutMs - elapsed;
332
+ if (remaining <= 0) {
333
+ throw new Error(formatTimeoutMessage(opts.timeoutMs));
334
+ }
335
+ const waitMs = Math.min(pollIntervalMs, remaining);
336
+ try {
337
+ await sleep(waitMs, signal);
338
+ }
339
+ catch (err) {
340
+ if (err instanceof Error && err.message === "aborted")
341
+ throw err;
342
+ throw new Error(`wait-for-device sleep failed: ${err instanceof Error ? err.message : String(err)}`);
343
+ }
344
+ }
345
+ else {
346
+ try {
347
+ await sleep(pollIntervalMs, signal);
348
+ }
349
+ catch (err) {
350
+ if (err instanceof Error && err.message === "aborted")
351
+ throw err;
352
+ throw new Error(`wait-for-device sleep failed: ${err instanceof Error ? err.message : String(err)}`);
353
+ }
354
+ }
355
+ if (signal?.aborted)
356
+ throw new Error("aborted");
357
+ const found = await probeOnce(deps);
358
+ if (found) {
359
+ return {
360
+ deviceFound: true,
361
+ udid: found.udid,
362
+ name: found.name,
363
+ waitedMs: now() - start,
364
+ source: "polling-fallback",
365
+ };
366
+ }
367
+ }
368
+ }
@@ -0,0 +1,38 @@
1
+ import type { TargetResolver } from "./target.js";
2
+ import { type LaunchFailureReason, type WdaSimulatorSession } from "./wda-simulator.js";
3
+ export interface StartOpts {
4
+ udid?: string;
5
+ derivedDataPath?: string;
6
+ startupTimeoutMs?: number;
7
+ }
8
+ export type StartFailureReason = "no-booted-simulator" | "simulator-not-found" | LaunchFailureReason;
9
+ export type StartResult = {
10
+ ok: true;
11
+ port: number;
12
+ udid: string;
13
+ reused: boolean;
14
+ projectPath: string;
15
+ } | {
16
+ ok: false;
17
+ reason: StartFailureReason;
18
+ error: string;
19
+ output?: string[];
20
+ };
21
+ export declare class WdaManager {
22
+ private readonly target;
23
+ private readonly cacheDir;
24
+ private readonly projectDir;
25
+ private session;
26
+ private inflight;
27
+ private inflightChild;
28
+ private stopGeneration;
29
+ private stopping;
30
+ constructor(target: TargetResolver, cacheDir: string, projectDir: string);
31
+ getSession(): WdaSimulatorSession | null;
32
+ start(opts?: StartOpts): Promise<StartResult>;
33
+ stop(): Promise<{
34
+ stopped: boolean;
35
+ udid?: string;
36
+ port?: number;
37
+ }>;
38
+ }
@@ -0,0 +1,186 @@
1
+ import { join } from "node:path";
2
+ import { disableCaffeinate, enableCaffeinate } from "./mac-caffeinate.js";
3
+ import { listBootedSimulators } from "./target.js";
4
+ import { launchSimulatorWda, } from "./wda-simulator.js";
5
+ export class WdaManager {
6
+ target;
7
+ cacheDir;
8
+ projectDir;
9
+ session = null;
10
+ inflight = null;
11
+ inflightChild = null;
12
+ stopGeneration = 0;
13
+ stopping = null;
14
+ constructor(target, cacheDir, projectDir) {
15
+ this.target = target;
16
+ this.cacheDir = cacheDir;
17
+ this.projectDir = projectDir;
18
+ }
19
+ getSession() {
20
+ return this.session;
21
+ }
22
+ async start(opts = {}) {
23
+ if (this.stopping) {
24
+ await this.stopping.catch(() => undefined);
25
+ }
26
+ if (this.session) {
27
+ const childAlive = this.session.process.exitCode === null;
28
+ const udidMatches = !opts.udid || opts.udid === this.session.udid;
29
+ if (childAlive && udidMatches) {
30
+ return {
31
+ ok: true,
32
+ port: this.session.port,
33
+ udid: this.session.udid,
34
+ reused: true,
35
+ projectPath: this.session.projectPath,
36
+ };
37
+ }
38
+ if (childAlive && !udidMatches) {
39
+ await this.stop().catch(() => undefined);
40
+ }
41
+ else {
42
+ this.session = null;
43
+ this.target.clearSimulatorWdaPort();
44
+ }
45
+ }
46
+ if (this.inflight)
47
+ return this.inflight;
48
+ const stopGenerationAtStart = this.stopGeneration;
49
+ const work = (async () => {
50
+ const sims = await listBootedSimulators();
51
+ if (sims.length === 0) {
52
+ return {
53
+ ok: false,
54
+ reason: "no-booted-simulator",
55
+ error: "No booted iOS simulator found. Boot one first: " +
56
+ "`xcrun simctl boot <udid>` or open it via " +
57
+ "`open -a Simulator` and pick a device. Then retry " +
58
+ "`rn_wda_start`.",
59
+ };
60
+ }
61
+ let picked;
62
+ if (opts.udid) {
63
+ const found = sims.find((s) => s.udid === opts.udid);
64
+ if (!found) {
65
+ return {
66
+ ok: false,
67
+ reason: "simulator-not-found",
68
+ error: `Simulator UDID ${opts.udid} is not booted. Booted ` +
69
+ `simulators: ${sims.map((s) => `${s.name} (${s.udid})`).join(", ")}.`,
70
+ };
71
+ }
72
+ picked = found;
73
+ }
74
+ else {
75
+ picked = sims[0];
76
+ }
77
+ const launchOpts = {
78
+ udid: picked.udid,
79
+ projectDir: this.projectDir,
80
+ derivedDataPath: opts.derivedDataPath ?? join(this.cacheDir, "wda-derived"),
81
+ startupTimeoutMs: opts.startupTimeoutMs,
82
+ onChildSpawned: (child) => {
83
+ this.inflightChild = child;
84
+ },
85
+ };
86
+ let result;
87
+ try {
88
+ result = await launchSimulatorWda(launchOpts);
89
+ }
90
+ finally {
91
+ this.inflightChild = null;
92
+ }
93
+ if (!result.ok) {
94
+ return {
95
+ ok: false,
96
+ reason: result.reason,
97
+ error: result.error,
98
+ output: result.output,
99
+ };
100
+ }
101
+ if (this.stopGeneration !== stopGenerationAtStart) {
102
+ await result.session.stop().catch(() => undefined);
103
+ return {
104
+ ok: false,
105
+ reason: "xcodebuild-exited-early",
106
+ error: "`rn_wda_stop` (or MCP shutdown) ran while the simulator " +
107
+ "WDA build was in flight. The build was terminated. Re-run " +
108
+ "`rn_wda_start` if you still want a simulator session.",
109
+ };
110
+ }
111
+ this.session = result.session;
112
+ this.target.setSimulatorWdaPort(result.session.port, result.session.udid);
113
+ const session = result.session;
114
+ session.process.once("exit", () => {
115
+ if (this.session === session) {
116
+ this.session = null;
117
+ this.target.clearSimulatorWdaPort();
118
+ }
119
+ });
120
+ try {
121
+ enableCaffeinate();
122
+ }
123
+ catch {
124
+ }
125
+ return {
126
+ ok: true,
127
+ port: result.session.port,
128
+ udid: result.session.udid,
129
+ reused: false,
130
+ projectPath: result.session.projectPath,
131
+ };
132
+ })();
133
+ this.inflight = work;
134
+ try {
135
+ return await work;
136
+ }
137
+ finally {
138
+ this.inflight = null;
139
+ }
140
+ }
141
+ async stop() {
142
+ if (this.stopping) {
143
+ await this.stopping.catch(() => undefined);
144
+ return { stopped: false };
145
+ }
146
+ this.stopGeneration++;
147
+ let resolveStopping;
148
+ this.stopping = new Promise((r) => {
149
+ resolveStopping = r;
150
+ });
151
+ try {
152
+ const inflightChild = this.inflightChild;
153
+ this.inflightChild = null;
154
+ if (inflightChild && inflightChild.exitCode === null) {
155
+ try {
156
+ inflightChild.kill("SIGTERM");
157
+ }
158
+ catch {
159
+ }
160
+ }
161
+ if (this.inflight) {
162
+ await this.inflight.catch(() => undefined);
163
+ }
164
+ if (!this.session)
165
+ return { stopped: false };
166
+ const { udid, port } = this.session;
167
+ try {
168
+ await this.session.stop();
169
+ }
170
+ finally {
171
+ this.session = null;
172
+ this.target.clearSimulatorWdaPort();
173
+ }
174
+ return { stopped: true, udid, port };
175
+ }
176
+ finally {
177
+ try {
178
+ disableCaffeinate();
179
+ }
180
+ catch {
181
+ }
182
+ resolveStopping();
183
+ this.stopping = null;
184
+ }
185
+ }
186
+ }