@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.
- package/LICENSE +116 -15
- package/README.md +79 -77
- package/assets/default.png +0 -0
- package/dist/app-lifecycle.d.ts +50 -0
- package/dist/app-lifecycle.js +487 -0
- package/dist/camera-image-writer.d.ts +43 -0
- package/dist/camera-image-writer.js +280 -0
- package/dist/camera-registry-sync.d.ts +18 -0
- package/dist/camera-registry-sync.js +117 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +41 -9
- package/dist/device-autonomy.d.ts +30 -0
- package/dist/device-autonomy.js +117 -0
- package/dist/error-parser.d.ts +6 -26
- package/dist/error-parser.js +4 -74
- package/dist/expo-manager.d.ts +2 -74
- package/dist/expo-manager.js +11 -125
- package/dist/index.d.ts +0 -7
- package/dist/index.js +1266 -56
- package/dist/init/ast-camera.d.ts +29 -0
- package/dist/init/ast-camera.js +267 -0
- package/dist/init/ast-layout.d.ts +15 -0
- package/dist/init/ast-layout.js +167 -0
- package/dist/init/claude-hook-constants.d.ts +9 -0
- package/dist/init/claude-hook-constants.js +91 -0
- package/dist/init/lan-ip.d.ts +11 -0
- package/dist/init/lan-ip.js +51 -0
- package/dist/init/monorepo.d.ts +13 -0
- package/dist/init/monorepo.js +185 -0
- package/dist/init/oauth.d.ts +52 -0
- package/dist/init/oauth.js +220 -0
- package/dist/init/package-manager.d.ts +11 -0
- package/dist/init/package-manager.js +60 -0
- package/dist/init/prompt.d.ts +12 -0
- package/dist/init/prompt.js +68 -0
- package/dist/init/shell-profile.d.ts +22 -0
- package/dist/init/shell-profile.js +85 -0
- package/dist/init/steps.d.ts +135 -0
- package/dist/init/steps.js +399 -0
- package/dist/init/url-scheme.d.ts +42 -0
- package/dist/init/url-scheme.js +187 -0
- package/dist/init/walkthrough.d.ts +76 -0
- package/dist/init/walkthrough.js +340 -0
- package/dist/init.d.ts +7 -7
- package/dist/init.js +280 -120
- package/dist/iproxy-manager.d.ts +32 -0
- package/dist/iproxy-manager.js +216 -0
- package/dist/mac-caffeinate.d.ts +10 -0
- package/dist/mac-caffeinate.js +56 -0
- package/dist/permission-interceptor.d.ts +29 -0
- package/dist/permission-interceptor.js +185 -0
- package/dist/prebuild-detector.d.ts +0 -30
- package/dist/prebuild-detector.js +1 -42
- package/dist/preflight.d.ts +34 -0
- package/dist/preflight.js +847 -0
- package/dist/screen.d.ts +132 -43
- package/dist/screen.js +668 -94
- package/dist/shim/boot.d.ts +41 -0
- package/dist/shim/boot.js +141 -0
- package/dist/shim/camera.d.ts +22 -0
- package/dist/shim/camera.js +62 -0
- package/dist/shim/config.d.ts +6 -0
- package/dist/shim/config.js +56 -0
- package/dist/shim/deep-link.d.ts +1 -0
- package/dist/shim/deep-link.js +25 -0
- package/dist/shim/dev-guard.d.ts +1 -0
- package/dist/shim/dev-guard.js +3 -0
- package/dist/shim/error-handler.d.ts +20 -0
- package/dist/shim/error-handler.js +66 -0
- package/dist/shim/fetch-interceptor.d.ts +13 -0
- package/dist/shim/fetch-interceptor.js +93 -0
- package/dist/shim/index.d.ts +6 -0
- package/dist/shim/index.js +6 -0
- package/dist/shim/keep-awake.d.ts +13 -0
- package/dist/shim/keep-awake.js +118 -0
- package/dist/shim/reload.d.ts +23 -0
- package/dist/shim/reload.js +76 -0
- package/dist/shim/signal-capture.d.ts +11 -0
- package/dist/shim/signal-capture.js +15 -0
- package/dist/shim/signal-transport.d.ts +17 -0
- package/dist/shim/signal-transport.js +43 -0
- package/dist/signal-listener.d.ts +27 -0
- package/dist/signal-listener.js +135 -0
- package/dist/simulator-boot.d.ts +52 -0
- package/dist/simulator-boot.js +227 -0
- package/dist/target.d.ts +48 -0
- package/dist/target.js +267 -0
- package/dist/uninstall/cli-runner.d.ts +32 -0
- package/dist/uninstall/cli-runner.js +223 -0
- package/dist/uninstall/footprint.d.ts +40 -0
- package/dist/uninstall/footprint.js +288 -0
- package/dist/uninstall/mcp-tools.d.ts +14 -0
- package/dist/uninstall/mcp-tools.js +175 -0
- package/dist/uninstall/revert-auth.d.ts +22 -0
- package/dist/uninstall/revert-auth.js +31 -0
- package/dist/uninstall/revert-boot.d.ts +24 -0
- package/dist/uninstall/revert-boot.js +242 -0
- package/dist/uninstall/revert-camera.d.ts +12 -0
- package/dist/uninstall/revert-camera.js +199 -0
- package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
- package/dist/uninstall/revert-ceraph-dir.js +38 -0
- package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
- package/dist/uninstall/revert-claude-hooks.js +191 -0
- package/dist/uninstall/revert-gitignore.d.ts +17 -0
- package/dist/uninstall/revert-gitignore.js +43 -0
- package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
- package/dist/uninstall/revert-mcp-clients.js +194 -0
- package/dist/uninstall/revert-package.d.ts +34 -0
- package/dist/uninstall/revert-package.js +98 -0
- package/dist/uninstall/revert-scheme.d.ts +36 -0
- package/dist/uninstall/revert-scheme.js +139 -0
- package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
- package/dist/uninstall/revert-signal-host-env.js +61 -0
- package/dist/uninstall/walkthrough.d.ts +80 -0
- package/dist/uninstall/walkthrough.js +1244 -0
- package/dist/utils/atomic-write.d.ts +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/wait-for-device.d.ts +68 -0
- package/dist/wait-for-device.js +368 -0
- package/dist/wda-manager.d.ts +38 -0
- package/dist/wda-manager.js +186 -0
- package/dist/wda-simulator.d.ts +28 -0
- package/dist/wda-simulator.js +257 -0
- 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
|
-
|
|
69
|
-
return JSON.parse(content);
|
|
34
|
+
raw = await readFile(path, "utf-8");
|
|
70
35
|
}
|
|
71
|
-
catch {
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
113
|
+
await writeFileAtomic(configPath, content + tomlBlock);
|
|
122
114
|
log("Codex: configured ✓");
|
|
123
115
|
return true;
|
|
124
116
|
}
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
122
|
+
await writeFileAtomic(hookPath, HOOK_SCRIPT);
|
|
134
123
|
await chmod(hookPath, 0o755);
|
|
135
124
|
log("Hook script: .claude/hooks/rn-error-notify.sh ✓");
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
358
|
+
console.log("\nCamera test images:");
|
|
359
|
+
await setupCameraImagesDir();
|
|
228
360
|
console.log("\nGitignore:");
|
|
229
361
|
await setupGitignore();
|
|
230
|
-
console.log("
|
|
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
|
+
}
|