@ceraph/react-native-mcp 0.3.3 → 0.4.5

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 (132) hide show
  1. package/README.md +335 -68
  2. package/dist/babel-plugin/index.cjs +1 -0
  3. package/dist/babel-plugin/index.js +1 -0
  4. package/dist/cli.d.ts +3 -1
  5. package/dist/cli.js +1 -47
  6. package/dist/index.d.ts +106 -1
  7. package/dist/index.js +2 -1651
  8. package/dist/shim/async-storage-ops.d.ts +26 -0
  9. package/dist/shim/async-storage-ops.js +1 -0
  10. package/dist/shim/boot.d.ts +9 -0
  11. package/dist/shim/boot.js +1 -141
  12. package/dist/shim/camera.js +1 -62
  13. package/dist/shim/command-poll.d.ts +18 -0
  14. package/dist/shim/command-poll.js +1 -0
  15. package/dist/shim/config.js +1 -56
  16. package/dist/shim/console-capture.d.ts +16 -0
  17. package/dist/shim/console-capture.js +1 -0
  18. package/dist/shim/deep-link.js +1 -25
  19. package/dist/shim/dev-guard.js +1 -3
  20. package/dist/shim/dev-host.d.ts +1 -0
  21. package/dist/shim/dev-host.js +1 -0
  22. package/dist/shim/error-handler.js +1 -66
  23. package/dist/shim/fetch-interceptor.js +1 -93
  24. package/dist/shim/index.d.ts +3 -0
  25. package/dist/shim/index.js +1 -6
  26. package/dist/shim/keep-awake.js +1 -118
  27. package/dist/shim/network-ownership.d.ts +4 -0
  28. package/dist/shim/network-ownership.js +1 -0
  29. package/dist/shim/optimistic-observer.d.ts +29 -0
  30. package/dist/shim/optimistic-observer.js +1 -0
  31. package/dist/shim/reload.js +1 -76
  32. package/dist/shim/reset.d.ts +30 -0
  33. package/dist/shim/reset.js +1 -0
  34. package/dist/shim/signal-capture.d.ts +8 -0
  35. package/dist/shim/signal-capture.js +1 -15
  36. package/dist/shim/signal-transport.d.ts +14 -1
  37. package/dist/shim/signal-transport.js +1 -43
  38. package/dist/shim/xhr-interceptor.d.ts +39 -0
  39. package/dist/shim/xhr-interceptor.js +1 -0
  40. package/package.json +40 -11
  41. package/dist/app-lifecycle.d.ts +0 -50
  42. package/dist/app-lifecycle.js +0 -487
  43. package/dist/camera-image-writer.d.ts +0 -43
  44. package/dist/camera-image-writer.js +0 -280
  45. package/dist/camera-registry-sync.d.ts +0 -18
  46. package/dist/camera-registry-sync.js +0 -117
  47. package/dist/device-autonomy.d.ts +0 -30
  48. package/dist/device-autonomy.js +0 -117
  49. package/dist/error-parser.d.ts +0 -51
  50. package/dist/error-parser.js +0 -275
  51. package/dist/expo-manager.d.ts +0 -62
  52. package/dist/expo-manager.js +0 -447
  53. package/dist/init/ast-camera.d.ts +0 -29
  54. package/dist/init/ast-camera.js +0 -267
  55. package/dist/init/ast-layout.d.ts +0 -15
  56. package/dist/init/ast-layout.js +0 -167
  57. package/dist/init/claude-hook-constants.d.ts +0 -9
  58. package/dist/init/claude-hook-constants.js +0 -91
  59. package/dist/init/lan-ip.d.ts +0 -11
  60. package/dist/init/lan-ip.js +0 -51
  61. package/dist/init/monorepo.d.ts +0 -13
  62. package/dist/init/monorepo.js +0 -185
  63. package/dist/init/oauth.d.ts +0 -52
  64. package/dist/init/oauth.js +0 -220
  65. package/dist/init/package-manager.d.ts +0 -11
  66. package/dist/init/package-manager.js +0 -60
  67. package/dist/init/prompt.d.ts +0 -12
  68. package/dist/init/prompt.js +0 -68
  69. package/dist/init/shell-profile.d.ts +0 -22
  70. package/dist/init/shell-profile.js +0 -85
  71. package/dist/init/steps.d.ts +0 -135
  72. package/dist/init/steps.js +0 -399
  73. package/dist/init/url-scheme.d.ts +0 -42
  74. package/dist/init/url-scheme.js +0 -187
  75. package/dist/init/walkthrough.d.ts +0 -76
  76. package/dist/init/walkthrough.js +0 -340
  77. package/dist/init.d.ts +0 -8
  78. package/dist/init.js +0 -395
  79. package/dist/iproxy-manager.d.ts +0 -32
  80. package/dist/iproxy-manager.js +0 -216
  81. package/dist/mac-caffeinate.d.ts +0 -10
  82. package/dist/mac-caffeinate.js +0 -56
  83. package/dist/permission-interceptor.d.ts +0 -29
  84. package/dist/permission-interceptor.js +0 -185
  85. package/dist/prebuild-detector.d.ts +0 -19
  86. package/dist/prebuild-detector.js +0 -174
  87. package/dist/preflight.d.ts +0 -34
  88. package/dist/preflight.js +0 -847
  89. package/dist/screen.d.ts +0 -184
  90. package/dist/screen.js +0 -931
  91. package/dist/signal-listener.d.ts +0 -27
  92. package/dist/signal-listener.js +0 -135
  93. package/dist/simulator-boot.d.ts +0 -52
  94. package/dist/simulator-boot.js +0 -227
  95. package/dist/target.d.ts +0 -48
  96. package/dist/target.js +0 -267
  97. package/dist/uninstall/cli-runner.d.ts +0 -32
  98. package/dist/uninstall/cli-runner.js +0 -223
  99. package/dist/uninstall/footprint.d.ts +0 -40
  100. package/dist/uninstall/footprint.js +0 -288
  101. package/dist/uninstall/mcp-tools.d.ts +0 -14
  102. package/dist/uninstall/mcp-tools.js +0 -175
  103. package/dist/uninstall/revert-auth.d.ts +0 -22
  104. package/dist/uninstall/revert-auth.js +0 -31
  105. package/dist/uninstall/revert-boot.d.ts +0 -24
  106. package/dist/uninstall/revert-boot.js +0 -242
  107. package/dist/uninstall/revert-camera.d.ts +0 -12
  108. package/dist/uninstall/revert-camera.js +0 -199
  109. package/dist/uninstall/revert-ceraph-dir.d.ts +0 -27
  110. package/dist/uninstall/revert-ceraph-dir.js +0 -38
  111. package/dist/uninstall/revert-claude-hooks.d.ts +0 -19
  112. package/dist/uninstall/revert-claude-hooks.js +0 -191
  113. package/dist/uninstall/revert-gitignore.d.ts +0 -17
  114. package/dist/uninstall/revert-gitignore.js +0 -43
  115. package/dist/uninstall/revert-mcp-clients.d.ts +0 -57
  116. package/dist/uninstall/revert-mcp-clients.js +0 -194
  117. package/dist/uninstall/revert-package.d.ts +0 -34
  118. package/dist/uninstall/revert-package.js +0 -98
  119. package/dist/uninstall/revert-scheme.d.ts +0 -36
  120. package/dist/uninstall/revert-scheme.js +0 -139
  121. package/dist/uninstall/revert-signal-host-env.d.ts +0 -31
  122. package/dist/uninstall/revert-signal-host-env.js +0 -61
  123. package/dist/uninstall/walkthrough.d.ts +0 -80
  124. package/dist/uninstall/walkthrough.js +0 -1244
  125. package/dist/utils/atomic-write.d.ts +0 -1
  126. package/dist/utils/atomic-write.js +0 -30
  127. package/dist/wait-for-device.d.ts +0 -68
  128. package/dist/wait-for-device.js +0 -368
  129. package/dist/wda-manager.d.ts +0 -38
  130. package/dist/wda-manager.js +0 -186
  131. package/dist/wda-simulator.d.ts +0 -28
  132. package/dist/wda-simulator.js +0 -257
