@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
@@ -0,0 +1,280 @@
1
+ import { readdir, rename, unlink, writeFile, mkdir, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+ import { syncCameraRegistry, relativeRegistryPath, } from "./camera-registry-sync.js";
4
+ export const SUPPORTED_EXTENSIONS = [
5
+ "jpg",
6
+ "jpeg",
7
+ "png",
8
+ "webp",
9
+ "heic",
10
+ ];
11
+ const CAMERA_IMAGES_SUBPATH = join(".ceraph", "camera-images");
12
+ export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
13
+ const IMAGE_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
+ const SUBDIR_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
+ export class AddCameraImageError extends Error {
16
+ code;
17
+ remediation;
18
+ constructor(code, message, remediation) {
19
+ super(message);
20
+ this.name = "AddCameraImageError";
21
+ this.code = code;
22
+ this.remediation = remediation;
23
+ }
24
+ }
25
+ export function validateImageKey(key) {
26
+ if (typeof key !== "string" || key.length === 0) {
27
+ throw new AddCameraImageError("invalid-image-key", "imageKey is required.", "Pass a non-empty `imageKey` like `\"profile\"` or `\"handwriting-sample-1\"`.");
28
+ }
29
+ if (key.length > 64) {
30
+ throw new AddCameraImageError("invalid-image-key", `imageKey '${key}' is longer than 64 characters.`, "Keep imageKey short and descriptive (lowercase, hyphen-separated).");
31
+ }
32
+ if (!IMAGE_KEY_REGEX.test(key)) {
33
+ throw new AddCameraImageError("invalid-image-key", `imageKey '${key}' must be lowercase letters/digits separated by single hyphens.`, "Use only [a-z0-9-], no leading/trailing/consecutive hyphens, no spaces, " +
34
+ "no uppercase. Example: `handwriting-sample-1`.");
35
+ }
36
+ }
37
+ export function validateSubDir(subDir) {
38
+ if (typeof subDir !== "string" || subDir.length === 0) {
39
+ throw new AddCameraImageError("invalid-subdir", "subDir was provided but is empty.", "Either omit `subDir` or pass a single safe path segment " +
40
+ "(lowercase, hyphen-separated, no slashes).");
41
+ }
42
+ if (subDir.length > 64) {
43
+ throw new AddCameraImageError("invalid-subdir", `subDir '${subDir}' is longer than 64 characters.`, "Use a short, single-segment subdirectory name.");
44
+ }
45
+ if (!SUBDIR_REGEX.test(subDir)) {
46
+ throw new AddCameraImageError("invalid-subdir", `subDir '${subDir}' must be a single lowercase segment.`, "Use only [a-z0-9-], no slashes, no `..`, no leading `/`. " +
47
+ "Example: `id-documents`.");
48
+ }
49
+ }
50
+ export function detectImageType(bytes) {
51
+ if (bytes.length < 12)
52
+ return null;
53
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
54
+ return { ext: "jpg", mime: "image/jpeg" };
55
+ }
56
+ if (bytes[0] === 0x89 &&
57
+ bytes[1] === 0x50 &&
58
+ bytes[2] === 0x4e &&
59
+ bytes[3] === 0x47 &&
60
+ bytes[4] === 0x0d &&
61
+ bytes[5] === 0x0a &&
62
+ bytes[6] === 0x1a &&
63
+ bytes[7] === 0x0a) {
64
+ return { ext: "png", mime: "image/png" };
65
+ }
66
+ if (bytes[0] === 0x52 &&
67
+ bytes[1] === 0x49 &&
68
+ bytes[2] === 0x46 &&
69
+ bytes[3] === 0x46 &&
70
+ bytes[8] === 0x57 &&
71
+ bytes[9] === 0x45 &&
72
+ bytes[10] === 0x42 &&
73
+ bytes[11] === 0x50) {
74
+ return { ext: "webp", mime: "image/webp" };
75
+ }
76
+ if (bytes[4] === 0x66 &&
77
+ bytes[5] === 0x74 &&
78
+ bytes[6] === 0x79 &&
79
+ bytes[7] === 0x70) {
80
+ const brand = String.fromCharCode(bytes[8] ?? 0, bytes[9] ?? 0, bytes[10] ?? 0, bytes[11] ?? 0);
81
+ const HEIF_BRANDS = new Set([
82
+ "heic",
83
+ "heix",
84
+ "hevc",
85
+ "hevx",
86
+ "heim",
87
+ "heis",
88
+ "mif1",
89
+ "msf1",
90
+ ]);
91
+ if (HEIF_BRANDS.has(brand)) {
92
+ return { ext: "heic", mime: "image/heic" };
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ export function normalizeContentTypeHint(hint) {
98
+ if (!hint || typeof hint !== "string")
99
+ return null;
100
+ const lower = hint.trim().toLowerCase();
101
+ if (lower.length === 0)
102
+ return null;
103
+ let tail = lower;
104
+ if (tail.startsWith("image/"))
105
+ tail = tail.slice("image/".length);
106
+ if (tail.startsWith("."))
107
+ tail = tail.slice(1);
108
+ const semi = tail.indexOf(";");
109
+ if (semi >= 0)
110
+ tail = tail.slice(0, semi).trim();
111
+ if (tail === "jpeg" || tail === "jpg")
112
+ return "jpg";
113
+ if (tail === "png")
114
+ return "png";
115
+ if (tail === "webp")
116
+ return "webp";
117
+ if (tail === "heic" || tail === "heif")
118
+ return "heic";
119
+ return null;
120
+ }
121
+ async function findExistingForKey(dir, imageKey) {
122
+ let entries;
123
+ try {
124
+ const s = await stat(dir);
125
+ if (!s.isDirectory())
126
+ return null;
127
+ entries = await readdir(dir);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ const lowerKey = imageKey.toLowerCase();
133
+ for (const name of entries) {
134
+ const dot = name.lastIndexOf(".");
135
+ if (dot <= 0)
136
+ continue;
137
+ const stem = name.slice(0, dot);
138
+ if (stem.toLowerCase() !== lowerKey)
139
+ continue;
140
+ const extWithDot = name.slice(dot).toLowerCase();
141
+ const ext = extWithDot.startsWith(".") ? extWithDot.slice(1) : extWithDot;
142
+ if (!SUPPORTED_EXTENSIONS.includes(ext))
143
+ continue;
144
+ return {
145
+ absolutePath: join(dir, name),
146
+ ext: ext,
147
+ };
148
+ }
149
+ return null;
150
+ }
151
+ export async function addCameraImage(projectDir, options, deps = {}) {
152
+ const { imageKey, imageBase64, contentType, overwrite, subDir } = options;
153
+ const sync = deps.syncCameraRegistry ?? syncCameraRegistry;
154
+ validateImageKey(imageKey);
155
+ if (subDir !== undefined && subDir !== "") {
156
+ validateSubDir(subDir);
157
+ }
158
+ if (typeof imageBase64 !== "string" || imageBase64.length === 0) {
159
+ throw new AddCameraImageError("invalid-base64", "imageBase64 is required and must be a non-empty string.", "Pass the base64-encoded image bytes (without a `data:` prefix).");
160
+ }
161
+ const rawBase64 = imageBase64.startsWith("data:")
162
+ ? imageBase64.slice(imageBase64.indexOf(",") + 1)
163
+ : imageBase64;
164
+ const MAX_BASE64_CHARS = Math.ceil((MAX_IMAGE_BYTES * 4) / 3) + 32;
165
+ if (rawBase64.length > MAX_BASE64_CHARS) {
166
+ const approxMb = ((rawBase64.length * 3) / 4 / (1024 * 1024)).toFixed(2);
167
+ throw new AddCameraImageError("image-too-large", `Encoded payload is ~${approxMb} MB which exceeds the 5 MB cap.`, "Resize or recompress the image before uploading. Test fixtures " +
168
+ "rarely need to be larger than 1 MB. Cap: 5 MB.");
169
+ }
170
+ const bytes = Buffer.from(rawBase64, "base64");
171
+ if (bytes.length === 0) {
172
+ throw new AddCameraImageError("invalid-base64", "imageBase64 decoded to zero bytes.", "Verify the base64 string is intact and not a placeholder.");
173
+ }
174
+ if (bytes.length > MAX_IMAGE_BYTES) {
175
+ const mb = (bytes.length / (1024 * 1024)).toFixed(2);
176
+ throw new AddCameraImageError("image-too-large", `Image is ${mb} MB which exceeds the 5 MB cap.`, "Resize or recompress the image before uploading. Test fixtures " +
177
+ "rarely need to be larger than 1 MB — over-large fixtures inflate " +
178
+ "the repo and slow Metro bundling. Cap: 5 MB.");
179
+ }
180
+ const detected = detectImageType(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength));
181
+ if (!detected) {
182
+ throw new AddCameraImageError("unsupported-image-format", "Could not detect a supported image format from the decoded bytes.", "Supported formats: JPEG, PNG, WebP, HEIC. " +
183
+ "Re-export the image in one of these and try again.");
184
+ }
185
+ const warnings = [];
186
+ if (contentType !== undefined && contentType !== "") {
187
+ const hinted = normalizeContentTypeHint(contentType);
188
+ if (hinted === null) {
189
+ warnings.push({
190
+ code: "content-type-unrecognized",
191
+ message: `contentType '${contentType}' is not a recognized image MIME type; ` +
192
+ `using detected '${detected.mime}' instead.`,
193
+ });
194
+ }
195
+ else if (hinted !== detected.ext) {
196
+ warnings.push({
197
+ code: "content-type-mismatch",
198
+ message: `Declared contentType '${contentType}' does not match detected ` +
199
+ `'${detected.mime}' — trusting the bytes.`,
200
+ });
201
+ }
202
+ }
203
+ if (bytes.length > 1 * 1024 * 1024) {
204
+ const mb = (bytes.length / (1024 * 1024)).toFixed(2);
205
+ warnings.push({
206
+ code: "image-larger-than-typical",
207
+ message: `Image is ${mb} MB. Test fixtures typically fit under 1 MB; ` +
208
+ `consider downsampling if it's larger than the camera screen needs.`,
209
+ });
210
+ }
211
+ const baseDir = join(projectDir, CAMERA_IMAGES_SUBPATH);
212
+ const targetDir = subDir !== undefined && subDir !== "" ? join(baseDir, subDir) : baseDir;
213
+ const rel = relative(baseDir, targetDir);
214
+ if (rel.startsWith("..") || isAbsolute(rel) || rel.includes("\0")) {
215
+ throw new AddCameraImageError("path-traversal", `Resolved subDir escapes .ceraph/camera-images/: ${targetDir}`, "Pass a single safe segment for subDir (lowercase, hyphens, " +
216
+ "no slashes, no `..`).");
217
+ }
218
+ const targetExt = detected.ext;
219
+ const targetFile = join(targetDir, `${imageKey}.${targetExt}`);
220
+ const existing = await findExistingForKey(targetDir, imageKey);
221
+ let removedSiblingPath;
222
+ let replaced = false;
223
+ if (existing) {
224
+ if (!overwrite) {
225
+ const relExisting = relative(projectDir, existing.absolutePath);
226
+ throw new AddCameraImageError("collision", `The imageKey '${imageKey}' already maps to ${relExisting}.`, `Pass \`overwrite: true\` to replace it, or pick a different imageKey.`);
227
+ }
228
+ replaced = true;
229
+ }
230
+ await mkdir(targetDir, { recursive: true });
231
+ if (existing &&
232
+ existing.ext === targetExt &&
233
+ existing.absolutePath !== targetFile &&
234
+ existing.absolutePath.toLowerCase() === targetFile.toLowerCase()) {
235
+ try {
236
+ await rename(existing.absolutePath, targetFile);
237
+ }
238
+ catch (err) {
239
+ void err;
240
+ }
241
+ }
242
+ await writeFile(targetFile, bytes);
243
+ if (existing) {
244
+ const oldPath = existing.absolutePath;
245
+ const newPath = targetFile;
246
+ const samePathInsensitive = oldPath.toLowerCase() === newPath.toLowerCase();
247
+ const extChanged = existing.ext !== targetExt;
248
+ const stemCaseChanged = !samePathInsensitive && oldPath !== newPath;
249
+ if (!samePathInsensitive && (extChanged || stemCaseChanged)) {
250
+ try {
251
+ await unlink(oldPath);
252
+ removedSiblingPath = relative(projectDir, oldPath);
253
+ }
254
+ catch (err) {
255
+ throw new AddCameraImageError("unlink-failed", `New image was written but failed to remove stale ${relative(projectDir, oldPath)}: ` +
256
+ `${err instanceof Error ? err.message : String(err)}`, "Remove the stale file manually so the imageKey is not backed by two files.");
257
+ }
258
+ }
259
+ }
260
+ let syncResult;
261
+ try {
262
+ syncResult = await sync(projectDir);
263
+ }
264
+ catch (err) {
265
+ throw new AddCameraImageError("registry-sync-failed", `Image was written but registry sync failed: ${err instanceof Error ? err.message : String(err)}`, "Re-run `rn_sync_camera_registry` manually to regenerate `_registry.ts`. " +
266
+ "The image file itself is on disk and safe.");
267
+ }
268
+ return {
269
+ written: relative(projectDir, targetFile),
270
+ registry: relativeRegistryPath(projectDir, syncResult.registryPath),
271
+ registered: syncResult.registered.map((r) => r.key),
272
+ detectedContentType: detected.mime,
273
+ detectedExt: detected.ext,
274
+ replaced,
275
+ removedSiblingPath,
276
+ bytes: bytes.length,
277
+ warnings,
278
+ registryState: syncResult.writtenOrUnchanged,
279
+ };
280
+ }
@@ -0,0 +1,18 @@
1
+ declare const SUPPORTED_EXTENSIONS: readonly [".jpg", ".jpeg", ".png", ".webp", ".heic"];
2
+ type SupportedExt = (typeof SUPPORTED_EXTENSIONS)[number];
3
+ declare const REGISTRY_BASENAME = "_registry.ts";
4
+ declare const CAMERA_IMAGES_SUBPATH: string;
5
+ export interface RegisteredImage {
6
+ key: string;
7
+ relativePath: string;
8
+ ext: SupportedExt;
9
+ }
10
+ export interface SyncResult {
11
+ registered: RegisteredImage[];
12
+ registryPath: string;
13
+ writtenOrUnchanged: "written" | "unchanged" | "missing-dir";
14
+ }
15
+ export declare function buildRegistrySource(images: RegisteredImage[]): string;
16
+ export declare function syncCameraRegistry(projectDir: string): Promise<SyncResult>;
17
+ export declare function relativeRegistryPath(projectDir: string, registryPath: string): string;
18
+ export { CAMERA_IMAGES_SUBPATH, REGISTRY_BASENAME };
@@ -0,0 +1,117 @@
1
+ import { readdir, readFile, writeFile, mkdir, stat } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ const SUPPORTED_EXTENSIONS = [
4
+ ".jpg",
5
+ ".jpeg",
6
+ ".png",
7
+ ".webp",
8
+ ".heic",
9
+ ];
10
+ const REGISTRY_BASENAME = "_registry.ts";
11
+ const CAMERA_IMAGES_SUBPATH = join(".ceraph", "camera-images");
12
+ function isSupportedExt(ext) {
13
+ return SUPPORTED_EXTENSIONS.includes(ext);
14
+ }
15
+ function extPriority(ext) {
16
+ return SUPPORTED_EXTENSIONS.indexOf(ext);
17
+ }
18
+ function splitImageName(name) {
19
+ const dot = name.lastIndexOf(".");
20
+ if (dot <= 0)
21
+ return null;
22
+ const stem = name.slice(0, dot);
23
+ const ext = name.slice(dot).toLowerCase();
24
+ if (!isSupportedExt(ext))
25
+ return null;
26
+ if (stem.length === 0)
27
+ return null;
28
+ return { stem, ext };
29
+ }
30
+ export function buildRegistrySource(images) {
31
+ const header = "// AUTO-GENERATED by @ceraph/react-native-mcp — do not edit by hand.\n" +
32
+ "// Re-run `rn_sync_camera_registry` after adding or removing images.\n" +
33
+ 'import { configureTestImage } from "@ceraph/react-native-mcp/shim";\n' +
34
+ "\n" +
35
+ "export function applyCameraImageRegistry(): void {\n";
36
+ if (images.length === 0) {
37
+ return (header +
38
+ " // No images registered yet. Drop files into .ceraph/camera-images/\n" +
39
+ " // and re-run `rn_sync_camera_registry`.\n" +
40
+ "}\n");
41
+ }
42
+ const keyColumn = Math.max(...images.map((i) => i.key.length + 2));
43
+ const lines = images.map((img) => {
44
+ const keyLiteral = `"${img.key}",`;
45
+ const padded = keyLiteral.padEnd(keyColumn + 1, " ");
46
+ return ` configureTestImage(${padded} require("${img.relativePath}"));`;
47
+ });
48
+ return header + lines.join("\n") + "\n}\n";
49
+ }
50
+ export async function syncCameraRegistry(projectDir) {
51
+ const dir = join(projectDir, CAMERA_IMAGES_SUBPATH);
52
+ const registryPath = join(dir, REGISTRY_BASENAME);
53
+ let entries;
54
+ try {
55
+ const s = await stat(dir);
56
+ if (!s.isDirectory()) {
57
+ return {
58
+ registered: [],
59
+ registryPath,
60
+ writtenOrUnchanged: "missing-dir",
61
+ };
62
+ }
63
+ entries = await readdir(dir);
64
+ }
65
+ catch {
66
+ return {
67
+ registered: [],
68
+ registryPath,
69
+ writtenOrUnchanged: "missing-dir",
70
+ };
71
+ }
72
+ const byStem = new Map();
73
+ for (const name of entries) {
74
+ if (name === REGISTRY_BASENAME)
75
+ continue;
76
+ if (name.startsWith("_") || name.startsWith("."))
77
+ continue;
78
+ if (name === "README.md")
79
+ continue;
80
+ const split = splitImageName(name);
81
+ if (!split)
82
+ continue;
83
+ const candidate = {
84
+ key: split.stem,
85
+ relativePath: `./${name}`,
86
+ ext: split.ext,
87
+ };
88
+ const existing = byStem.get(split.stem);
89
+ if (!existing) {
90
+ byStem.set(split.stem, candidate);
91
+ continue;
92
+ }
93
+ if (extPriority(candidate.ext) < extPriority(existing.ext)) {
94
+ byStem.set(split.stem, candidate);
95
+ }
96
+ }
97
+ const registered = Array.from(byStem.values()).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
98
+ const nextSource = buildRegistrySource(registered);
99
+ let current = null;
100
+ try {
101
+ current = await readFile(registryPath, "utf-8");
102
+ }
103
+ catch {
104
+ current = null;
105
+ }
106
+ if (current === nextSource) {
107
+ return { registered, registryPath, writtenOrUnchanged: "unchanged" };
108
+ }
109
+ await mkdir(dir, { recursive: true });
110
+ await writeFile(registryPath, nextSource, "utf-8");
111
+ return { registered, registryPath, writtenOrUnchanged: "written" };
112
+ }
113
+ export function relativeRegistryPath(projectDir, registryPath) {
114
+ const rel = relative(projectDir, registryPath);
115
+ return rel.length > 0 ? rel : registryPath;
116
+ }
117
+ export { CAMERA_IMAGES_SUBPATH, REGISTRY_BASENAME };
package/dist/cli.d.ts CHANGED
@@ -1,9 +1,2 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * @ceraph/react-native-mcp CLI entry point.
4
- *
5
- * Usage:
6
- * npx @ceraph/react-native-mcp init — set up MCP config and error hook
7
- * npx @ceraph/react-native-mcp — start the MCP server (stdio transport)
8
- */
9
2
  export {};
package/dist/cli.js CHANGED
@@ -1,14 +1,46 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * @ceraph/react-native-mcp CLI entry point.
4
- *
5
- * Usage:
6
- * npx @ceraph/react-native-mcp init — set up MCP config and error hook
7
- * npx @ceraph/react-native-mcp — start the MCP server (stdio transport)
8
- */
9
- const command = process.argv[2];
2
+ const argv = process.argv.slice(2);
3
+ const command = argv[0];
4
+ const flags = new Set(argv.slice(1));
5
+ const HELP_TEXT = `@ceraph/react-native-mcp
6
+
7
+ USAGE
8
+ npx @ceraph/react-native-mcp <command> [flags]
9
+
10
+ COMMANDS
11
+ init Wire up MCP clients, hooks, AST replacements, and preflight.
12
+ uninstall Reverse the mutations init applied.
13
+ (no command) Start the MCP server over stdio.
14
+
15
+ FLAGS
16
+ --agent Pass when an AI tool is driving the install — defers
17
+ interactive camera setup to the agent via a single
18
+ CERAPH_INIT_AGENT_HINT={...} JSON line emitted on
19
+ stdout at completion. Equivalent to CERAPH_AGENT_MODE=1.
20
+ --help, -h Show this help text.
21
+ `;
22
+ if (command === "--help" || command === "-h" || command === "help") {
23
+ process.stdout.write(HELP_TEXT);
24
+ process.exit(0);
25
+ }
10
26
  if (command === "init") {
11
- await import("./init.js");
27
+ const agentMode = flags.has("--agent") || process.env.CERAPH_AGENT_MODE === "1";
28
+ const { runInit } = await import("./init.js");
29
+ await runInit({ agentMode }).catch((err) => {
30
+ console.error("Init failed:", err);
31
+ process.exit(1);
32
+ });
33
+ }
34
+ else if (command === "uninstall") {
35
+ const { runUninstallCli } = await import("./uninstall/cli-runner.js");
36
+ try {
37
+ const res = await runUninstallCli({ argv: process.argv.slice(3) });
38
+ process.exit(res.exitCode);
39
+ }
40
+ catch (err) {
41
+ console.error("Uninstall failed:", err);
42
+ process.exit(1);
43
+ }
12
44
  }
13
45
  else {
14
46
  await import("./index.js");
@@ -0,0 +1,30 @@
1
+ import type { ScreenManager } from "./screen.js";
2
+ export type LockState = "unlocked" | "locked" | "screen-off" | "unknown";
3
+ export interface AwakeResult {
4
+ awake: boolean;
5
+ lockState: LockState;
6
+ remediation?: string;
7
+ details?: Record<string, unknown>;
8
+ }
9
+ export interface HeartbeatHandle {
10
+ stop: () => void;
11
+ intervalMs: number;
12
+ }
13
+ export declare class DeviceAutonomy {
14
+ private screen;
15
+ constructor(screen: ScreenManager);
16
+ ensureAwake(): Promise<AwakeResult>;
17
+ keepAliveHeartbeat(opts?: {
18
+ intervalMs?: number;
19
+ }): HeartbeatHandle;
20
+ getLockState(): Promise<LockState>;
21
+ private hasCoverSheet;
22
+ setOrientation(orientation: "portrait" | "landscape"): Promise<{
23
+ success: boolean;
24
+ error?: string;
25
+ }>;
26
+ disableAutoLock(): {
27
+ success: false;
28
+ message: string;
29
+ };
30
+ }
@@ -0,0 +1,117 @@
1
+ export class DeviceAutonomy {
2
+ screen;
3
+ constructor(screen) {
4
+ this.screen = screen;
5
+ }
6
+ async ensureAwake() {
7
+ const reachable = await this.screen.isAvailable();
8
+ if (!reachable) {
9
+ return {
10
+ awake: false,
11
+ lockState: "unknown",
12
+ remediation: "WebDriverAgent is not reachable at localhost:8100. " +
13
+ "Start WDA on your device (Xcode → WebDriverAgentRunner) and try again.",
14
+ };
15
+ }
16
+ const state = await this.getLockState();
17
+ if (state === "unlocked") {
18
+ return { awake: true, lockState: "unlocked" };
19
+ }
20
+ if (state === "locked" || state === "screen-off") {
21
+ const unlock = await this.screen.unlock();
22
+ if (unlock.success) {
23
+ const after = await this.getLockState();
24
+ if (after === "unlocked") {
25
+ return { awake: true, lockState: "unlocked" };
26
+ }
27
+ }
28
+ await this.screen.pressKey("home").catch(() => undefined);
29
+ const after = await this.getLockState();
30
+ if (after === "unlocked") {
31
+ return { awake: true, lockState: "unlocked" };
32
+ }
33
+ return {
34
+ awake: false,
35
+ lockState: after,
36
+ remediation: "Device is locked. Unlock it manually once, then set " +
37
+ "Settings → Display & Brightness → Auto-Lock → Never so it " +
38
+ "stays awake while flows run. WDA can't bypass Face ID or a passcode.",
39
+ };
40
+ }
41
+ return {
42
+ awake: false,
43
+ lockState: state,
44
+ remediation: "Unable to determine device lock state. Make sure the device is " +
45
+ "unlocked and the screen is on, then re-run the flow.",
46
+ };
47
+ }
48
+ keepAliveHeartbeat(opts = {}) {
49
+ const intervalMs = opts.intervalMs ?? 1500;
50
+ const timer = setInterval(() => {
51
+ this.screen.pingStatus().catch(() => undefined);
52
+ }, intervalMs);
53
+ if (typeof timer.unref === "function")
54
+ timer.unref();
55
+ let stopped = false;
56
+ return {
57
+ intervalMs,
58
+ stop: () => {
59
+ if (stopped)
60
+ return;
61
+ stopped = true;
62
+ clearInterval(timer);
63
+ },
64
+ };
65
+ }
66
+ async getLockState() {
67
+ const locked = await this.screen.isLocked();
68
+ if (locked === true)
69
+ return "locked";
70
+ if (locked === false) {
71
+ const cover = await this.hasCoverSheet();
72
+ return cover ? "locked" : "unlocked";
73
+ }
74
+ const cover = await this.hasCoverSheet();
75
+ if (cover === null)
76
+ return "unknown";
77
+ return cover ? "locked" : "unlocked";
78
+ }
79
+ async hasCoverSheet() {
80
+ const src = await this.screen.getSource();
81
+ if (!src.success || !src.source)
82
+ return null;
83
+ const needles = ["sbcoversheet", "lock screen", "lockscreen"];
84
+ let found = false;
85
+ const visit = (e) => {
86
+ if (found)
87
+ return;
88
+ const haystack = [e.name, e.label, e.type]
89
+ .filter(Boolean)
90
+ .join(" ")
91
+ .toLowerCase();
92
+ if (needles.some((n) => haystack.includes(n))) {
93
+ found = true;
94
+ return;
95
+ }
96
+ const children = e.children;
97
+ if (Array.isArray(children)) {
98
+ for (const c of children)
99
+ visit(c);
100
+ }
101
+ };
102
+ visit(src.source);
103
+ return found;
104
+ }
105
+ async setOrientation(orientation) {
106
+ return this.screen.setOrientation(orientation);
107
+ }
108
+ disableAutoLock() {
109
+ return {
110
+ success: false,
111
+ message: "iOS does not expose an API for disabling Auto-Lock. Open " +
112
+ "Settings → Display & Brightness → Auto-Lock → Never on the device " +
113
+ "before running long flows. The flow runner keeps WDA's session " +
114
+ "warm with a heartbeat, but it cannot prevent Auto-Lock by itself.",
115
+ };
116
+ }
117
+ }