@ceraph/react-native-mcp 0.2.2 → 0.3.2
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 +59 -5
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { confirm, print } from "../init/prompt.js";
|
|
5
|
+
import { runUninstallWalkthrough, } from "./walkthrough.js";
|
|
6
|
+
const USAGE = `Usage: npx @ceraph/react-native-mcp uninstall [flags]
|
|
7
|
+
|
|
8
|
+
Removes Ceraph from the current React Native / Expo project. By default:
|
|
9
|
+
- Reverts source-tree edits (CameraView, layout boot, ceraph URL scheme)
|
|
10
|
+
- Strips MCP client configs (.mcp.json, .cursor/, .vscode/, .codex/)
|
|
11
|
+
- Removes gitignore entries
|
|
12
|
+
- Removes CERAPH_SIGNAL_HOST from your shell rc
|
|
13
|
+
- Removes @ceraph/react-native-mcp from package.json
|
|
14
|
+
- LEAVES .ceraph/ (test images, design snapshots) in place
|
|
15
|
+
- LEAVES ~/.ceraph/auth.json in place (token is user-global)
|
|
16
|
+
|
|
17
|
+
Flags:
|
|
18
|
+
-y, --yes Skip confirmation prompts.
|
|
19
|
+
--purge-images Also delete .ceraph/ (camera images + snapshots).
|
|
20
|
+
--global Also delete ~/.ceraph/auth.json (logs you out of
|
|
21
|
+
every Ceraph install on this machine).
|
|
22
|
+
--dry-run Preview the revert without mutating anything.
|
|
23
|
+
--project-dir <p> Operate on this subpackage. Required when a
|
|
24
|
+
monorepo has more than one RN app.
|
|
25
|
+
-h, --help Print this help and exit.
|
|
26
|
+
`;
|
|
27
|
+
export async function runUninstallCli(opts) {
|
|
28
|
+
const flags = parseArgs(opts.argv);
|
|
29
|
+
if (flags.help) {
|
|
30
|
+
print(USAGE, opts);
|
|
31
|
+
return { exitCode: 0, walkthrough: null, parsedFlags: flags };
|
|
32
|
+
}
|
|
33
|
+
if (flags.unknown.length > 0) {
|
|
34
|
+
print(`Unknown flag(s): ${flags.unknown.join(", ")}\n\n${USAGE}`, opts);
|
|
35
|
+
return { exitCode: 1, walkthrough: null, parsedFlags: flags };
|
|
36
|
+
}
|
|
37
|
+
const projectDir = flags.projectDir ?? opts.cwd ?? process.cwd();
|
|
38
|
+
if (flags.projectDir != null) {
|
|
39
|
+
const validation = await validateProjectDir(flags.projectDir);
|
|
40
|
+
if (validation !== null) {
|
|
41
|
+
print(`${validation}\n`, opts);
|
|
42
|
+
return { exitCode: 1, walkthrough: null, parsedFlags: flags };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let purgeImages = flags.purgeImages;
|
|
46
|
+
let global = flags.global;
|
|
47
|
+
if (!flags.yes && !flags.dryRun) {
|
|
48
|
+
print(`\n@ceraph/react-native-mcp uninstall\n`, opts);
|
|
49
|
+
print(` Project: ${projectDir}\n`, opts);
|
|
50
|
+
print(` Mode: live (no --dry-run)\n`, opts);
|
|
51
|
+
print(` Default-safe: leaves .ceraph/ and ~/.ceraph/auth.json untouched.\n\n`, opts);
|
|
52
|
+
const proceed = await confirm("Continue with uninstall?", {
|
|
53
|
+
...opts,
|
|
54
|
+
defaultYes: false,
|
|
55
|
+
});
|
|
56
|
+
if (!proceed) {
|
|
57
|
+
print(`\nAborted — no changes made.\n`, opts);
|
|
58
|
+
return { exitCode: 0, walkthrough: null, parsedFlags: flags };
|
|
59
|
+
}
|
|
60
|
+
if (!purgeImages) {
|
|
61
|
+
purgeImages = await confirm("Also purge camera images + snapshots at .ceraph/?", { ...opts, defaultYes: false });
|
|
62
|
+
}
|
|
63
|
+
if (!global) {
|
|
64
|
+
global = await confirm("Also sign out (delete ~/.ceraph/auth.json)?", { ...opts, defaultYes: false });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (flags.dryRun) {
|
|
68
|
+
print(`\n@ceraph/react-native-mcp uninstall — dry-run preview\n`, opts);
|
|
69
|
+
print(` Project: ${projectDir}\n\n`, opts);
|
|
70
|
+
}
|
|
71
|
+
const walkthrough = await runUninstallWalkthrough({
|
|
72
|
+
projectDir,
|
|
73
|
+
purgeImages,
|
|
74
|
+
global,
|
|
75
|
+
dryRun: flags.dryRun,
|
|
76
|
+
nonInteractive: flags.yes,
|
|
77
|
+
spawnRemove: opts.spawnRemove ?? defaultSpawnRemove,
|
|
78
|
+
onStep: (step) => {
|
|
79
|
+
print(formatStepLine(step), opts);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
print(formatSummary(walkthrough), opts);
|
|
83
|
+
const hasError = walkthrough.steps.some((s) => s.status === "error" || s.status === "manual");
|
|
84
|
+
return {
|
|
85
|
+
exitCode: hasError ? 1 : 0,
|
|
86
|
+
walkthrough,
|
|
87
|
+
parsedFlags: { ...flags, purgeImages, global },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function formatStepLine(step) {
|
|
91
|
+
const icon = iconFor(step.status);
|
|
92
|
+
const summary = summaryFor(step);
|
|
93
|
+
return ` ${icon} ${step.name.padEnd(13)} ${summary}\n`;
|
|
94
|
+
}
|
|
95
|
+
function iconFor(status) {
|
|
96
|
+
switch (status) {
|
|
97
|
+
case "reverted":
|
|
98
|
+
return "[ok]";
|
|
99
|
+
case "already-reverted":
|
|
100
|
+
return "[--]";
|
|
101
|
+
case "skipped":
|
|
102
|
+
return "[--]";
|
|
103
|
+
case "warning":
|
|
104
|
+
return "[!]";
|
|
105
|
+
case "manual":
|
|
106
|
+
return "[!]";
|
|
107
|
+
case "error":
|
|
108
|
+
return "[X]";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function summaryFor(step) {
|
|
112
|
+
switch (step.status) {
|
|
113
|
+
case "reverted":
|
|
114
|
+
return `reverted${step.details?.dryRun ? " (preview)" : ""}`;
|
|
115
|
+
case "already-reverted":
|
|
116
|
+
return `nothing to do${step.details?.dryRun ? " (preview)" : ""}`;
|
|
117
|
+
case "skipped":
|
|
118
|
+
return `skipped — ${step.remediation ?? "gate closed"}`;
|
|
119
|
+
case "warning":
|
|
120
|
+
return `reverted with warning — ${step.remediation ?? ""}`;
|
|
121
|
+
case "manual":
|
|
122
|
+
return `manual action required — ${step.remediation ?? ""}`;
|
|
123
|
+
case "error":
|
|
124
|
+
return `error — ${step.remediation ?? step.details?.error ?? "see details"}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function formatSummary(result) {
|
|
128
|
+
const { reverted, skipped, warnings, manualSteps } = result.summary;
|
|
129
|
+
const lines = ["\nSummary:\n"];
|
|
130
|
+
lines.push(` Reverted: ${reverted}\n`);
|
|
131
|
+
lines.push(` Skipped/no-op: ${skipped}\n`);
|
|
132
|
+
lines.push(` Warnings: ${warnings.length}\n`);
|
|
133
|
+
lines.push(` Manual actions: ${manualSteps.length}\n`);
|
|
134
|
+
if (warnings.length > 0) {
|
|
135
|
+
lines.push(`\nWarnings:\n`);
|
|
136
|
+
for (const w of warnings)
|
|
137
|
+
lines.push(` - ${w}\n`);
|
|
138
|
+
}
|
|
139
|
+
if (manualSteps.length > 0) {
|
|
140
|
+
lines.push(`\nManual actions needed:\n`);
|
|
141
|
+
for (const m of manualSteps)
|
|
142
|
+
lines.push(` - ${m}\n`);
|
|
143
|
+
}
|
|
144
|
+
return lines.join("");
|
|
145
|
+
}
|
|
146
|
+
export function parseArgs(argv) {
|
|
147
|
+
const flags = {
|
|
148
|
+
yes: false,
|
|
149
|
+
purgeImages: false,
|
|
150
|
+
global: false,
|
|
151
|
+
dryRun: false,
|
|
152
|
+
projectDir: null,
|
|
153
|
+
help: false,
|
|
154
|
+
unknown: [],
|
|
155
|
+
};
|
|
156
|
+
for (let i = 0; i < argv.length; i++) {
|
|
157
|
+
const arg = argv[i];
|
|
158
|
+
switch (arg) {
|
|
159
|
+
case "-y":
|
|
160
|
+
case "--yes":
|
|
161
|
+
flags.yes = true;
|
|
162
|
+
break;
|
|
163
|
+
case "--purge-images":
|
|
164
|
+
flags.purgeImages = true;
|
|
165
|
+
break;
|
|
166
|
+
case "--global":
|
|
167
|
+
flags.global = true;
|
|
168
|
+
break;
|
|
169
|
+
case "--dry-run":
|
|
170
|
+
flags.dryRun = true;
|
|
171
|
+
break;
|
|
172
|
+
case "--project-dir": {
|
|
173
|
+
const next = argv[i + 1];
|
|
174
|
+
if (next == null) {
|
|
175
|
+
flags.unknown.push(`${arg} (missing value)`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
flags.projectDir = next;
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "-h":
|
|
184
|
+
case "--help":
|
|
185
|
+
flags.help = true;
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
if (arg !== undefined)
|
|
189
|
+
flags.unknown.push(arg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return flags;
|
|
193
|
+
}
|
|
194
|
+
async function validateProjectDir(path) {
|
|
195
|
+
let entryStat;
|
|
196
|
+
try {
|
|
197
|
+
entryStat = await stat(path);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return `Error: --project-dir does not exist: ${path}`;
|
|
201
|
+
}
|
|
202
|
+
if (!entryStat.isDirectory()) {
|
|
203
|
+
return `Error: --project-dir is not a directory: ${path}`;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await stat(join(path, "package.json"));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return (`Error: --project-dir does not look like an npm/RN project ` +
|
|
210
|
+
`(no package.json at ${join(path, "package.json")})`);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function defaultSpawnRemove(input) {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const child = spawn(input.bin, input.args, {
|
|
217
|
+
cwd: input.cwd,
|
|
218
|
+
stdio: "inherit",
|
|
219
|
+
});
|
|
220
|
+
child.on("error", () => resolve({ exitCode: 127 }));
|
|
221
|
+
child.on("exit", (code) => resolve({ exitCode: code ?? 1 }));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { detectMonorepoSubpackages } from "../init/monorepo.js";
|
|
2
|
+
import { type PackageManager } from "./revert-package.js";
|
|
3
|
+
export interface InstalledFootprint {
|
|
4
|
+
workingDir: string;
|
|
5
|
+
monorepoConflict: {
|
|
6
|
+
matches: Array<{
|
|
7
|
+
relPath: string;
|
|
8
|
+
absPath: string;
|
|
9
|
+
}>;
|
|
10
|
+
} | null;
|
|
11
|
+
packageInstalled: boolean;
|
|
12
|
+
packageManager: PackageManager;
|
|
13
|
+
appiumPresent: boolean;
|
|
14
|
+
gitignoreEntriesPresent: boolean;
|
|
15
|
+
gitignoreEntries: string[];
|
|
16
|
+
schemeRegistered: boolean | null;
|
|
17
|
+
bootTargetPath: string | null;
|
|
18
|
+
bootInjected: boolean | null;
|
|
19
|
+
ceraphCameraUsageCount: number;
|
|
20
|
+
mcpClientsConfigured: {
|
|
21
|
+
claudeCode: boolean;
|
|
22
|
+
cursor: boolean;
|
|
23
|
+
codex: boolean;
|
|
24
|
+
vscode: boolean;
|
|
25
|
+
windsurf: boolean;
|
|
26
|
+
antigravity: boolean;
|
|
27
|
+
};
|
|
28
|
+
claudeHooksPresent: {
|
|
29
|
+
errorScript: boolean;
|
|
30
|
+
flowProgressScript: boolean;
|
|
31
|
+
settingsEntries: boolean;
|
|
32
|
+
};
|
|
33
|
+
ceraphDirPresent: boolean;
|
|
34
|
+
authPresent: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface FootprintDeps {
|
|
37
|
+
homeDir?: () => string;
|
|
38
|
+
detectMonorepo?: typeof detectMonorepoSubpackages;
|
|
39
|
+
}
|
|
40
|
+
export declare function getInstalledFootprint(projectDir: string, deps?: FootprintDeps): Promise<InstalledFootprint>;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectMonorepoSubpackages } from "../init/monorepo.js";
|
|
4
|
+
import { detectPackageManager, } from "./revert-package.js";
|
|
5
|
+
import { detectRevertTarget } from "./revert-boot.js";
|
|
6
|
+
import { userGlobalMcpClientPaths } from "./revert-mcp-clients.js";
|
|
7
|
+
import { CLAUDE_SETTINGS_REL_PATHS, ERROR_HOOK_COMMAND, ERROR_HOOK_MATCHER, ERROR_HOOK_REL_PATH, FLOW_PROGRESS_HOOK_COMMAND, FLOW_PROGRESS_HOOK_MATCHER, FLOW_PROGRESS_HOOK_REL_PATH, } from "../init/claude-hook-constants.js";
|
|
8
|
+
async function fileExists(path) {
|
|
9
|
+
try {
|
|
10
|
+
await access(path);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function readPackageJson(projectDir) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(join(projectDir, "package.json"), "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function detectGitignoreEntries(projectDir) {
|
|
27
|
+
const path = join(projectDir, ".gitignore");
|
|
28
|
+
let content;
|
|
29
|
+
try {
|
|
30
|
+
content = await readFile(path, "utf-8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const TARGETS = [
|
|
36
|
+
".rn-errors.json",
|
|
37
|
+
".rn-flow-progress.json",
|
|
38
|
+
".rn-mcp-cache/",
|
|
39
|
+
".rn-mcp-cache",
|
|
40
|
+
];
|
|
41
|
+
return TARGETS.filter((t) => content.split(/\r?\n/).some((line) => line.trim() === t));
|
|
42
|
+
}
|
|
43
|
+
async function detectSchemeRegistered(projectDir) {
|
|
44
|
+
const appJsonPath = join(projectDir, "app.json");
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = await readFile(appJsonPath, "utf-8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const scheme = parsed.expo?.scheme;
|
|
60
|
+
if (typeof scheme === "string")
|
|
61
|
+
return scheme === "ceraph";
|
|
62
|
+
if (Array.isArray(scheme))
|
|
63
|
+
return scheme.includes("ceraph");
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
async function countCeraphCameraUsages(projectDir) {
|
|
67
|
+
const SCAN_ROOTS = ["app", "src", "screens"];
|
|
68
|
+
const SCAN_EXT = new Set([".tsx", ".jsx"]);
|
|
69
|
+
const SKIP_DIRS = new Set([
|
|
70
|
+
"node_modules",
|
|
71
|
+
"dist",
|
|
72
|
+
"build",
|
|
73
|
+
".git",
|
|
74
|
+
".ceraph",
|
|
75
|
+
".rn-mcp-cache",
|
|
76
|
+
".expo",
|
|
77
|
+
".next",
|
|
78
|
+
"ios",
|
|
79
|
+
"android",
|
|
80
|
+
]);
|
|
81
|
+
let count = 0;
|
|
82
|
+
const walk = async (dir) => {
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
const { readdir } = await import("node:fs/promises");
|
|
86
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (SKIP_DIRS.has(entry.name))
|
|
94
|
+
continue;
|
|
95
|
+
await walk(join(dir, entry.name));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!entry.isFile())
|
|
99
|
+
continue;
|
|
100
|
+
const dot = entry.name.lastIndexOf(".");
|
|
101
|
+
if (dot < 0)
|
|
102
|
+
continue;
|
|
103
|
+
const ext = entry.name.slice(dot).toLowerCase();
|
|
104
|
+
if (!SCAN_EXT.has(ext))
|
|
105
|
+
continue;
|
|
106
|
+
try {
|
|
107
|
+
const raw = await readFile(join(dir, entry.name), "utf-8");
|
|
108
|
+
const matches = raw.match(/<CeraphCamera/g);
|
|
109
|
+
if (matches)
|
|
110
|
+
count += matches.length;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
for (const root of SCAN_ROOTS) {
|
|
117
|
+
await walk(join(projectDir, root));
|
|
118
|
+
}
|
|
119
|
+
for (const single of ["App.tsx", "App.jsx"]) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await readFile(join(projectDir, single), "utf-8");
|
|
122
|
+
const matches = raw.match(/<CeraphCamera/g);
|
|
123
|
+
if (matches)
|
|
124
|
+
count += matches.length;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
async function detectMcpClientConfigured(projectDir, home) {
|
|
132
|
+
const hasEntry = async (path, kind) => {
|
|
133
|
+
try {
|
|
134
|
+
const raw = await readFile(path, "utf-8");
|
|
135
|
+
if (kind === "json") {
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
137
|
+
const servers = parsed.mcpServers ?? {};
|
|
138
|
+
return Boolean(servers["react-native-mcp"] || servers["mobile-mcp"]);
|
|
139
|
+
}
|
|
140
|
+
return (raw.includes("[mcp_servers.react-native-mcp]") ||
|
|
141
|
+
raw.includes("[mcp_servers.mobile-mcp]"));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const globals = userGlobalMcpClientPaths(home || undefined);
|
|
148
|
+
const byLabel = {};
|
|
149
|
+
for (const g of globals)
|
|
150
|
+
byLabel[g.label] = g.path;
|
|
151
|
+
return {
|
|
152
|
+
claudeCode: await hasEntry(join(projectDir, ".mcp.json"), "json"),
|
|
153
|
+
cursor: await hasEntry(join(projectDir, ".cursor", "mcp.json"), "json"),
|
|
154
|
+
codex: await hasEntry(join(projectDir, ".codex", "config.toml"), "toml"),
|
|
155
|
+
vscode: await hasEntry(join(projectDir, ".vscode", "mcp.json"), "json"),
|
|
156
|
+
windsurf: home
|
|
157
|
+
? await hasEntry(byLabel.Windsurf ?? "", "json")
|
|
158
|
+
: false,
|
|
159
|
+
antigravity: home
|
|
160
|
+
? await hasEntry(byLabel.Antigravity ?? "", "json")
|
|
161
|
+
: false,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function detectClaudeHooksPresent(projectDir) {
|
|
165
|
+
const errorScript = await fileExists(join(projectDir, ...ERROR_HOOK_REL_PATH));
|
|
166
|
+
const flowProgressScript = await fileExists(join(projectDir, ...FLOW_PROGRESS_HOOK_REL_PATH));
|
|
167
|
+
let settingsEntries = false;
|
|
168
|
+
for (const relParts of CLAUDE_SETTINGS_REL_PATHS) {
|
|
169
|
+
const abs = join(projectDir, ...relParts);
|
|
170
|
+
let raw;
|
|
171
|
+
try {
|
|
172
|
+
raw = await readFile(abs, "utf-8");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let parsed;
|
|
178
|
+
try {
|
|
179
|
+
parsed = JSON.parse(raw);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
185
|
+
continue;
|
|
186
|
+
const hooks = parsed.hooks;
|
|
187
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
|
|
188
|
+
continue;
|
|
189
|
+
const fc = hooks.FileChanged;
|
|
190
|
+
if (!Array.isArray(fc))
|
|
191
|
+
continue;
|
|
192
|
+
for (const entry of fc) {
|
|
193
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
194
|
+
continue;
|
|
195
|
+
const e = entry;
|
|
196
|
+
const matcher = e.matcher;
|
|
197
|
+
if (matcher !== ERROR_HOOK_MATCHER && matcher !== FLOW_PROGRESS_HOOK_MATCHER) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const ourCommand = matcher === ERROR_HOOK_MATCHER
|
|
201
|
+
? ERROR_HOOK_COMMAND
|
|
202
|
+
: FLOW_PROGRESS_HOOK_COMMAND;
|
|
203
|
+
const list = e.hooks;
|
|
204
|
+
if (!Array.isArray(list))
|
|
205
|
+
continue;
|
|
206
|
+
for (const raw2 of list) {
|
|
207
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2))
|
|
208
|
+
continue;
|
|
209
|
+
const cmd = raw2.command;
|
|
210
|
+
if (typeof cmd === "string" && cmd === ourCommand) {
|
|
211
|
+
settingsEntries = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (settingsEntries)
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (settingsEntries)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
return { errorScript, flowProgressScript, settingsEntries };
|
|
222
|
+
}
|
|
223
|
+
export async function getInstalledFootprint(projectDir, deps = {}) {
|
|
224
|
+
const detect = deps.detectMonorepo ?? detectMonorepoSubpackages;
|
|
225
|
+
const detection = await detect(projectDir);
|
|
226
|
+
let workingDir = projectDir;
|
|
227
|
+
let monorepoConflict = null;
|
|
228
|
+
if (detection.kind === "single" && detection.matches[0]) {
|
|
229
|
+
workingDir = detection.matches[0].absPath;
|
|
230
|
+
}
|
|
231
|
+
else if (detection.kind === "multi") {
|
|
232
|
+
monorepoConflict = {
|
|
233
|
+
matches: detection.matches.map((m) => ({
|
|
234
|
+
relPath: m.relPath,
|
|
235
|
+
absPath: m.absPath,
|
|
236
|
+
})),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const home = deps.homeDir?.() ??
|
|
240
|
+
process.env.HOME ??
|
|
241
|
+
process.env.USERPROFILE ??
|
|
242
|
+
"";
|
|
243
|
+
const pkg = await readPackageJson(workingDir);
|
|
244
|
+
const allDeps = {
|
|
245
|
+
...(pkg?.dependencies ?? {}),
|
|
246
|
+
...(pkg?.devDependencies ?? {}),
|
|
247
|
+
};
|
|
248
|
+
const packageInstalled = Boolean(allDeps["@ceraph/react-native-mcp"]);
|
|
249
|
+
const appiumPresent = Boolean(allDeps["appium-webdriveragent"]);
|
|
250
|
+
const packageManager = await detectPackageManager(workingDir);
|
|
251
|
+
const gitignoreEntries = await detectGitignoreEntries(workingDir);
|
|
252
|
+
const schemeRegistered = await detectSchemeRegistered(workingDir);
|
|
253
|
+
const bootTarget = await detectRevertTarget(workingDir);
|
|
254
|
+
let bootInjected = null;
|
|
255
|
+
if (bootTarget) {
|
|
256
|
+
try {
|
|
257
|
+
const raw = await readFile(bootTarget.filePath, "utf-8");
|
|
258
|
+
bootInjected = raw.includes("installCeraph(");
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
bootInjected = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const ceraphCameraUsageCount = await countCeraphCameraUsages(workingDir);
|
|
265
|
+
const mcpClientsConfigured = await detectMcpClientConfigured(workingDir, home);
|
|
266
|
+
const claudeHooksPresent = await detectClaudeHooksPresent(workingDir);
|
|
267
|
+
const ceraphDirPresent = await fileExists(join(workingDir, ".ceraph"));
|
|
268
|
+
const authPresent = home
|
|
269
|
+
? await fileExists(join(home, ".ceraph", "auth.json"))
|
|
270
|
+
: false;
|
|
271
|
+
return {
|
|
272
|
+
workingDir,
|
|
273
|
+
monorepoConflict,
|
|
274
|
+
packageInstalled,
|
|
275
|
+
packageManager,
|
|
276
|
+
appiumPresent,
|
|
277
|
+
gitignoreEntriesPresent: gitignoreEntries.length > 0,
|
|
278
|
+
gitignoreEntries,
|
|
279
|
+
schemeRegistered,
|
|
280
|
+
bootTargetPath: bootTarget?.filePath ?? null,
|
|
281
|
+
bootInjected,
|
|
282
|
+
ceraphCameraUsageCount,
|
|
283
|
+
mcpClientsConfigured,
|
|
284
|
+
claudeHooksPresent,
|
|
285
|
+
ceraphDirPresent,
|
|
286
|
+
authPresent,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type UninstallWalkthroughResult } from "./walkthrough.js";
|
|
3
|
+
interface RegisterServer {
|
|
4
|
+
tool: (name: string, description: string, schema: Record<string, z.ZodTypeAny>, handler: (args: Record<string, unknown>) => Promise<{
|
|
5
|
+
content: {
|
|
6
|
+
type: "text";
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
isError?: true;
|
|
10
|
+
}>) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function compactWalkthroughResult(result: UninstallWalkthroughResult, maxBytes?: number): UninstallWalkthroughResult;
|
|
13
|
+
export declare function registerUninstallTools(server: RegisterServer): void;
|
|
14
|
+
export {};
|