@@ -1,85 +0,0 @@
1
- import { readFile, writeFile, mkdir } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { join, dirname } from "node:path";
4
- const SENTINEL_HEAD = "# Ceraph signal host — auto-set by @ceraph/react-native-mcp init";
5
- const SENTINEL_TAIL = "# End Ceraph signal host";
6
- function isValidIpv4(input) {
7
- if (typeof input !== "string")
8
- return false;
9
- const parts = input.split(".");
10
- if (parts.length !== 4)
11
- return false;
12
- for (const part of parts) {
13
- if (!/^[0-9]+$/.test(part))
14
- return false;
15
- if (part.length > 1 && part.startsWith("0"))
16
- return false;
17
- const n = Number(part);
18
- if (!Number.isInteger(n))
19
- return false;
20
- if (n < 0 || n > 255)
21
- return false;
22
- }
23
- return true;
24
- }
25
- export function pickShellProfile(deps = {}) {
26
- const env = deps.env ?? process.env;
27
- const home = deps.home ?? homedir();
28
- const shellPath = env.SHELL ?? "";
29
- if (shellPath.endsWith("/bash")) {
30
- return { path: join(home, ".bashrc"), shell: "bash" };
31
- }
32
- return { path: join(home, ".zshrc"), shell: "zsh" };
33
- }
34
- export async function writeSignalHostEnv(ip, deps = {}) {
35
- if (!isValidIpv4(ip)) {
36
- throw new Error(`writeSignalHostEnv: refusing non-IPv4 input ${JSON.stringify(ip)} — ` +
37
- `expected a dotted-quad like 192.168.1.42 with each octet 0-255`);
38
- }
39
- const target = deps.target ?? pickShellProfile();
40
- const read = deps.readFile ?? ((p) => readFile(p, "utf-8"));
41
- const write = deps.writeFile ?? ((p, c) => writeFile(p, c, "utf-8"));
42
- const ensureDir = deps.mkdir ?? ((p, o) => mkdir(p, o));
43
- let body = "";
44
- try {
45
- body = await read(target.path);
46
- }
47
- catch {
48
- body = "";
49
- }
50
- const block = `${SENTINEL_HEAD}\n` +
51
- `export CERAPH_SIGNAL_HOST=${ip}\n` +
52
- `${SENTINEL_TAIL}\n`;
53
- const existingBlock = extractSentinelBlock(body);
54
- if (existingBlock) {
55
- if (existingBlock.value === ip) {
56
- return { path: target.path, shell: target.shell, state: "unchanged" };
57
- }
58
- const next = body.slice(0, existingBlock.start) +
59
- block +
60
- body.slice(existingBlock.end);
61
- await ensureDir(dirname(target.path), { recursive: true });
62
- await write(target.path, next);
63
- return { path: target.path, shell: target.shell, state: "updated" };
64
- }
65
- const prefix = body === "" || body.endsWith("\n") ? "" : "\n";
66
- const separator = body === "" ? "" : "\n";
67
- const next = body + prefix + separator + block;
68
- await ensureDir(dirname(target.path), { recursive: true });
69
- await write(target.path, next);
70
- return { path: target.path, shell: target.shell, state: "added" };
71
- }
72
- function extractSentinelBlock(body) {
73
- const headIdx = body.indexOf(SENTINEL_HEAD);
74
- if (headIdx === -1)
75
- return null;
76
- const tailIdx = body.indexOf(SENTINEL_TAIL, headIdx);
77
- if (tailIdx === -1)
78
- return null;
79
- const afterTail = tailIdx + SENTINEL_TAIL.length;
80
- const end = body[afterTail] === "\n" ? afterTail + 1 : afterTail;
81
- const between = body.slice(headIdx, end);
82
- const exportMatch = between.match(/export\s+CERAPH_SIGNAL_HOST=(\S+)/);
83
- const value = exportMatch ? exportMatch[1] : "";
84
- return { start: headIdx, end, value };
85
- }
@@ -1,135 +0,0 @@
1
- import { type PackageManager } from "./package-manager.js";
2
- import { type RootComponentTarget } from "./ast-layout.js";
3
- import { type SchemeAction } from "./url-scheme.js";
4
- import { type MonorepoCandidate } from "./monorepo.js";
5
- import { syncCameraRegistry, relativeRegistryPath } from "../camera-registry-sync.js";
6
- import { type PreflightCheck } from "../preflight.js";
7
- import { ScreenManager } from "../screen.js";
8
- import { AppLifecycle } from "../app-lifecycle.js";
9
- import { DeviceAutonomy } from "../device-autonomy.js";
10
- import type { TargetResolver } from "../target.js";
11
- export interface InitStatus {
12
- workingDir: string;
13
- packageInstalled: boolean;
14
- authenticated: boolean;
15
- authLogin?: string;
16
- schemeRegistered: boolean | null;
17
- projectKind: "expo-json" | "expo-config" | "bare-rn" | "none";
18
- cameraScanResults: Array<{
19
- relPath: string;
20
- line: number;
21
- alreadyHasImageKey: boolean;
22
- suggestedKey: string;
23
- }>;
24
- bootInjected: boolean | null;
25
- bootTarget: RootComponentTarget | null;
26
- mcpClientsPresent: {
27
- claudeCode: boolean;
28
- cursor: boolean;
29
- codex: boolean;
30
- vscode: boolean;
31
- };
32
- }
33
- export declare function getInitStatus(projectDir: string): Promise<InitStatus>;
34
- export interface InstallPackageResult {
35
- already: boolean;
36
- packageManager: PackageManager;
37
- exitCode?: number;
38
- }
39
- export declare function installCeraphPackage(projectDir: string, deps?: {
40
- spawnInstall?: (input: {
41
- bin: string;
42
- args: string[];
43
- cwd: string;
44
- }) => Promise<{
45
- exitCode: number;
46
- }>;
47
- }): Promise<InstallPackageResult>;
48
- export interface AuthStartResult {
49
- verificationUri: string;
50
- userCode: string;
51
- pollUrl: string;
52
- expiresIn: number;
53
- intervalSeconds: number;
54
- }
55
- export declare function startAuthFlow(): Promise<AuthStartResult>;
56
- export type AuthPollResult = {
57
- status: "pending";
58
- } | {
59
- status: "slow_down";
60
- intervalSeconds: number;
61
- } | {
62
- status: "complete";
63
- login: string;
64
- } | {
65
- status: "failed";
66
- reason: "expired" | "denied" | "unknown-poll-url";
67
- };
68
- export declare function pollAuthFlow(pollUrl: string): Promise<AuthPollResult>;
69
- export interface ScanCameraUsage {
70
- filePath: string;
71
- relPath: string;
72
- line: number;
73
- suggestedKey: string;
74
- alreadyHasImageKey: boolean;
75
- surroundingCode: string;
76
- }
77
- export declare function scanCameraUsages(projectDir: string): Promise<{
78
- usages: ScanCameraUsage[];
79
- filesScanned: number;
80
- }>;
81
- export interface ReplaceCameraInput {
82
- filePath: string;
83
- line: number;
84
- imageKey: string;
85
- }
86
- export interface ReplaceCameraResult {
87
- success: boolean;
88
- already?: boolean;
89
- newContent?: string;
90
- reason?: string;
91
- }
92
- export declare function replaceCameraView(projectDir: string, input: ReplaceCameraInput): Promise<ReplaceCameraResult>;
93
- export interface InjectBootResult {
94
- applied: boolean;
95
- filePath?: string;
96
- already?: boolean;
97
- reason?: string;
98
- }
99
- export declare function injectBoot(projectDir: string): Promise<InjectBootResult>;
100
- export interface RegisterSchemeResult {
101
- action: SchemeAction;
102
- instructions?: string;
103
- }
104
- export declare function registerScheme(projectDir: string): Promise<RegisterSchemeResult>;
105
- export interface SetupImagesDirResult {
106
- dirPath: string;
107
- readmeWritten: boolean;
108
- gitignoreUpdated: boolean;
109
- }
110
- export declare function setupImagesDir(projectDir: string): Promise<SetupImagesDirResult>;
111
- export interface SetupMcpClientsResult {
112
- written: string[];
113
- skipped: string[];
114
- }
115
- export declare function setupMcpClients(projectDir: string): Promise<SetupMcpClientsResult>;
116
- export interface PreflightToolResult {
117
- ok: boolean;
118
- passedCount: number;
119
- totalCount: number;
120
- checks: PreflightCheck[];
121
- }
122
- export declare function preflightForMcp(projectDir: string, deps?: {
123
- screen?: ScreenManager;
124
- apps?: AppLifecycle;
125
- autonomy?: DeviceAutonomy;
126
- target?: TargetResolver;
127
- iproxyManager?: import("../iproxy-manager.js").IproxyManager | null;
128
- }): Promise<PreflightToolResult>;
129
- export interface MonorepoStatusResult {
130
- isMonorepo: boolean;
131
- rootIsRnApp: boolean;
132
- matches: MonorepoCandidate[];
133
- }
134
- export declare function detectMonorepoStatus(projectDir: string): Promise<MonorepoStatusResult>;
135
- export { syncCameraRegistry, relativeRegistryPath };
@@ -1,399 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { mkdir, readFile, writeFile, access } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { detectPackageManager, installArgv, isPackageInstalled, } from "./package-manager.js";
6
- import { finalizeAndPersist, pollDeviceFlow, readExistingAuth, startDeviceFlow, } from "./oauth.js";
7
- import { applyCameraEdits, scanCameraViews, setImageKeyOnExistingTag, } from "./ast-camera.js";
8
- import { detectRootComponent, injectInstallCeraph, } from "./ast-layout.js";
9
- import { ensureCeraphUrlScheme, bareRnInstructions, expoDynamicConfigInstructions, } from "./url-scheme.js";
10
- import { detectMonorepoSubpackages, } from "./monorepo.js";
11
- import { syncCameraRegistry, relativeRegistryPath, } from "../camera-registry-sync.js";
12
- import { runPreflight } from "../preflight.js";
13
- import { ScreenManager } from "../screen.js";
14
- import { AppLifecycle } from "../app-lifecycle.js";
15
- import { DeviceAutonomy } from "../device-autonomy.js";
16
- export async function getInitStatus(projectDir) {
17
- const [pkgInstalled, auth, scheme, scan, bootTarget] = await Promise.all([
18
- isPackageInstalled(projectDir),
19
- readExistingAuth(),
20
- ensureCeraphUrlSchemeReadOnly(projectDir),
21
- scanCameraViews(projectDir),
22
- detectRootComponent(projectDir),
23
- ]);
24
- const bootInjected = bootTarget
25
- ? (await readFile(bootTarget.filePath, "utf-8")).includes("installCeraph(")
26
- : null;
27
- const schemeRegistered = scheme.kind === "expo-json-already-registered"
28
- ? true
29
- : scheme.kind === "expo-json-added"
30
- ?
31
- false
32
- : null;
33
- const projectKind = scheme.kind === "expo-json-added" ||
34
- scheme.kind === "expo-json-already-registered"
35
- ? "expo-json"
36
- : scheme.kind === "expo-config-needs-manual"
37
- ? "expo-config"
38
- : scheme.kind === "bare-rn-needs-manual"
39
- ? "bare-rn"
40
- : "none";
41
- return {
42
- workingDir: projectDir,
43
- packageInstalled: pkgInstalled,
44
- authenticated: auth != null,
45
- authLogin: auth?.githubUser.login,
46
- schemeRegistered,
47
- projectKind,
48
- cameraScanResults: scan.edits.map((e) => ({
49
- relPath: e.relPath,
50
- line: e.line,
51
- alreadyHasImageKey: e.alreadyHasImageKey,
52
- suggestedKey: e.suggestedKey,
53
- })),
54
- bootInjected,
55
- bootTarget,
56
- mcpClientsPresent: {
57
- claudeCode: await fileExists(join(projectDir, ".mcp.json")),
58
- cursor: await fileExists(join(projectDir, ".cursor", "mcp.json")),
59
- codex: await fileExists(join(projectDir, ".codex", "config.toml")),
60
- vscode: await fileExists(join(projectDir, ".vscode", "mcp.json")),
61
- },
62
- };
63
- }
64
- async function ensureCeraphUrlSchemeReadOnly(projectDir) {
65
- return ensureCeraphUrlScheme(projectDir, {
66
- writeFile: async () => undefined,
67
- });
68
- }
69
- export async function installCeraphPackage(projectDir, deps = {}) {
70
- const pm = await detectPackageManager(projectDir);
71
- if (await isPackageInstalled(projectDir)) {
72
- return { already: true, packageManager: pm };
73
- }
74
- const argv = installArgv(pm);
75
- const installer = deps.spawnInstall ?? defaultSpawnInstall;
76
- const res = await installer({ bin: argv.bin, args: argv.args, cwd: projectDir });
77
- return { already: false, packageManager: pm, exitCode: res.exitCode };
78
- }
79
- function defaultSpawnInstall(input) {
80
- return new Promise((resolve) => {
81
- const child = spawn(input.bin, input.args, {
82
- cwd: input.cwd,
83
- stdio: "inherit",
84
- });
85
- child.on("error", () => resolve({ exitCode: 127 }));
86
- child.on("exit", (code) => resolve({ exitCode: code ?? 1 }));
87
- });
88
- }
89
- const deviceFlows = new Map();
90
- export async function startAuthFlow() {
91
- const now = Date.now();
92
- for (const [key, entry] of deviceFlows) {
93
- if (now - entry.createdAt > 15 * 60_000)
94
- deviceFlows.delete(key);
95
- }
96
- const start = await startDeviceFlow();
97
- const pollUrl = randomBytes(16).toString("hex");
98
- deviceFlows.set(pollUrl, {
99
- start,
100
- deviceCode: start.deviceCode,
101
- createdAt: Date.now(),
102
- });
103
- return {
104
- verificationUri: start.verificationUri,
105
- userCode: start.userCode,
106
- pollUrl,
107
- expiresIn: start.expiresIn,
108
- intervalSeconds: start.intervalSeconds,
109
- };
110
- }
111
- export async function pollAuthFlow(pollUrl) {
112
- const entry = deviceFlows.get(pollUrl);
113
- if (!entry)
114
- return { status: "failed", reason: "unknown-poll-url" };
115
- const result = await pollDeviceFlow(entry.deviceCode);
116
- if (result.kind === "pending")
117
- return { status: "pending" };
118
- if (result.kind === "slow_down")
119
- return { status: "slow_down", intervalSeconds: result.intervalSeconds };
120
- if (result.kind === "expired") {
121
- deviceFlows.delete(pollUrl);
122
- return { status: "failed", reason: "expired" };
123
- }
124
- if (result.kind === "denied") {
125
- deviceFlows.delete(pollUrl);
126
- return { status: "failed", reason: "denied" };
127
- }
128
- const stored = await finalizeAndPersist(result.accessToken);
129
- deviceFlows.delete(pollUrl);
130
- return { status: "complete", login: stored.githubUser.login };
131
- }
132
- export async function scanCameraUsages(projectDir) {
133
- const scan = await scanCameraViews(projectDir);
134
- const usages = [];
135
- const cache = new Map();
136
- for (const edit of scan.edits) {
137
- let body = cache.get(edit.filePath);
138
- if (body === undefined) {
139
- body = await readFile(edit.filePath, "utf-8").catch(() => "");
140
- cache.set(edit.filePath, body);
141
- }
142
- usages.push({
143
- filePath: edit.filePath,
144
- relPath: edit.relPath,
145
- line: edit.line,
146
- suggestedKey: edit.suggestedKey,
147
- alreadyHasImageKey: edit.alreadyHasImageKey,
148
- surroundingCode: extractWindow(body, edit.line, 15),
149
- });
150
- }
151
- return { usages, filesScanned: scan.filesScanned };
152
- }
153
- function extractWindow(body, centerLine, half) {
154
- const lines = body.split(/\r?\n/);
155
- const start = Math.max(0, centerLine - 1 - half);
156
- const end = Math.min(lines.length, centerLine - 1 + half + 1);
157
- return lines.slice(start, end).join("\n");
158
- }
159
- export async function replaceCameraView(projectDir, input) {
160
- const scan = await scanCameraViews(projectDir);
161
- const match = scan.edits.find((e) => e.filePath === input.filePath && e.line === input.line);
162
- if (!match) {
163
- const body = await readFile(input.filePath, "utf-8").catch(() => null);
164
- if (body == null) {
165
- return {
166
- success: false,
167
- reason: `File not found: ${input.filePath}`,
168
- };
169
- }
170
- if (body.includes("CeraphCamera")) {
171
- const res = await setImageKeyOnExistingTag(input.filePath, input.line, input.imageKey);
172
- if (res.applied) {
173
- const newContent = await readFile(input.filePath, "utf-8");
174
- return { success: true, newContent };
175
- }
176
- if (res.reason === "imageKey unchanged") {
177
- return { success: true, already: true, newContent: body };
178
- }
179
- return {
180
- success: false,
181
- reason: res.reason ?? `No <CeraphCamera> at ${input.filePath}:${input.line}`,
182
- };
183
- }
184
- return {
185
- success: false,
186
- reason: `No <CameraView> at ${input.filePath}:${input.line}`,
187
- };
188
- }
189
- const edit = {
190
- filePath: match.filePath,
191
- relPath: match.relPath,
192
- line: match.line,
193
- suggestedKey: input.imageKey,
194
- alreadyHasImageKey: match.alreadyHasImageKey,
195
- };
196
- await applyCameraEdits(projectDir, [edit]);
197
- const newContent = await readFile(input.filePath, "utf-8");
198
- return { success: true, newContent };
199
- }
200
- export async function injectBoot(projectDir) {
201
- const target = await detectRootComponent(projectDir);
202
- if (!target) {
203
- return {
204
- applied: false,
205
- reason: "Could not find app/_layout.tsx or App.tsx — add `useEffect(() => { installCeraph(); }, [])` manually.",
206
- };
207
- }
208
- const body = await readFile(target.filePath, "utf-8").catch(() => "");
209
- const alreadyCalled = body.includes("installCeraph(");
210
- const result = await injectInstallCeraph(target);
211
- return {
212
- applied: result.applied,
213
- filePath: target.filePath,
214
- already: alreadyCalled,
215
- reason: result.reason,
216
- };
217
- }
218
- export async function registerScheme(projectDir) {
219
- const action = await ensureCeraphUrlScheme(projectDir);
220
- let instructions;
221
- if (action.kind === "expo-config-needs-manual") {
222
- instructions = expoDynamicConfigInstructions(action.path);
223
- }
224
- else if (action.kind === "bare-rn-needs-manual") {
225
- instructions = bareRnInstructions();
226
- }
227
- return { action, instructions };
228
- }
229
- const CAMERA_IMAGES_README_CONTENT = `# Ceraph camera test images
230
-
231
- Per-screen test images for \`<CeraphCamera />\` from
232
- \`@ceraph/react-native-mcp/shim\`. Three valid imageKey shapes:
233
-
234
- - \`<CeraphCamera />\` (no imageKey) → uninitialized; the bundled
235
- 1024x1024 black PNG renders. \`rn_preflight\` lists these so the
236
- agent prompts you to pick a deliberate value.
237
- - \`imageKey="default"\` → explicit acknowledgment of the bundled
238
- black PNG. Best when image content doesn't matter (most form /
239
- upload / permission flows).
240
- - \`imageKey="<scenario>"\` → a file in this folder. Drop
241
- \`<scenario>.{jpg,png,webp,heic}\` here.
242
- - \`imageKey="@runtime"\` → the flow planner picks the image
243
- per-step at test time. Use for cameras that serve multiple
244
- scenarios across flows.
245
-
246
- ## Naming
247
-
248
- Lowercase + hyphen-separated, descriptive of what the image represents:
249
-
250
- - \`profile.jpg\` — face photo for a profile picture screen
251
- - \`id-card.png\` — government-issued ID for a KYC scan screen
252
- - \`product-photo.jpg\` — a product image for an upload screen
253
-
254
- Supported extensions: \`.jpg\`, \`.jpeg\`, \`.png\`, \`.webp\`, \`.heic\`.
255
- Extension priority on collision: jpg > jpeg > png > webp > heic.
256
- `;
257
- export async function setupImagesDir(projectDir) {
258
- const dir = join(projectDir, ".ceraph", "camera-images");
259
- await mkdir(dir, { recursive: true });
260
- const readmePath = join(dir, "README.md");
261
- const readmeWritten = !(await fileExists(readmePath));
262
- if (readmeWritten) {
263
- await writeFile(readmePath, CAMERA_IMAGES_README_CONTENT, "utf-8");
264
- }
265
- const gitignorePath = join(projectDir, ".gitignore");
266
- let content = "";
267
- try {
268
- content = await readFile(gitignorePath, "utf-8");
269
- }
270
- catch {
271
- }
272
- const entries = [".rn-errors.json", ".rn-flow-progress.json", ".rn-mcp-cache/"];
273
- const missing = entries.filter((e) => !content.includes(e));
274
- let gitignoreUpdated = false;
275
- if (missing.length > 0) {
276
- const prefix = content === "" || content.endsWith("\n") ? "" : "\n";
277
- await writeFile(gitignorePath, content + prefix + missing.join("\n") + "\n", "utf-8");
278
- gitignoreUpdated = true;
279
- }
280
- return { dirPath: dir, readmeWritten, gitignoreUpdated };
281
- }
282
- const MCP_ENTRY = {
283
- "mobile-mcp": {
284
- command: "npx",
285
- args: ["-y", "@mobilenext/mobile-mcp@latest"],
286
- },
287
- "react-native-mcp": {
288
- command: "npx",
289
- args: ["-y", "@ceraph/react-native-mcp@latest"],
290
- },
291
- };
292
- export async function setupMcpClients(projectDir) {
293
- const written = [];
294
- const skipped = [];
295
- const targets = [
296
- { name: "Claude Code", path: join(projectDir, ".mcp.json") },
297
- { name: "Cursor", path: join(projectDir, ".cursor", "mcp.json") },
298
- {
299
- name: "Codex",
300
- path: join(projectDir, ".codex", "config.toml"),
301
- toml: true,
302
- },
303
- { name: "VS Code", path: join(projectDir, ".vscode", "mcp.json") },
304
- ];
305
- for (const t of targets) {
306
- const did = t.toml
307
- ? await setupCodexToml(t.path)
308
- : await setupJsonMcp(t.path);
309
- if (did)
310
- written.push(t.name);
311
- else
312
- skipped.push(t.name);
313
- }
314
- return { written, skipped };
315
- }
316
- async function setupJsonMcp(configPath) {
317
- const existing = await readJsonOrEmpty(configPath);
318
- const servers = (existing.mcpServers ?? {});
319
- if (servers["react-native-mcp"])
320
- return false;
321
- servers["mobile-mcp"] = MCP_ENTRY["mobile-mcp"];
322
- servers["react-native-mcp"] = MCP_ENTRY["react-native-mcp"];
323
- existing.mcpServers = servers;
324
- await mkdir(join(configPath, ".."), { recursive: true });
325
- await writeFile(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
326
- return true;
327
- }
328
- async function setupCodexToml(configPath) {
329
- let content = "";
330
- try {
331
- content = await readFile(configPath, "utf-8");
332
- }
333
- catch {
334
- }
335
- if (content.includes("react-native-mcp"))
336
- return false;
337
- const tomlBlock = `
338
- [mcp_servers.mobile-mcp]
339
- command = "npx"
340
- args = ["-y", "@mobilenext/mobile-mcp@latest"]
341
-
342
- [mcp_servers.react-native-mcp]
343
- command = "npx"
344
- args = ["-y", "@ceraph/react-native-mcp@latest"]
345
- `;
346
- await mkdir(join(configPath, ".."), { recursive: true });
347
- await writeFile(configPath, content + tomlBlock, "utf-8");
348
- return true;
349
- }
350
- async function readJsonOrEmpty(path) {
351
- try {
352
- const raw = await readFile(path, "utf-8");
353
- const parsed = JSON.parse(raw);
354
- if (parsed && typeof parsed === "object") {
355
- return parsed;
356
- }
357
- }
358
- catch {
359
- }
360
- return {};
361
- }
362
- export async function preflightForMcp(projectDir, deps = {}) {
363
- const screen = deps.screen ?? new ScreenManager(deps.target ? { targetResolver: deps.target } : {});
364
- const apps = deps.apps ?? new AppLifecycle(screen, deps.target);
365
- const autonomy = deps.autonomy ?? new DeviceAutonomy(screen);
366
- const res = await runPreflight({
367
- screen,
368
- apps,
369
- autonomy,
370
- projectDir,
371
- target: deps.target,
372
- iproxyManager: deps.iproxyManager ?? null,
373
- });
374
- const passedCount = res.checks.filter((c) => c.ok).length;
375
- return {
376
- ok: res.ok,
377
- passedCount,
378
- totalCount: res.checks.length,
379
- checks: res.checks,
380
- };
381
- }
382
- export async function detectMonorepoStatus(projectDir) {
383
- const detection = await detectMonorepoSubpackages(projectDir);
384
- return {
385
- isMonorepo: detection.isMonorepo,
386
- rootIsRnApp: detection.rootIsRnApp,
387
- matches: detection.matches,
388
- };
389
- }
390
- async function fileExists(path) {
391
- try {
392
- await access(path);
393
- return true;
394
- }
395
- catch {
396
- return false;
397
- }
398
- }
399
- export { syncCameraRegistry, relativeRegistryPath };
@@ -1,42 +0,0 @@
1
- export type SchemeAction = {
2
- kind: "expo-json-added";
3
- previousScheme: SchemeShape;
4
- nextScheme: string[];
5
- path: string;
6
- } | {
7
- kind: "expo-json-already-registered";
8
- scheme: SchemeShape;
9
- path: string;
10
- } | {
11
- kind: "expo-config-needs-manual";
12
- path: string;
13
- } | {
14
- kind: "bare-rn-needs-manual";
15
- } | {
16
- kind: "no-rn-project";
17
- };
18
- export type SchemeShape = string | string[] | undefined;
19
- export interface UrlSchemeDeps {
20
- readFile?: (p: string) => Promise<string>;
21
- writeFile?: (p: string, content: string) => Promise<void>;
22
- fileExists?: (p: string) => Promise<boolean>;
23
- }
24
- export declare const CERAPH_SCHEME = "ceraph";
25
- export declare function ensureCeraphUrlScheme(projectDir: string, deps?: UrlSchemeDeps): Promise<SchemeAction>;
26
- interface ParsedAppJson {
27
- expo?: {
28
- scheme?: SchemeShape;
29
- [k: string]: unknown;
30
- };
31
- [k: string]: unknown;
32
- }
33
- interface InjectResult {
34
- action: "added" | "already";
35
- scheme: SchemeShape;
36
- previousScheme: SchemeShape;
37
- nextScheme: string[];
38
- }
39
- export declare function injectSchemeIntoAppJson(parsed: ParsedAppJson, schemeToAdd: string): InjectResult;
40
- export declare function bareRnInstructions(): string;
41
- export declare function expoDynamicConfigInstructions(configPath: string): string;
42
- export {};