@ceraph/react-native-mcp 0.2.1 → 0.3.1

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 +38 -5
package/dist/init.js CHANGED
@@ -1,46 +1,14 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * @ceraph/react-native-mcp init
4
- *
5
- * Sets up MCP configuration and the runtime error hook for the current project.
6
- * Detects which MCP clients are in use and writes config for each one.
7
- */
8
2
  import { readFile, writeFile, mkdir, access, chmod } from "node:fs/promises";
9
3
  import { join } from "node:path";
4
+ import { runWalkthrough } from "./init/walkthrough.js";
5
+ import { AppLifecycle } from "./app-lifecycle.js";
6
+ import { ScreenManager } from "./screen.js";
7
+ import { DeviceAutonomy } from "./device-autonomy.js";
8
+ import { runPreflight } from "./preflight.js";
9
+ import { HOOK_SCRIPT, FLOW_PROGRESS_HOOK_SCRIPT, ERROR_HOOK_COMMAND, ERROR_HOOK_MATCHER, FLOW_PROGRESS_HOOK_COMMAND, FLOW_PROGRESS_HOOK_MATCHER, } from "./init/claude-hook-constants.js";
10
+ import { writeFileAtomic } from "./utils/atomic-write.js";
10
11
  const PROJECT_DIR = process.cwd();
