@ceraph/react-native-mcp 0.2.2 → 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,194 @@
1
+ import { readFile, unlink, access } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { writeFileAtomic } from "../utils/atomic-write.js";
5
+ export const JSON_CLIENT_PATHS = [
6
+ ".mcp.json",
7
+ join(".cursor", "mcp.json"),
8
+ join(".vscode", "mcp.json"),
9
+ ];
10
+ export const TOML_CLIENT_PATHS = [join(".codex", "config.toml")];
11
+ export const USER_GLOBAL_MCP_CLIENTS = [
12
+ {
13
+ label: "Windsurf",
14
+ dir: [".codeium", "windsurf"],
15
+ file: [".codeium", "windsurf", "mcp_config.json"],
16
+ },
17
+ {
18
+ label: "Antigravity",
19
+ dir: [".gemini", "antigravity"],
20
+ file: [".gemini", "antigravity", "mcp_config.json"],
21
+ },
22
+ ];
23
+ export function userGlobalMcpClientPaths(home) {
24
+ const h = home ?? homedir();
25
+ return USER_GLOBAL_MCP_CLIENTS.map((c) => ({
26
+ label: c.label,
27
+ path: join(h, ...c.file),
28
+ }));
29
+ }
30
+ export const SERVER_KEYS = ["mobile-mcp", "react-native-mcp"];
31
+ async function defaultExists(p) {
32
+ try {
33
+ await access(p);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ export async function revertMcpClients(projectDir, deps = {}) {
41
+ const read = deps.readFile ?? ((p) => readFile(p, "utf-8"));
42
+ const write = deps.writeFile ?? writeFileAtomic;
43
+ const unlinkFn = deps.unlink ?? ((p) => unlink(p));
44
+ const exists = deps.fileExists ?? defaultExists;
45
+ const files = [];
46
+ for (const rel of JSON_CLIENT_PATHS) {
47
+ const abs = join(projectDir, rel);
48
+ if (!(await exists(abs))) {
49
+ files.push({ path: abs, status: "not-found" });
50
+ continue;
51
+ }
52
+ files.push(await revertJsonClient(abs, { read, write, unlink: unlinkFn }));
53
+ }
54
+ for (const rel of TOML_CLIENT_PATHS) {
55
+ const abs = join(projectDir, rel);
56
+ if (!(await exists(abs))) {
57
+ files.push({ path: abs, status: "not-found" });
58
+ continue;
59
+ }
60
+ files.push(await revertTomlClient(abs, { read, write, unlink: unlinkFn }));
61
+ }
62
+ return { files };
63
+ }
64
+ async function revertJsonClient(abs, io) {
65
+ const raw = await io.read(abs);
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(raw);
69
+ }
70
+ catch {
71
+ return { path: abs, status: "malformed" };
72
+ }
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
74
+ return { path: abs, status: "malformed" };
75
+ }
76
+ const obj = parsed;
77
+ const servers = obj.mcpServers;
78
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
79
+ return { path: abs, status: "no-changes" };
80
+ }
81
+ const map = servers;
82
+ const removedKeys = [];
83
+ for (const key of SERVER_KEYS) {
84
+ if (key in map) {
85
+ delete map[key];
86
+ removedKeys.push(key);
87
+ }
88
+ }
89
+ if (removedKeys.length === 0) {
90
+ return { path: abs, status: "no-changes" };
91
+ }
92
+ const otherTopKeys = Object.keys(obj).filter((k) => k !== "mcpServers");
93
+ const mcpEmpty = Object.keys(map).length === 0;
94
+ if (mcpEmpty && otherTopKeys.length === 0) {
95
+ await io.unlink(abs);
96
+ return { path: abs, status: "deleted-file", removedKeys };
97
+ }
98
+ const indentMatch = raw.match(/\n(\s+)"/);
99
+ const indent = indentMatch?.[1] ?? " ";
100
+ const hadTrailingNewline = raw.endsWith("\n");
101
+ const out = JSON.stringify(obj, null, indent);
102
+ await io.write(abs, hadTrailingNewline ? out + "\n" : out);
103
+ return { path: abs, status: "removed-entries", removedKeys };
104
+ }
105
+ function stripTomlSections(raw, sectionHeadings) {
106
+ const lines = raw.split(/\r?\n/);
107
+ const hadTrailingNewline = raw.endsWith("\n");
108
+ if (hadTrailingNewline && lines.length > 0 && lines[lines.length - 1] === "") {
109
+ lines.pop();
110
+ }
111
+ const out = [];
112
+ const removedKeys = [];
113
+ let inDropSection = false;
114
+ let justExitedDrop = false;
115
+ for (let i = 0; i < lines.length; i++) {
116
+ const line = lines[i];
117
+ const trimmed = line.trim();
118
+ const isHeading = trimmed.startsWith("[") && trimmed.endsWith("]");
119
+ if (isHeading) {
120
+ const heading = trimmed.slice(1, -1).trim();
121
+ const isDropHeading = sectionHeadings.has(heading);
122
+ if (isDropHeading) {
123
+ if (out.length > 0 && out[out.length - 1] === "") {
124
+ out.pop();
125
+ }
126
+ const key = heading.startsWith("mcp_servers.")
127
+ ? heading.slice("mcp_servers.".length)
128
+ : heading;
129
+ removedKeys.push(key);
130
+ inDropSection = true;
131
+ justExitedDrop = false;
132
+ continue;
133
+ }
134
+ if ((inDropSection || justExitedDrop) && out.length > 0 && out[out.length - 1] !== "") {
135
+ out.push("");
136
+ }
137
+ inDropSection = false;
138
+ justExitedDrop = false;
139
+ out.push(line);
140
+ continue;
141
+ }
142
+ if (inDropSection) {
143
+ justExitedDrop = true;
144
+ continue;
145
+ }
146
+ if (justExitedDrop) {
147
+ if (trimmed === "")
148
+ continue;
149
+ if (out.length > 0 && out[out.length - 1] !== "") {
150
+ out.push("");
151
+ }
152
+ justExitedDrop = false;
153
+ out.push(line);
154
+ continue;
155
+ }
156
+ out.push(line);
157
+ }
158
+ while (out.length > 0 && out[out.length - 1] === "") {
159
+ out.pop();
160
+ }
161
+ const joined = out.join("\n");
162
+ const next = hadTrailingNewline && joined.length > 0 ? joined + "\n" : joined;
163
+ return { next, removedKeys };
164
+ }
165
+ async function revertTomlClient(abs, io) {
166
+ const raw = await io.read(abs);
167
+ const headings = new Set(SERVER_KEYS.map((k) => `mcp_servers.${k}`));
168
+ const { next, removedKeys } = stripTomlSections(raw, headings);
169
+ if (removedKeys.length === 0) {
170
+ return { path: abs, status: "no-changes" };
171
+ }
172
+ if (next.trim().length === 0) {
173
+ await io.unlink(abs);
174
+ return { path: abs, status: "deleted-file", removedKeys };
175
+ }
176
+ await io.write(abs, next);
177
+ return { path: abs, status: "removed-entries", removedKeys };
178
+ }
179
+ export { stripTomlSections };
180
+ export async function revertUserGlobalMcpClients(deps = {}) {
181
+ const read = deps.readFile ?? ((p) => readFile(p, "utf-8"));
182
+ const write = deps.writeFile ?? writeFileAtomic;
183
+ const unlinkFn = deps.unlink ?? ((p) => unlink(p));
184
+ const exists = deps.fileExists ?? defaultExists;
185
+ const files = [];
186
+ for (const { path } of userGlobalMcpClientPaths(deps.home)) {
187
+ if (!(await exists(path))) {
188
+ files.push({ path, status: "not-found" });
189
+ continue;
190
+ }
191
+ files.push(await revertJsonClient(path, { read, write, unlink: unlinkFn }));
192
+ }
193
+ return { files };
194
+ }
@@ -0,0 +1,34 @@
1
+ export type PackageManager = "pnpm" | "yarn" | "bun" | "npm";
2
+ export declare const LOCKFILES: ReadonlyArray<{
3
+ file: string;
4
+ pm: PackageManager;
5
+ }>;
6
+ export declare const CERAPH_PKG = "@ceraph/react-native-mcp";
7
+ export declare const APPIUM_PKG = "appium-webdriveragent";
8
+ export declare function detectPackageManager(projectDir: string): Promise<PackageManager>;
9
+ export declare function removeArgv(pm: PackageManager, pkgs: string[]): {
10
+ bin: string;
11
+ args: string[];
12
+ };
13
+ export declare function listInstalledPackages(projectDir: string, pkgs: string[]): Promise<string[]>;
14
+ export type RevertPackageStatus = "removed" | "nothing-to-remove" | "command-failed";
15
+ export interface RevertPackageResult {
16
+ status: RevertPackageStatus;
17
+ pm: PackageManager;
18
+ removedPackages: string[];
19
+ command?: {
20
+ bin: string;
21
+ args: string[];
22
+ };
23
+ exitCode?: number;
24
+ }
25
+ export interface RevertPackageDeps {
26
+ spawnRemove?: (input: {
27
+ bin: string;
28
+ args: string[];
29
+ cwd: string;
30
+ }) => Promise<{
31
+ exitCode: number;
32
+ }>;
33
+ }
34
+ export declare function revertPackage(projectDir: string, deps?: RevertPackageDeps): Promise<RevertPackageResult>;
@@ -0,0 +1,98 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile, access } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ export const LOCKFILES = [
5
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
6
+ { file: "yarn.lock", pm: "yarn" },
7
+ { file: "bun.lockb", pm: "bun" },
8
+ { file: "bun.lock", pm: "bun" },
9
+ { file: "package-lock.json", pm: "npm" },
10
+ ];
11
+ export const CERAPH_PKG = "@ceraph/react-native-mcp";
12
+ export const APPIUM_PKG = "appium-webdriveragent";
13
+ async function fileExists(path) {
14
+ try {
15
+ await access(path);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export async function detectPackageManager(projectDir) {
23
+ for (const { file, pm } of LOCKFILES) {
24
+ if (await fileExists(join(projectDir, file))) {
25
+ return pm;
26
+ }
27
+ }
28
+ return "npm";
29
+ }
30
+ export function removeArgv(pm, pkgs) {
31
+ switch (pm) {
32
+ case "pnpm":
33
+ return { bin: "pnpm", args: ["remove", ...pkgs] };
34
+ case "yarn":
35
+ return { bin: "yarn", args: ["remove", ...pkgs] };
36
+ case "bun":
37
+ return { bin: "bun", args: ["remove", ...pkgs] };
38
+ case "npm":
39
+ return { bin: "npm", args: ["uninstall", ...pkgs] };
40
+ }
41
+ }
42
+ export async function listInstalledPackages(projectDir, pkgs) {
43
+ let raw;
44
+ try {
45
+ raw = await readFile(join(projectDir, "package.json"), "utf-8");
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ if (!parsed || typeof parsed !== "object")
58
+ return [];
59
+ const dep = parsed.dependencies ?? {};
60
+ const dev = parsed.devDependencies ?? {};
61
+ return pkgs.filter((p) => Boolean(dep[p] || dev[p]));
62
+ }
63
+ export async function revertPackage(projectDir, deps = {}) {
64
+ const pm = await detectPackageManager(projectDir);
65
+ const candidates = [CERAPH_PKG];
66
+ const listed = await listInstalledPackages(projectDir, candidates);
67
+ if (listed.length === 0) {
68
+ return { status: "nothing-to-remove", pm, removedPackages: [] };
69
+ }
70
+ const argv = removeArgv(pm, listed);
71
+ const runner = deps.spawnRemove ?? defaultSpawnRemove;
72
+ const result = await runner({ bin: argv.bin, args: argv.args, cwd: projectDir });
73
+ if (result.exitCode !== 0) {
74
+ return {
75
+ status: "command-failed",
76
+ pm,
77
+ removedPackages: [],
78
+ command: argv,
79
+ exitCode: result.exitCode,
80
+ };
81
+ }
82
+ return {
83
+ status: "removed",
84
+ pm,
85
+ removedPackages: listed,
86
+ command: argv,
87
+ };
88
+ }
89
+ function defaultSpawnRemove(input) {
90
+ return new Promise((resolve) => {
91
+ const child = spawn(input.bin, input.args, {
92
+ cwd: input.cwd,
93
+ stdio: "inherit",
94
+ });
95
+ child.on("error", () => resolve({ exitCode: 127 }));
96
+ child.on("exit", (code) => resolve({ exitCode: code ?? 1 }));
97
+ });
98
+ }
@@ -0,0 +1,36 @@
1
+ export declare const CERAPH_SCHEME = "ceraph";
2
+ export type RevertSchemeResult = {
3
+ status: "reverted";
4
+ path: string;
5
+ previousScheme: string | string[];
6
+ nextScheme: string | string[] | undefined;
7
+ } | {
8
+ status: "already-reverted";
9
+ path: string;
10
+ } | {
11
+ status: "manual";
12
+ kind: "expo-dynamic" | "bare-rn";
13
+ path?: string;
14
+ remediation: string;
15
+ } | {
16
+ status: "no-rn-project";
17
+ };
18
+ export interface RevertSchemeDeps {
19
+ readFile?: (p: string) => Promise<string>;
20
+ writeFile?: (p: string, c: string) => Promise<void>;
21
+ fileExists?: (p: string) => Promise<boolean>;
22
+ }
23
+ interface ParsedAppJson {
24
+ expo?: {
25
+ scheme?: string | string[] | unknown;
26
+ [k: string]: unknown;
27
+ };
28
+ [k: string]: unknown;
29
+ }
30
+ export declare function stripCeraphSchemeFromAppJson(parsed: ParsedAppJson): {
31
+ changed: boolean;
32
+ previousScheme: string | string[] | undefined;
33
+ nextScheme: string | string[] | undefined;
34
+ };
35
+ export declare function revertCeraphUrlScheme(projectDir: string, deps?: RevertSchemeDeps): Promise<RevertSchemeResult>;
36
+ export {};
@@ -0,0 +1,139 @@
1
+ import { readFile, writeFile, access } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export const CERAPH_SCHEME = "ceraph";
4
+ async function defaultExists(p) {
5
+ try {
6
+ await access(p);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export function stripCeraphSchemeFromAppJson(parsed) {
14
+ if (!parsed.expo || typeof parsed.expo !== "object") {
15
+ return { changed: false, previousScheme: undefined, nextScheme: undefined };
16
+ }
17
+ const expo = parsed.expo;
18
+ const current = expo.scheme;
19
+ if (typeof current === "string") {
20
+ if (current === CERAPH_SCHEME) {
21
+ delete expo.scheme;
22
+ return {
23
+ changed: true,
24
+ previousScheme: current,
25
+ nextScheme: undefined,
26
+ };
27
+ }
28
+ return { changed: false, previousScheme: current, nextScheme: current };
29
+ }
30
+ if (Array.isArray(current)) {
31
+ const hadCeraph = current.includes(CERAPH_SCHEME);
32
+ if (!hadCeraph) {
33
+ return {
34
+ changed: false,
35
+ previousScheme: [...current],
36
+ nextScheme: [...current],
37
+ };
38
+ }
39
+ const remaining = current.filter((s) => s !== CERAPH_SCHEME);
40
+ const previous = [...current];
41
+ if (remaining.length === 0) {
42
+ delete expo.scheme;
43
+ return {
44
+ changed: true,
45
+ previousScheme: previous,
46
+ nextScheme: undefined,
47
+ };
48
+ }
49
+ if (remaining.length === 1) {
50
+ expo.scheme = remaining[0];
51
+ return {
52
+ changed: true,
53
+ previousScheme: previous,
54
+ nextScheme: remaining[0],
55
+ };
56
+ }
57
+ expo.scheme = remaining;
58
+ return {
59
+ changed: true,
60
+ previousScheme: previous,
61
+ nextScheme: remaining,
62
+ };
63
+ }
64
+ return { changed: false, previousScheme: undefined, nextScheme: undefined };
65
+ }
66
+ function parseAppJson(raw) {
67
+ try {
68
+ const parsed = JSON.parse(raw);
69
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
70
+ return null;
71
+ }
72
+ return parsed;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ function serialiseAppJson(parsed, original) {
79
+ const indentMatch = original.match(/\n(\s+)"/);
80
+ const indent = indentMatch?.[1] ?? " ";
81
+ const hadTrailingNewline = original.endsWith("\n");
82
+ const output = JSON.stringify(parsed, null, indent);
83
+ return hadTrailingNewline ? output + "\n" : output;
84
+ }
85
+ const BARE_RN_REMEDIATION = "Remove the ceraph scheme from ios/<App>/Info.plist CFBundleURLTypes and android/app/src/main/AndroidManifest.xml. " +
86
+ "Both files were never written by init — they require manual edits because they are inside the customer's native project tree.";
87
+ function dynamicConfigRemediation(path) {
88
+ return (`Your project uses ${path} (dynamic Expo config). The init walkthrough ` +
89
+ "never edited this file automatically; if you (or a teammate) added `ceraph` " +
90
+ "to its `scheme` value, remove that entry by hand. After editing, run " +
91
+ "`expo prebuild --clean` or restart the dev client.");
92
+ }
93
+ export async function revertCeraphUrlScheme(projectDir, deps = {}) {
94
+ const exists = deps.fileExists ?? defaultExists;
95
+ const read = deps.readFile ?? ((p) => readFile(p, "utf-8"));
96
+ const write = deps.writeFile ?? ((p, c) => writeFile(p, c, "utf-8"));
97
+ const appJsonPath = join(projectDir, "app.json");
98
+ const appConfigJs = join(projectDir, "app.config.js");
99
+ const appConfigTs = join(projectDir, "app.config.ts");
100
+ const iosDir = join(projectDir, "ios");
101
+ const androidDir = join(projectDir, "android");
102
+ const hasAppConfigJs = await exists(appConfigJs);
103
+ const hasAppConfigTs = await exists(appConfigTs);
104
+ if (hasAppConfigJs || hasAppConfigTs) {
105
+ const path = hasAppConfigTs ? appConfigTs : appConfigJs;
106
+ return {
107
+ status: "manual",
108
+ kind: "expo-dynamic",
109
+ path,
110
+ remediation: dynamicConfigRemediation(path),
111
+ };
112
+ }
113
+ if (await exists(appJsonPath)) {
114
+ const raw = await read(appJsonPath);
115
+ const parsed = parseAppJson(raw);
116
+ if (!parsed) {
117
+ return { status: "already-reverted", path: appJsonPath };
118
+ }
119
+ const { changed, previousScheme, nextScheme } = stripCeraphSchemeFromAppJson(parsed);
120
+ if (!changed) {
121
+ return { status: "already-reverted", path: appJsonPath };
122
+ }
123
+ await write(appJsonPath, serialiseAppJson(parsed, raw));
124
+ return {
125
+ status: "reverted",
126
+ path: appJsonPath,
127
+ previousScheme: previousScheme,
128
+ nextScheme: nextScheme,
129
+ };
130
+ }
131
+ if ((await exists(iosDir)) || (await exists(androidDir))) {
132
+ return {
133
+ status: "manual",
134
+ kind: "bare-rn",
135
+ remediation: BARE_RN_REMEDIATION,
136
+ };
137
+ }
138
+ return { status: "no-rn-project" };
139
+ }
@@ -0,0 +1,31 @@
1
+ export interface ShellProfileTarget {
2
+ path: string;
3
+ shell: "zsh" | "bash";
4
+ }
5
+ export interface PickProfileDeps {
6
+ env?: NodeJS.ProcessEnv;
7
+ home?: string;
8
+ }
9
+ export declare function pickShellProfile(deps?: PickProfileDeps): ShellProfileTarget;
10
+ export type RevertSignalHostEnvResult = {
11
+ status: "reverted";
12
+ path: string;
13
+ shell: "zsh" | "bash";
14
+ removedIp: string | null;
15
+ } | {
16
+ status: "already-reverted";
17
+ path: string;
18
+ shell: "zsh" | "bash";
19
+ } | {
20
+ status: "no-profile";
21
+ path: string;
22
+ shell: "zsh" | "bash";
23
+ };
24
+ export interface RevertSignalHostEnvDeps {
25
+ readFile?: (p: string) => Promise<string>;
26
+ writeFile?: (p: string, c: string) => Promise<void>;
27
+ target?: ShellProfileTarget;
28
+ env?: NodeJS.ProcessEnv;
29
+ home?: string;
30
+ }
31
+ export declare function revertSignalHostEnv(deps?: RevertSignalHostEnvDeps): Promise<RevertSignalHostEnvResult>;
@@ -0,0 +1,61 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } 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
+ export function pickShellProfile(deps = {}) {
7
+ const env = deps.env ?? process.env;
8
+ const home = deps.home ?? homedir();
9
+ const shellPath = env.SHELL ?? "";
10
+ if (shellPath.endsWith("/bash")) {
11
+ return { path: join(home, ".bashrc"), shell: "bash" };
12
+ }
13
+ return { path: join(home, ".zshrc"), shell: "zsh" };
14
+ }
15
+ export async function revertSignalHostEnv(deps = {}) {
16
+ const target = deps.target ?? pickShellProfile({ env: deps.env, home: deps.home });
17
+ const read = deps.readFile ?? ((p) => readFile(p, "utf-8"));
18
+ const write = deps.writeFile ?? ((p, c) => writeFile(p, c, "utf-8"));
19
+ let body;
20
+ try {
21
+ body = await read(target.path);
22
+ }
23
+ catch {
24
+ return { status: "no-profile", path: target.path, shell: target.shell };
25
+ }
26
+ const headIdx = body.indexOf(SENTINEL_HEAD);
27
+ if (headIdx === -1) {
28
+ return {
29
+ status: "already-reverted",
30
+ path: target.path,
31
+ shell: target.shell,
32
+ };
33
+ }
34
+ const tailIdx = body.indexOf(SENTINEL_TAIL, headIdx);
35
+ if (tailIdx === -1) {
36
+ return {
37
+ status: "already-reverted",
38
+ path: target.path,
39
+ shell: target.shell,
40
+ };
41
+ }
42
+ const afterTail = tailIdx + SENTINEL_TAIL.length;
43
+ const end = body[afterTail] === "\n" ? afterTail + 1 : afterTail;
44
+ const between = body.slice(headIdx, end);
45
+ const exportMatch = between.match(/export\s+CERAPH_SIGNAL_HOST=(\S+)/);
46
+ const removedIp = exportMatch ? exportMatch[1] : null;
47
+ let start = headIdx;
48
+ if (start >= 1 && body[start - 1] === "\n") {
49
+ if (start >= 2 && body[start - 2] === "\n") {
50
+ start -= 1;
51
+ }
52
+ }
53
+ const next = body.slice(0, start) + body.slice(end);
54
+ await write(target.path, next);
55
+ return {
56
+ status: "reverted",
57
+ path: target.path,
58
+ shell: target.shell,
59
+ removedIp,
60
+ };
61
+ }
@@ -0,0 +1,80 @@
1
+ import { type MonorepoDetectionResult } from "../init/monorepo.js";
2
+ import { type RevertGitignoreResult } from "./revert-gitignore.js";
3
+ import { type RevertSignalHostEnvResult } from "./revert-signal-host-env.js";
4
+ import { type RevertCeraphDirResult } from "./revert-ceraph-dir.js";
5
+ import { type RevertAuthResult } from "./revert-auth.js";
6
+ import { type RevertSchemeResult } from "./revert-scheme.js";
7
+ import { type RevertMcpClientsResult, type RevertUserGlobalMcpClientsResult } from "./revert-mcp-clients.js";
8
+ import { type RevertClaudeHooksResult } from "./revert-claude-hooks.js";
9
+ import { type RevertBootResult, type RevertBootTarget } from "./revert-boot.js";
10
+ import { type RevertCameraResult } from "./revert-camera.js";
11
+ import { type RevertPackageResult } from "./revert-package.js";
12
+ export type UninstallStepName = "monorepo" | "camera" | "boot" | "scheme" | "mcp-clients" | "claude-hooks" | "gitignore" | "signal-host" | "ceraph-dir" | "auth" | "package";
13
+ export type UninstallStepStatus = "reverted" | "skipped" | "already-reverted" | "warning" | "manual" | "error";
14
+ export interface UninstallStep {
15
+ name: UninstallStepName;
16
+ status: UninstallStepStatus;
17
+ details?: Record<string, unknown>;
18
+ remediation?: string;
19
+ }
20
+ export interface UninstallWalkthroughSummary {
21
+ reverted: number;
22
+ skipped: number;
23
+ warnings: string[];
24
+ manualSteps: string[];
25
+ }
26
+ export interface UninstallWalkthroughResult {
27
+ workingDir: string;
28
+ steps: UninstallStep[];
29
+ summary: UninstallWalkthroughSummary;
30
+ }
31
+ export interface UninstallWalkthroughOpts {
32
+ projectDir: string;
33
+ purgeImages?: boolean;
34
+ global?: boolean;
35
+ dryRun?: boolean;
36
+ nonInteractive?: boolean;
37
+ onStep?: (step: UninstallStep) => void;
38
+ spawnRemove?: (input: {
39
+ bin: string;
40
+ args: string[];
41
+ cwd: string;
42
+ }) => Promise<{
43
+ exitCode: number;
44
+ }>;
45
+ detectMonorepo?: (projectDir: string) => Promise<MonorepoDetectionResult>;
46
+ homeDir?: () => string;
47
+ revertCamera?: (projectDir: string) => Promise<RevertCameraResult>;
48
+ revertBoot?: (projectDir: string) => Promise<RevertBootResult>;
49
+ revertCeraphUrlScheme?: (projectDir: string) => Promise<RevertSchemeResult>;
50
+ revertMcpClients?: (projectDir: string) => Promise<RevertMcpClientsResult>;
51
+ revertUserGlobalMcpClients?: (deps: {
52
+ home?: string;
53
+ }) => Promise<RevertUserGlobalMcpClientsResult>;
54
+ revertClaudeHooks?: (input: {
55
+ projectDir: string;
56
+ }) => Promise<RevertClaudeHooksResult>;
57
+ revertGitignore?: (projectDir: string) => Promise<RevertGitignoreResult>;
58
+ revertSignalHostEnv?: (deps?: {
59
+ home?: string;
60
+ }) => Promise<RevertSignalHostEnvResult>;
61
+ revertCeraphDir?: (input: {
62
+ projectDir: string;
63
+ confirm: boolean;
64
+ }) => Promise<RevertCeraphDirResult>;
65
+ revertAuth?: (input: {
66
+ global: boolean;
67
+ home?: string;
68
+ }) => Promise<RevertAuthResult>;
69
+ revertPackage?: (projectDir: string, deps: {
70
+ spawnRemove?: (input: {
71
+ bin: string;
72
+ args: string[];
73
+ cwd: string;
74
+ }) => Promise<{
75
+ exitCode: number;
76
+ }>;
77
+ }) => Promise<RevertPackageResult>;
78
+ detectBootTarget?: (projectDir: string) => Promise<RevertBootTarget | null>;
79
+ }
80
+ export declare function runUninstallWalkthrough(opts: UninstallWalkthroughOpts): Promise<UninstallWalkthroughResult>;