11
- // ---------------------------------------------------------------------------
12
- // Hook script content
13
- // ---------------------------------------------------------------------------
14
- const HOOK_SCRIPT = `#!/bin/bash
15
- # rn-error-notify.sh — Injected by @ceraph/react-native-mcp init
16
- # Reads .rn-errors.json and injects runtime errors into Claude's context.
17
-
18
- ERROR_FILE="\$CLAUDE_PROJECT_DIR/mobile/.rn-errors.json"
19
-
20
- # Also check project root if mobile/ doesn't exist
21
- if [ ! -f "\$ERROR_FILE" ]; then
22
- ERROR_FILE="\$CLAUDE_PROJECT_DIR/.rn-errors.json"
23
- fi
24
-
25
- if [ ! -f "\$ERROR_FILE" ]; then
26
- exit 0
27
- fi
28
-
29
- ERROR_COUNT=\$(jq -r '.errors | length' "\$ERROR_FILE" 2>/dev/null)
30
-
31
- if [ "\$ERROR_COUNT" = "0" ] || [ -z "\$ERROR_COUNT" ]; then
32
- exit 0
33
- fi
34
-
35
- echo "REACT NATIVE RUNTIME ERROR DETECTED:"
36
- echo ""
37
- jq -r '.errors[] | "Error: \\(.message)\\nStack: \\(.stack)\\nTime: \\(.timestamp)\\n---"' "\$ERROR_FILE" 2>/dev/null
38
- echo ""
39
- echo "Use rn_get_errors for full details. Fix the error and rebuild."
40
- `;
41
- // ---------------------------------------------------------------------------
42
- // MCP server entry (same for all JSON-based clients)
43
- // ---------------------------------------------------------------------------
44
12
  const MCP_ENTRY = {
45
13
  "mobile-mcp": {
46
14
  command: "npx",
@@ -51,9 +19,6 @@ const MCP_ENTRY = {
51
19
  args: ["-y", "@ceraph/react-native-mcp@latest"],
52
20
  },
53
21
  };
54
- // ---------------------------------------------------------------------------
55
- // Helpers
56
- // ---------------------------------------------------------------------------
57
22
  async function fileExists(path) {
58
23
  try {
59
24
  await access(path);
@@ -64,27 +29,55 @@ async function fileExists(path) {
64
29
  }
65
30
  }
66
31
  async function readJson(path) {
32
+ let raw;
67
33
  try {
68
- const content = await readFile(path, "utf-8");
69
- return JSON.parse(content);
34
+ raw = await readFile(path, "utf-8");
70
35
  }
71
- catch {
72
- return null;
36
+ catch (err) {
37
+ const e = err;
38
+ if (e && e.code === "ENOENT")
39
+ return { kind: "missing" };
40
+ throw err;
73
41
  }
42
+ try {
43
+ return { kind: "ok", value: JSON.parse(raw) };
44
+ }
45
+ catch (err) {
46
+ return { kind: "malformed", raw, error: err };
47
+ }
48
+ }
49
+ function isPlainObject(v) {
50
+ return v != null && typeof v === "object" && !Array.isArray(v);
74
51
  }
75
52
  async function writeJson(path, data) {
76
53
  await mkdir(join(path, ".."), { recursive: true });
77
- await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
54
+ await writeFileAtomic(path, JSON.stringify(data, null, 2) + "\n");
78
55
  }
79
56
  function log(msg) {
80
57
  console.log(` ${msg}`);
81
58
  }
82
- // ---------------------------------------------------------------------------
83
- // MCP config writers
84
- // ---------------------------------------------------------------------------
85
- async function setupJsonMcp(configPath, label) {
86
- const existing = (await readJson(configPath)) ?? {};
87
- const servers = (existing.mcpServers ?? {});
59
+ export async function setupJsonMcp(configPath, label) {
60
+ const readResult = await readJson(configPath);
61
+ if (readResult.kind === "malformed") {
62
+ log(`${label}: existing config at ${configPath} is malformed JSON — preserving it. Fix the file manually then re-run init.`);
63
+ return false;
64
+ }
65
+ const baseExisting = readResult.kind === "ok" ? readResult.value : {};
66
+ if (readResult.kind === "ok" && !isPlainObject(baseExisting)) {
67
+ log(`${label}: existing config at ${configPath} is not a JSON object (got ${Array.isArray(baseExisting) ? "array" : typeof baseExisting}) — preserving it.`);
68
+ return false;
69
+ }
70
+ const existing = isPlainObject(baseExisting)
71
+ ? baseExisting
72
+ : {};
73
+ const rawServers = existing.mcpServers;
74
+ if (rawServers !== undefined && !isPlainObject(rawServers)) {
75
+ log(`${label}: existing mcpServers in ${configPath} is not a JSON object (got ${Array.isArray(rawServers) ? "array" : typeof rawServers}) — preserving the file.`);
76
+ return false;
77
+ }
78
+ const servers = isPlainObject(rawServers)
79
+ ? rawServers
80
+ : {};
88
81
  if (servers["react-native-mcp"]) {
89
82
  log(`${label}: already configured`);
90
83
  return false;
@@ -102,7 +95,6 @@ async function setupCodexToml(configPath) {
102
95
  content = await readFile(configPath, "utf-8");
103
96
  }
104
97
  catch {
105
- // File doesn't exist, we'll create it
106
98
  }
107
99
  if (content.includes("react-native-mcp")) {
108
100
  log("Codex: already configured");
@@ -118,62 +110,192 @@ command = "npx"
118
110
  args = ["-y", "@ceraph/react-native-mcp@latest"]
119
111
  `;
120
112
  await mkdir(join(configPath, ".."), { recursive: true });
121
- await writeFile(configPath, content + tomlBlock, "utf-8");
113
+ await writeFileAtomic(configPath, content + tomlBlock);
122
114
  log("Codex: configured ✓");
123
115
  return true;
124
116
  }
125
- // ---------------------------------------------------------------------------
126
- // Hook setup
127
- // ---------------------------------------------------------------------------
128
- async function setupClaudeHook() {
129
- // Write the hook script
130
- const hooksDir = join(PROJECT_DIR, ".claude", "hooks");
117
+ export async function setupClaudeHook(projectDir = PROJECT_DIR) {
118
+ const hooksDir = join(projectDir, ".claude", "hooks");
131
119
  const hookPath = join(hooksDir, "rn-error-notify.sh");
120
+ const progressHookPath = join(hooksDir, "rn-flow-progress-notify.sh");
132
121
  await mkdir(hooksDir, { recursive: true });
133
- await writeFile(hookPath, HOOK_SCRIPT, "utf-8");
122
+ await writeFileAtomic(hookPath, HOOK_SCRIPT);
134
123
  await chmod(hookPath, 0o755);
135
124
  log("Hook script: .claude/hooks/rn-error-notify.sh ✓");
136
- // Add FileChanged hook to settings.
137
- // Check both settings.json and settings.local.json — if either already has
138
- // the hook, we treat it as configured. This prevents duplicate hooks (which
139
- // would fire twice per .rn-errors.json write) for projects where the hook
140
- // was originally written to settings.local.json.
141
- const sharedSettingsPath = join(PROJECT_DIR, ".claude", "settings.json");
142
- const localSettingsPath = join(PROJECT_DIR, ".claude", "settings.local.json");
143
- const isMatcher = (h) => h.matcher === ".rn-errors.json";
144
- const fileChangedFrom = (s) => {
145
- const hooks = (s?.hooks ?? {});
146
- return (hooks.FileChanged ?? []);
125
+ await writeFileAtomic(progressHookPath, FLOW_PROGRESS_HOOK_SCRIPT);
126
+ await chmod(progressHookPath, 0o755);
127
+ log("Hook script: .claude/hooks/rn-flow-progress-notify.sh ✓");
128
+ const sharedSettingsPath = join(projectDir, ".claude", "settings.json");
129
+ const localSettingsPath = join(projectDir, ".claude", "settings.local.json");
130
+ const hasCeraphEntry = (s, matcher, command) => {
131
+ const settingsObj = isPlainObject(s) ? s : {};
132
+ const hooksVal = settingsObj.hooks;
133
+ const hooks = isPlainObject(hooksVal)
134
+ ? hooksVal
135
+ : {};
136
+ const fc = hooks.FileChanged;
137
+ if (!Array.isArray(fc)) {
138
+ return fc !== undefined;
139
+ }
140
+ return fc.some((entry) => {
141
+ if (!isPlainObject(entry))
142
+ return false;
143
+ if (entry.matcher !== matcher)
144
+ return false;
145
+ const inner = entry.hooks;
146
+ if (!Array.isArray(inner))
147
+ return false;
148
+ return inner.some((h) => {
149
+ if (!isPlainObject(h))
150
+ return false;
151
+ const cmd = h.command;
152
+ return typeof cmd === "string" && cmd === command;
153
+ });
154
+ });
147
155
  };
148
- const sharedSettings = await readJson(sharedSettingsPath);
149
- const localSettings = await readJson(localSettingsPath);
150
- if (fileChangedFrom(sharedSettings).some(isMatcher) ||
151
- fileChangedFrom(localSettings).some(isMatcher)) {
152
- log("Claude Code hook: already configured");
156
+ const sharedRead = await readJson(sharedSettingsPath);
157
+ const localRead = await readJson(localSettingsPath);
158
+ if (sharedRead.kind === "malformed") {
159
+ log(`Claude Code hook: ${sharedSettingsPath} is malformed JSON — preserving it. Fix the file manually then re-run init.`);
160
+ return;
161
+ }
162
+ if (localRead.kind === "malformed") {
163
+ log(`Claude Code hook: ${localSettingsPath} is malformed JSON — preserving it. Fix the file manually then re-run init.`);
164
+ return;
165
+ }
166
+ const sharedSettings = sharedRead.kind === "ok" ? sharedRead.value : null;
167
+ const localSettings = localRead.kind === "ok" ? localRead.value : null;
168
+ if (sharedSettings !== null && !isPlainObject(sharedSettings)) {
169
+ log(`Claude Code hook: ${sharedSettingsPath} top-level is not a JSON object — preserving it.`);
170
+ return;
171
+ }
172
+ if (localSettings !== null && !isPlainObject(localSettings)) {
173
+ log(`Claude Code hook: ${localSettingsPath} top-level is not a JSON object — preserving it.`);
153
174
  return;
154
175
  }
155
- // Write to settings.json (the shared default). settings.local.json is
156
- // reserved for per-machine overrides; we don't write there.
157
176
  const settings = sharedSettings ?? {};
158
- const hooks = (settings.hooks ?? {});
159
- const fileChangedHooks = (hooks.FileChanged ?? []);
160
- fileChangedHooks.push({
161
- matcher: ".rn-errors.json",
162
- hooks: [
163
- {
164
- type: "command",
165
- command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/rn-error-notify.sh',
166
- },
167
- ],
168
- });
169
- hooks.FileChanged = fileChangedHooks;
170
- settings.hooks = hooks;
171
- await writeJson(sharedSettingsPath, settings);
172
- log("Claude Code hook: FileChanged → .rn-errors.json ✓");
177
+ const hooksVal = settings.hooks;
178
+ if (hooksVal !== undefined && !isPlainObject(hooksVal)) {
179
+ log(`Claude Code hook: ${sharedSettingsPath} \`hooks\` is not a JSON object — preserving it.`);
180
+ return;
181
+ }
182
+ const hooks = isPlainObject(hooksVal)
183
+ ? hooksVal
184
+ : {};
185
+ const fcVal = hooks.FileChanged;
186
+ const fileChangedHooks = Array.isArray(fcVal)
187
+ ? fcVal
188
+ : [];
189
+ let mutated = false;
190
+ if (!hasCeraphEntry(sharedSettings, ERROR_HOOK_MATCHER, ERROR_HOOK_COMMAND) &&
191
+ !hasCeraphEntry(localSettings, ERROR_HOOK_MATCHER, ERROR_HOOK_COMMAND)) {
192
+ fileChangedHooks.push({
193
+ matcher: ERROR_HOOK_MATCHER,
194
+ hooks: [
195
+ {
196
+ type: "command",
197
+ command: ERROR_HOOK_COMMAND,
198
+ },
199
+ ],
200
+ });
201
+ mutated = true;
202
+ log("Claude Code hook: FileChanged → .rn-errors.json ✓");
203
+ }
204
+ else {
205
+ log("Claude Code hook (.rn-errors.json): already configured");
206
+ }
207
+ if (!hasCeraphEntry(sharedSettings, FLOW_PROGRESS_HOOK_MATCHER, FLOW_PROGRESS_HOOK_COMMAND) &&
208
+ !hasCeraphEntry(localSettings, FLOW_PROGRESS_HOOK_MATCHER, FLOW_PROGRESS_HOOK_COMMAND)) {
209
+ fileChangedHooks.push({
210
+ matcher: FLOW_PROGRESS_HOOK_MATCHER,
211
+ hooks: [
212
+ {
213
+ type: "command",
214
+ command: FLOW_PROGRESS_HOOK_COMMAND,
215
+ },
216
+ ],
217
+ });
218
+ mutated = true;
219
+ log("Claude Code hook: FileChanged → .rn-flow-progress.json ✓");
220
+ }
221
+ else {
222
+ log("Claude Code hook (.rn-flow-progress.json): already configured");
223
+ }
224
+ if (mutated) {
225
+ hooks.FileChanged = fileChangedHooks;
226
+ settings.hooks = hooks;
227
+ await writeJson(sharedSettingsPath, settings);
228
+ }
229
+ }
230
+ const CAMERA_IMAGES_README = `# Ceraph camera test images
231
+
232
+ Per-screen test images for \`<CeraphCamera />\` from
233
+ \`@ceraph/react-native-mcp/shim\`. The filename (without extension) is
234
+ the imageKey you pass on the JSX tag.
235
+
236
+ ## Three ways to wire a camera
237
+
238
+ \`<CeraphCamera />\` (no \`imageKey\`) — uninitialized. Falls back to a
239
+ bundled 1024x1024 black PNG that ships with the package, so the screen
240
+ renders. The MCP's \`rn_preflight\` lists these as info findings so an
241
+ AI assistant can prompt you to make a deliberate choice. Pick from:
242
+
243
+ 1. \`<CeraphCamera imageKey="default" />\` — explicit acknowledgment of
244
+ the black PNG. Right answer for most form / upload / permission
245
+ flows where the image content doesn't matter.
246
+ 2. \`<CeraphCamera imageKey="profile" />\` — a specific test image you
247
+ provide (this folder). Drop \`profile.jpg\` (or \`.png\` / \`.webp\` /
248
+ \`.heic\`) here. The agent can write the file for you via
249
+ \`ceraph_add_camera_image\`.
250
+ 3. \`<CeraphCamera imageKey="@runtime" />\` — runtime control. The flow
251
+ planner picks the image per-step at test time. Use when one camera
252
+ screen feeds different scenarios across multiple flows.
253
+
254
+ ## Naming
255
+
256
+ Lowercase + hyphen-separated, descriptive of what the image represents:
257
+
258
+ - \`profile.jpg\` — face photo for a profile picture screen
259
+ - \`id-card.png\` — government-issued ID for a KYC scan screen
260
+ - \`product-photo.jpg\` — a product image for an upload screen
261
+
262
+ Supported extensions: \`.jpg\`, \`.jpeg\`, \`.png\`, \`.webp\`, \`.heic\`.
263
+ Extension priority on collision: jpg > jpeg > png > webp > heic.
264
+
265
+ ## Workflow
266
+
267
+ 1. Drop the image into this folder with the right filename.
268
+ 2. Reference it on the tag:
269
+ \`\`\`tsx
270
+ import { CeraphCamera } from "@ceraph/react-native-mcp/shim";
271
+ <CeraphCamera imageKey="profile" />
272
+ \`\`\`
273
+ 3. Regenerate the registry: call the MCP tool
274
+ \`rn_sync_camera_registry\`, or just call \`rn_preflight\` — it
275
+ syncs the registry automatically.
276
+ 4. In \`App.tsx\` or your root layout (once):
277
+ \`\`\`tsx
278
+ import { installCeraph } from "@ceraph/react-native-mcp/shim";
279
+ useEffect(() => { installCeraph(); }, []);
280
+ \`\`\`
281
+ \`installCeraph()\` loads the generated registry from
282
+ \`./.ceraph/camera-images/_registry\` and wires the deep-link
283
+ handlers the flow planner uses.
284
+
285
+ Commit these images so flows are reproducible across machines and CI.
286
+ `;
287
+ async function setupCameraImagesDir() {
288
+ const dir = join(PROJECT_DIR, ".ceraph", "camera-images");
289
+ await mkdir(dir, { recursive: true });
290
+ const readmePath = join(dir, "README.md");
291
+ if (!(await fileExists(readmePath))) {
292
+ await writeFile(readmePath, CAMERA_IMAGES_README, "utf-8");
293
+ log("Camera images: .ceraph/camera-images/ + README.md ✓");
294
+ }
295
+ else {
296
+ log("Camera images: .ceraph/camera-images/ already exists");
297
+ }
173
298
  }
174
- // ---------------------------------------------------------------------------
175
- // Gitignore
176
- // ---------------------------------------------------------------------------
177
299
  async function setupGitignore() {
178
300
  const gitignorePath = join(PROJECT_DIR, ".gitignore");
179
301
  let content = "";
@@ -181,55 +303,93 @@ async function setupGitignore() {
181
303
  content = await readFile(gitignorePath, "utf-8");
182
304
  }
183
305
  catch {
184
- // No .gitignore yet
185
306
  }
186
- const entries = [".rn-errors.json", ".rn-mcp-cache/"];
307
+ const entries = [".rn-errors.json", ".rn-flow-progress.json", ".rn-mcp-cache/"];
187
308
  const missing = entries.filter((e) => !content.includes(e));
188
309
  if (missing.length === 0) {
189
310
  return;
190
311
  }
191
312
  const prefix = content === "" || content.endsWith("\n") ? "" : "\n";
192
313
  const addition = prefix + missing.join("\n") + "\n";
193
- await writeFile(gitignorePath, content + addition, "utf-8");
314
+ await writeFileAtomic(gitignorePath, content + addition);
194
315
  log(`.gitignore: added ${missing.join(", ")} ✓`);
195
316
  }
196
- // ---------------------------------------------------------------------------
197
- // Main
198
- // ---------------------------------------------------------------------------
199
- async function main() {
200
- console.log("\n@ceraph/react-native-mcp init\n");
201
- // Detect and configure MCP clients
317
+ export async function setupMcpClients() {
202
318
  console.log("MCP configuration:");
203
- // Claude Code
204
319
  await setupJsonMcp(join(PROJECT_DIR, ".mcp.json"), "Claude Code");
205
- // Cursor
206
320
  await setupJsonMcp(join(PROJECT_DIR, ".cursor", "mcp.json"), "Cursor");
207
- // Codex
208
321
  await setupCodexToml(join(PROJECT_DIR, ".codex", "config.toml"));
209
- // VS Code / Copilot
210
322
  await setupJsonMcp(join(PROJECT_DIR, ".vscode", "mcp.json"), "VS Code");
211
- // Windsurf (user-level config)
212
323
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
213
324
  if (home) {
214
325
  const windsurfPath = join(home, ".codeium", "windsurf", "mcp_config.json");
215
326
  if (await fileExists(join(home, ".codeium", "windsurf"))) {
216
327
  await setupJsonMcp(windsurfPath, "Windsurf");
217
328
  }
218
- // Antigravity (user-level config)
219
329
  const antigravityPath = join(home, ".gemini", "antigravity", "mcp_config.json");
220
330
  if (await fileExists(join(home, ".gemini", "antigravity"))) {
221
331
  await setupJsonMcp(antigravityPath, "Antigravity");
222
332
  }
223
333
  }
224
- // Claude Code error hook
334
+ }
335
+ function summarisePreflight(checks) {
336
+ const blockers = checks.filter((c) => c.severity === "error" && !c.ok);
337
+ const warnings = checks.filter((c) => c.severity === "warning" && !c.ok);
338
+ if (blockers.length === 0 && warnings.length === 0)
339
+ return "";
340
+ const lines = [];
341
+ for (const c of blockers) {
342
+ lines.push(` [BLOCKER] ${c.name}: ${c.message}`);
343
+ if (c.remediation)
344
+ lines.push(` → ${c.remediation}`);
345
+ }
346
+ for (const c of warnings) {
347
+ lines.push(` [WARN] ${c.name}: ${c.message}`);
348
+ if (c.remediation)
349
+ lines.push(` → ${c.remediation}`);
350
+ }
351
+ return lines.join("\n") + "\n";
352
+ }
353
+ export async function runInit(opts = {}) {
354
+ console.log("\n@ceraph/react-native-mcp init\n");
355
+ await setupMcpClients();
225
356
  console.log("\nRuntime error hook:");
226
357
  await setupClaudeHook();
227
- // Gitignore
358
+ console.log("\nCamera test images:");
359
+ await setupCameraImagesDir();
228
360
  console.log("\nGitignore:");
229
361
  await setupGitignore();
230
- console.log("\nDone. Runtime errors will be automatically injected into Claude's context.\n");
362
+ console.log("");
363
+ await runWalkthrough(PROJECT_DIR, {
364
+ agentMode: opts.agentMode === true,
365
+ nonInteractive: opts.agentMode === true ? true : undefined,
366
+ runPreflight: async () => {
367
+ const screen = new ScreenManager();
368
+ const apps = new AppLifecycle(screen);
369
+ const autonomy = new DeviceAutonomy(screen);
370
+ const res = await runPreflight({
371
+ screen,
372
+ apps,
373
+ autonomy,
374
+ projectDir: PROJECT_DIR,
375
+ excludeRuntimeChecks: true,
376
+ });
377
+ const passedCount = res.checks.filter((c) => c.ok).length;
378
+ return {
379
+ ok: res.ok,
380
+ summary: summarisePreflight(res.checks),
381
+ passedCount,
382
+ totalCount: res.checks.length,
383
+ };
384
+ },
385
+ });
386
+ }
387
+ const argv1 = process.argv[1] ?? "";
388
+ if (argv1.endsWith("init.js") || argv1.endsWith("init.ts")) {
389
+ const agentMode = process.env.CERAPH_AGENT_MODE === "1" ||
390
+ process.argv.includes("--agent");
391
+ runInit({ agentMode }).catch((err) => {
392
+ console.error("Init failed:", err);
393
+ process.exit(1);
394
+ });
231
395
  }
232
- main().catch((err) => {
233
- console.error("Init failed:", err);
234
- process.exit(1);
235
- });
@@ -0,0 +1,32 @@
1
+ import { spawn } from "node:child_process";
2
+ export interface IproxyManagerOptions {
3
+ localPort?: number;
4
+ devicePort?: number;
5
+ spawnFn?: typeof spawn;
6
+ whichFn?: (bin: string) => Promise<string | null>;
7
+ }
8
+ export interface IproxyStartResult {
9
+ ok: boolean;
10
+ reason?: "not-installed" | "spawn-failed" | "already-running";
11
+ pid?: number;
12
+ stderr?: string[];
13
+ }
14
+ export declare class IproxyManager {
15
+ private child;
16
+ private udid;
17
+ private localPort;
18
+ private devicePort;
19
+ private stderrBuffer;
20
+ private spawnFn;
21
+ private whichFn;
22
+ private startInFlight;
23
+ constructor(opts?: IproxyManagerOptions);
24
+ isRunning(): boolean;
25
+ start(udid: string): Promise<IproxyStartResult>;
26
+ private startInner;
27
+ stop(): Promise<void>;
28
+ restart(udid?: string): Promise<IproxyStartResult>;
29
+ getStderr(): string[];
30
+ private appendStderr;
31
+ private detectEarlyExit;
32
+ }