@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,1244 @@
1
+ import { copyFile, mkdir, readFile, rm, stat } from "node:fs/promises";
2
+ import { tmpdir, homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, relative } from "node:path";
4
+ import { Node, Project, } from "ts-morph";
5
+ import { detectMonorepoSubpackages, } from "../init/monorepo.js";
6
+ import { snapshotFile, restoreFile } from "../init/ast-camera.js";
7
+ import { revertGitignore, } from "./revert-gitignore.js";
8
+ import { revertSignalHostEnv, } from "./revert-signal-host-env.js";
9
+ import { revertCeraphDir, } from "./revert-ceraph-dir.js";
10
+ import { revertAuth } from "./revert-auth.js";
11
+ import { revertCeraphUrlScheme, } from "./revert-scheme.js";
12
+ import { revertMcpClients, revertUserGlobalMcpClients, USER_GLOBAL_MCP_CLIENTS, userGlobalMcpClientPaths, } from "./revert-mcp-clients.js";
13
+ import { revertClaudeHooks, } from "./revert-claude-hooks.js";
14
+ import { revertBoot, detectRevertTarget, } from "./revert-boot.js";
15
+ import { revertCamera, } from "./revert-camera.js";
16
+ import { revertPackage, } from "./revert-package.js";
17
+ const NOOP_SPAWN = async (_) => ({ exitCode: 0 });
18
+ export async function runUninstallWalkthrough(opts) {
19
+ const steps = [];
20
+ const emit = (step) => {
21
+ steps.push(step);
22
+ opts.onStep?.(step);
23
+ };
24
+ const monorepo = await resolveWorkingDir(opts);
25
+ if (monorepo.kind === "error") {
26
+ const step = {
27
+ name: "monorepo",
28
+ status: "error",
29
+ details: { matches: monorepo.matches },
30
+ remediation: monorepo.remediation,
31
+ };
32
+ emit(step);
33
+ return finalize(opts.projectDir, steps);
34
+ }
35
+ const workingDir = monorepo.workingDir;
36
+ const dryRun = opts.dryRun === true;
37
+ const cameraStep = await runCameraStep(workingDir, opts, dryRun);
38
+ emit(cameraStep);
39
+ const bootStep = await runBootStep(workingDir, opts, dryRun);
40
+ emit(bootStep);
41
+ const schemeStep = await runSchemeStep(workingDir, opts, dryRun);
42
+ emit(schemeStep);
43
+ const mcpStep = await runMcpClientsStep(workingDir, opts, dryRun);
44
+ emit(mcpStep);
45
+ const claudeHooksStep = await runClaudeHooksStep(workingDir, opts, dryRun);
46
+ emit(claudeHooksStep);
47
+ const gitignoreStep = await runGitignoreStep(workingDir, opts, dryRun);
48
+ emit(gitignoreStep);
49
+ const signalHostStep = await runSignalHostStep(opts, dryRun);
50
+ emit(signalHostStep);
51
+ const ceraphDirStep = await runCeraphDirStep(workingDir, opts, dryRun);
52
+ emit(ceraphDirStep);
53
+ const authStep = await runAuthStep(opts, dryRun);
54
+ emit(authStep);
55
+ const packageStep = await runPackageStep(workingDir, opts, dryRun);
56
+ emit(packageStep);
57
+ return finalize(workingDir, steps);
58
+ }
59
+ async function resolveWorkingDir(opts) {
60
+ const detect = opts.detectMonorepo ?? detectMonorepoSubpackages;
61
+ const detection = await detect(opts.projectDir);
62
+ if (detection.kind === "none") {
63
+ return { kind: "ok", workingDir: opts.projectDir };
64
+ }
65
+ if (detection.kind === "single") {
66
+ const m = detection.matches[0];
67
+ if (!m)
68
+ return { kind: "ok", workingDir: opts.projectDir };
69
+ return { kind: "ok", workingDir: m.absPath };
70
+ }
71
+ return {
72
+ kind: "error",
73
+ matches: detection.matches.map((m) => ({
74
+ relPath: m.relPath,
75
+ absPath: m.absPath,
76
+ })),
77
+ remediation: "Multiple React Native subpackages detected. Pass --project-dir " +
78
+ "to target a specific subpackage, e.g. `--project-dir " +
79
+ `${detection.matches[0]?.relPath ?? "apps/mobile"}\`.`,
80
+ };
81
+ }
82
+ async function runCameraStep(workingDir, opts, dryRun) {
83
+ const primitive = opts.revertCamera ?? revertCamera;
84
+ if (dryRun) {
85
+ try {
86
+ const preview = await previewCameraRevert(workingDir, primitive);
87
+ return makeCameraStepFromResult(preview, { dryRun: true });
88
+ }
89
+ catch (err) {
90
+ return {
91
+ name: "camera",
92
+ status: "error",
93
+ details: { error: errMsg(err) },
94
+ remediation: "Dry-run camera preview failed. Re-run without --dry-run after " +
95
+ "fixing the error above, or pass --project-dir if the path is wrong.",
96
+ };
97
+ }
98
+ }
99
+ const snapshots = new Map();
100
+ try {
101
+ const filesToSnapshot = await listFilesContainingCeraphCamera(workingDir);
102
+ for (const f of filesToSnapshot) {
103
+ snapshots.set(f, await snapshotFile(f));
104
+ }
105
+ const result = await primitive(workingDir);
106
+ return makeCameraStepFromResult(result, { dryRun: false });
107
+ }
108
+ catch (err) {
109
+ const restoreFailures = [];
110
+ for (const [path, snap] of snapshots) {
111
+ if (snap == null)
112
+ continue;
113
+ try {
114
+ await restoreFile(path, snap);
115
+ }
116
+ catch (restoreErr) {
117
+ restoreFailures.push(`${path}: ${errMsg(restoreErr)}`);
118
+ }
119
+ }
120
+ const baseRemediation = `Camera revert failed: ${errMsg(err)}.`;
121
+ const remediation = restoreFailures.length === 0
122
+ ? `${baseRemediation} All edited files were restored from snapshots.`
123
+ : `${baseRemediation} Some files could not be restored: ` +
124
+ restoreFailures.join("; ");
125
+ return {
126
+ name: "camera",
127
+ status: "error",
128
+ details: {
129
+ error: errMsg(err),
130
+ snapshotCount: snapshots.size,
131
+ restoreFailures,
132
+ },
133
+ remediation,
134
+ };
135
+ }
136
+ }
137
+ function makeCameraStepFromResult(result, ctx) {
138
+ const totalReverted = result.files.reduce((n, f) => n + f.ceraphCamerasRemoved, 0);
139
+ const imageKeysStripped = totalReverted;
140
+ if (result.files.length === 0) {
141
+ return {
142
+ name: "camera",
143
+ status: "already-reverted",
144
+ details: {
145
+ filesScanned: result.filesScanned,
146
+ ...(ctx.dryRun ? { dryRun: true } : {}),
147
+ },
148
+ };
149
+ }
150
+ const status = imageKeysStripped > 0 ? "warning" : "reverted";
151
+ const step = {
152
+ name: "camera",
153
+ status,
154
+ details: {
155
+ filesScanned: result.filesScanned,
156
+ filesEdited: result.files.map((f) => f.relPath),
157
+ ceraphCamerasRemoved: totalReverted,
158
+ importDropped: result.files.filter((f) => f.importDropped).length,
159
+ importRestored: result.files.filter((f) => f.importRestored).length,
160
+ imageKeysStripped,
161
+ ...(ctx.dryRun ? { dryRun: true } : {}),
162
+ },
163
+ };
164
+ if (status === "warning") {
165
+ step.remediation =
166
+ `revert-camera removed imageKey props from ${imageKeysStripped} ` +
167
+ `<CeraphCamera> tag(s). If any of those keys were customer-set ` +
168
+ `(not Ceraph-generated), re-apply them after install rebuilds the ` +
169
+ `camera surface.`;
170
+ }
171
+ return step;
172
+ }
173
+ async function listFilesContainingCeraphCamera(projectDir) {
174
+ const SCAN_ROOTS = ["app", "src", "screens"];
175
+ const SCAN_EXT = new Set([".tsx", ".jsx"]);
176
+ const SKIP_DIRS = new Set([
177
+ "node_modules",
178
+ "dist",
179
+ "build",
180
+ ".git",
181
+ ".ceraph",
182
+ ".rn-mcp-cache",
183
+ ".expo",
184
+ ".next",
185
+ "ios",
186
+ "android",
187
+ ]);
188
+ const candidates = [];
189
+ const walk = async (dir) => {
190
+ let entries;
191
+ try {
192
+ const { readdir } = await import("node:fs/promises");
193
+ entries = await readdir(dir, { withFileTypes: true });
194
+ }
195
+ catch {
196
+ return;
197
+ }
198
+ for (const entry of entries) {
199
+ if (entry.isDirectory()) {
200
+ if (SKIP_DIRS.has(entry.name))
201
+ continue;
202
+ await walk(join(dir, entry.name));
203
+ continue;
204
+ }
205
+ if (!entry.isFile())
206
+ continue;
207
+ const dot = entry.name.lastIndexOf(".");
208
+ if (dot < 0)
209
+ continue;
210
+ const ext = entry.name.slice(dot).toLowerCase();
211
+ if (!SCAN_EXT.has(ext))
212
+ continue;
213
+ candidates.push(join(dir, entry.name));
214
+ }
215
+ };
216
+ for (const root of SCAN_ROOTS) {
217
+ await walk(join(projectDir, root));
218
+ }
219
+ for (const single of ["App.tsx", "App.jsx"]) {
220
+ candidates.push(join(projectDir, single));
221
+ }
222
+ const out = [];
223
+ for (const path of candidates) {
224
+ let raw;
225
+ try {
226
+ raw = await readFile(path, "utf-8");
227
+ }
228
+ catch {
229
+ continue;
230
+ }
231
+ if (raw.includes("<CeraphCamera"))
232
+ out.push(path);
233
+ }
234
+ return out;
235
+ }
236
+ async function previewCameraRevert(projectDir, primitive) {
237
+ const files = await listFilesContainingCeraphCamera(projectDir);
238
+ const tmp = join(tmpdir(), `ceraph-uninstall-preview-${Date.now()}-${Math.random()
239
+ .toString(36)
240
+ .slice(2)}`);
241
+ try {
242
+ await mkdir(tmp, { recursive: true });
243
+ for (const abs of files) {
244
+ const rel = relative(projectDir, abs);
245
+ const dest = join(tmp, rel);
246
+ await mkdir(dirname(dest), { recursive: true });
247
+ await copyFile(abs, dest);
248
+ }
249
+ return await primitive(tmp);
250
+ }
251
+ finally {
252
+ try {
253
+ await rm(tmp, { recursive: true, force: true });
254
+ }
255
+ catch {
256
+ }
257
+ }
258
+ }
259
+ async function runBootStep(workingDir, opts, dryRun) {
260
+ const primitive = opts.revertBoot ?? revertBoot;
261
+ const detect = opts.detectBootTarget ?? detectRevertTarget;
262
+ if (dryRun) {
263
+ try {
264
+ const target = await detect(workingDir);
265
+ if (!target) {
266
+ return {
267
+ name: "boot",
268
+ status: "already-reverted",
269
+ details: { reason: "no layout file detected", dryRun: true },
270
+ };
271
+ }
272
+ const tmpProj = await stageSingleFileInTmp(target.filePath, workingDir);
273
+ try {
274
+ const result = await primitive(tmpProj.projectDir);
275
+ return await boot_resultToStep(result, workingDir, target, opts, true);
276
+ }
277
+ finally {
278
+ await rm(tmpProj.tmpRoot, { recursive: true, force: true }).catch(() => undefined);
279
+ }
280
+ }
281
+ catch (err) {
282
+ return {
283
+ name: "boot",
284
+ status: "error",
285
+ details: { error: errMsg(err), dryRun: true },
286
+ remediation: "Dry-run boot preview failed; re-run without --dry-run to see " +
287
+ "the real error path.",
288
+ };
289
+ }
290
+ }
291
+ try {
292
+ const result = await primitive(workingDir);
293
+ const target = result.status === "no-layout-found"
294
+ ? null
295
+ : result.target;
296
+ return await boot_resultToStep(result, workingDir, target, opts, false);
297
+ }
298
+ catch (err) {
299
+ return {
300
+ name: "boot",
301
+ status: "error",
302
+ details: { error: errMsg(err) },
303
+ remediation: "Boot revert raised. Check the layout file for syntax errors " +
304
+ "before re-running.",
305
+ };
306
+ }
307
+ }
308
+ async function boot_resultToStep(result, workingDir, target, opts, dryRun) {
309
+ switch (result.status) {
310
+ case "reverted":
311
+ return {
312
+ name: "boot",
313
+ status: "reverted",
314
+ details: {
315
+ target: result.target,
316
+ removedUseEffect: result.removedUseEffect,
317
+ removedImport: result.removedImport,
318
+ ...(dryRun ? { dryRun: true } : {}),
319
+ },
320
+ };
321
+ case "already-reverted":
322
+ return {
323
+ name: "boot",
324
+ status: "already-reverted",
325
+ details: { target: result.target, ...(dryRun ? { dryRun: true } : {}) },
326
+ };
327
+ case "no-layout-found":
328
+ return {
329
+ name: "boot",
330
+ status: "already-reverted",
331
+ details: { reason: "no layout file detected", ...(dryRun ? { dryRun: true } : {}) },
332
+ };
333
+ case "skipped": {
334
+ const remediationDetail = await analyzeBootSkippedConflict(result.target.filePath);
335
+ return {
336
+ name: "boot",
337
+ status: "manual",
338
+ details: {
339
+ target: result.target,
340
+ reason: result.reason,
341
+ location: remediationDetail.location,
342
+ otherStatements: remediationDetail.otherStatements,
343
+ ...(remediationDetail.parseError !== undefined
344
+ ? { parseError: remediationDetail.parseError }
345
+ : {}),
346
+ ...(dryRun ? { dryRun: true } : {}),
347
+ },
348
+ remediation: `installCeraph() is wrapped in a useEffect that also contains ` +
349
+ `other statements (see ${result.target.filePath}` +
350
+ (remediationDetail.location
351
+ ? `:${remediationDetail.location.line}`
352
+ : "") +
353
+ `). Remove the \`installCeraph()\` call by hand without ` +
354
+ `dropping your own code, then re-run \`npx @ceraph/react-native-mcp ` +
355
+ `uninstall\`.` +
356
+ (remediationDetail.parseError !== undefined
357
+ ? ` (Note: re-parse failed — line info missing: ${remediationDetail.parseError})`
358
+ : ""),
359
+ };
360
+ }
361
+ }
362
+ return {
363
+ name: "boot",
364
+ status: "error",
365
+ details: { result },
366
+ remediation: "Boot revert returned an unrecognised status shape.",
367
+ };
368
+ }
369
+ async function analyzeBootSkippedConflict(filePath) {
370
+ try {
371
+ const project = new Project({
372
+ useInMemoryFileSystem: false,
373
+ skipAddingFilesFromTsConfig: true,
374
+ skipFileDependencyResolution: true,
375
+ skipLoadingLibFiles: true,
376
+ compilerOptions: { allowJs: true, jsx: 1 },
377
+ });
378
+ const source = project.addSourceFileAtPath(filePath);
379
+ const fn = findDefaultExportFn(source);
380
+ if (!fn)
381
+ return { location: null, otherStatements: [] };
382
+ const body = fn.getBody();
383
+ if (!body || !Node.isBlock(body)) {
384
+ return { location: null, otherStatements: [] };
385
+ }
386
+ let location = null;
387
+ let otherStatements = [];
388
+ body.forEachDescendant((node) => {
389
+ if (location)
390
+ return;
391
+ if (!Node.isCallExpression(node))
392
+ return;
393
+ const callee = node.getExpression();
394
+ if (!Node.isIdentifier(callee) || callee.getText() !== "useEffect") {
395
+ return;
396
+ }
397
+ const args = node.getArguments();
398
+ const cb = args[0];
399
+ if (!cb)
400
+ return;
401
+ if (!Node.isArrowFunction(cb) && !Node.isFunctionExpression(cb)) {
402
+ return;
403
+ }
404
+ const cbBody = cb.getBody();
405
+ if (!Node.isBlock(cbBody))
406
+ return;
407
+ const stmts = cbBody.getStatements();
408
+ const installCeraphStmts = [];
409
+ const others = [];
410
+ for (const s of stmts) {
411
+ if (isSoloInstallCeraphCall(s))
412
+ installCeraphStmts.push(s);
413
+ else
414
+ others.push(s);
415
+ }
416
+ if (installCeraphStmts.length > 0 && others.length > 0) {
417
+ location = { line: node.getStartLineNumber() };
418
+ otherStatements = others.map((s) => s.getText());
419
+ }
420
+ });
421
+ return { location, otherStatements };
422
+ }
423
+ catch (err) {
424
+ return {
425
+ location: null,
426
+ otherStatements: [],
427
+ parseError: errMsg(err),
428
+ };
429
+ }
430
+ }
431
+ function findDefaultExportFn(source) {
432
+ for (const fn of source.getFunctions()) {
433
+ if (fn.isDefaultExport())
434
+ return fn;
435
+ }
436
+ const exportAssignment = source
437
+ .getStatements()
438
+ .find((s) => Node.isExportAssignment(s));
439
+ if (exportAssignment && Node.isExportAssignment(exportAssignment)) {
440
+ const expr = exportAssignment.getExpression();
441
+ if (Node.isIdentifier(expr)) {
442
+ const symbol = expr.getSymbol();
443
+ if (symbol) {
444
+ const decl = symbol.getDeclarations()[0];
445
+ if (decl && Node.isVariableDeclaration(decl)) {
446
+ const init = decl.getInitializer();
447
+ if (init && Node.isArrowFunction(init))
448
+ return init;
449
+ if (init && Node.isFunctionExpression(init))
450
+ return init;
451
+ }
452
+ }
453
+ }
454
+ if (Node.isArrowFunction(expr))
455
+ return expr;
456
+ if (Node.isFunctionExpression(expr))
457
+ return expr;
458
+ }
459
+ return null;
460
+ }
461
+ function isSoloInstallCeraphCall(stmt) {
462
+ if (!Node.isExpressionStatement(stmt))
463
+ return false;
464
+ const expr = stmt.getExpression();
465
+ if (!Node.isCallExpression(expr))
466
+ return false;
467
+ const callee = expr.getExpression();
468
+ return Node.isIdentifier(callee) && callee.getText() === "installCeraph";
469
+ }
470
+ async function runSchemeStep(workingDir, opts, dryRun) {
471
+ const primitive = opts.revertCeraphUrlScheme ?? revertCeraphUrlScheme;
472
+ if (dryRun) {
473
+ try {
474
+ const tmpProj = await stageProjectFilesForSchemeDryRun(workingDir);
475
+ try {
476
+ const result = await primitive(tmpProj.projectDir);
477
+ return schemeResultToStep(result, true);
478
+ }
479
+ finally {
480
+ await rm(tmpProj.tmpRoot, { recursive: true, force: true }).catch(() => undefined);
481
+ }
482
+ }
483
+ catch (err) {
484
+ return {
485
+ name: "scheme",
486
+ status: "error",
487
+ details: { error: errMsg(err), dryRun: true },
488
+ };
489
+ }
490
+ }
491
+ try {
492
+ const result = await primitive(workingDir);
493
+ return schemeResultToStep(result, false);
494
+ }
495
+ catch (err) {
496
+ return {
497
+ name: "scheme",
498
+ status: "error",
499
+ details: { error: errMsg(err) },
500
+ };
501
+ }
502
+ }
503
+ function schemeResultToStep(result, dryRun) {
504
+ const dryRunTag = dryRun ? { dryRun: true } : {};
505
+ switch (result.status) {
506
+ case "reverted":
507
+ return {
508
+ name: "scheme",
509
+ status: "reverted",
510
+ details: {
511
+ path: result.path,
512
+ previousScheme: result.previousScheme,
513
+ nextScheme: result.nextScheme,
514
+ ...dryRunTag,
515
+ },
516
+ };
517
+ case "already-reverted":
518
+ return {
519
+ name: "scheme",
520
+ status: "already-reverted",
521
+ details: { path: result.path, ...dryRunTag },
522
+ };
523
+ case "manual":
524
+ return {
525
+ name: "scheme",
526
+ status: "manual",
527
+ details: { kind: result.kind, path: result.path, ...dryRunTag },
528
+ remediation: result.remediation,
529
+ };
530
+ case "no-rn-project":
531
+ return {
532
+ name: "scheme",
533
+ status: "already-reverted",
534
+ details: { reason: "no RN project detected", ...dryRunTag },
535
+ };
536
+ }
537
+ }
538
+ async function stageProjectFilesForSchemeDryRun(projectDir) {
539
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-scheme-preview-${Date.now()}-${Math.random()
540
+ .toString(36)
541
+ .slice(2)}`);
542
+ return stageOrCleanup(tmpRoot, async () => {
543
+ await mkdir(tmpRoot, { recursive: true });
544
+ for (const name of ["app.json", "app.config.js", "app.config.ts"]) {
545
+ const src = join(projectDir, name);
546
+ try {
547
+ await copyFile(src, join(tmpRoot, name));
548
+ }
549
+ catch {
550
+ }
551
+ }
552
+ for (const native of ["ios", "android"]) {
553
+ try {
554
+ await stat(join(projectDir, native));
555
+ await mkdir(join(tmpRoot, native), { recursive: true });
556
+ }
557
+ catch {
558
+ }
559
+ }
560
+ return { projectDir: tmpRoot, tmpRoot };
561
+ });
562
+ }
563
+ async function runMcpClientsStep(workingDir, opts, dryRun) {
564
+ const primitive = opts.revertMcpClients ?? revertMcpClients;
565
+ const globalPrimitive = opts.revertUserGlobalMcpClients ?? revertUserGlobalMcpClients;
566
+ const home = opts.homeDir?.();
567
+ if (dryRun) {
568
+ let tmpProj = null;
569
+ let tmpHome = null;
570
+ try {
571
+ tmpProj = await stageMcpClientFiles(workingDir);
572
+ tmpHome = await stageUserGlobalMcpClientFiles(home);
573
+ const result = await primitive(tmpProj.projectDir);
574
+ const globalResult = await globalPrimitive({ home: tmpHome.tmpHome });
575
+ return mcpResultToStep(result, globalResult, true);
576
+ }
577
+ catch (err) {
578
+ return {
579
+ name: "mcp-clients",
580
+ status: "error",
581
+ details: { error: errMsg(err), dryRun: true },
582
+ };
583
+ }
584
+ finally {
585
+ if (tmpProj) {
586
+ await rm(tmpProj.tmpRoot, { recursive: true, force: true }).catch(() => undefined);
587
+ }
588
+ if (tmpHome) {
589
+ await rm(tmpHome.tmpRoot, { recursive: true, force: true }).catch(() => undefined);
590
+ }
591
+ }
592
+ }
593
+ try {
594
+ const result = await primitive(workingDir);
595
+ const globalResult = await globalPrimitive({ home });
596
+ return mcpResultToStep(result, globalResult, false);
597
+ }
598
+ catch (err) {
599
+ return {
600
+ name: "mcp-clients",
601
+ status: "error",
602
+ details: { error: errMsg(err) },
603
+ };
604
+ }
605
+ }
606
+ function mcpResultToStep(result, globalResult, dryRun) {
607
+ const combined = [...result.files, ...globalResult.files];
608
+ const touched = combined.filter((f) => f.status === "removed-entries" || f.status === "deleted-file");
609
+ const dryRunTag = dryRun ? { dryRun: true } : {};
610
+ if (touched.length === 0) {
611
+ return {
612
+ name: "mcp-clients",
613
+ status: "already-reverted",
614
+ details: {
615
+ files: result.files,
616
+ userGlobalFiles: globalResult.files,
617
+ ...dryRunTag,
618
+ },
619
+ };
620
+ }
621
+ return {
622
+ name: "mcp-clients",
623
+ status: "reverted",
624
+ details: {
625
+ files: result.files,
626
+ userGlobalFiles: globalResult.files,
627
+ touched: touched.length,
628
+ ...dryRunTag,
629
+ },
630
+ };
631
+ }
632
+ async function stageMcpClientFiles(projectDir) {
633
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-mcp-preview-${Date.now()}-${Math.random()
634
+ .toString(36)
635
+ .slice(2)}`);
636
+ return stageOrCleanup(tmpRoot, async () => {
637
+ await mkdir(tmpRoot, { recursive: true });
638
+ const targets = [
639
+ ".mcp.json",
640
+ join(".cursor", "mcp.json"),
641
+ join(".vscode", "mcp.json"),
642
+ join(".codex", "config.toml"),
643
+ ];
644
+ for (const rel of targets) {
645
+ const src = join(projectDir, rel);
646
+ const dest = join(tmpRoot, rel);
647
+ try {
648
+ await mkdir(dirname(dest), { recursive: true });
649
+ await copyFile(src, dest);
650
+ }
651
+ catch {
652
+ }
653
+ }
654
+ return { projectDir: tmpRoot, tmpRoot };
655
+ });
656
+ }
657
+ async function stageUserGlobalMcpClientFiles(home) {
658
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-mcp-global-preview-${Date.now()}-${Math.random()
659
+ .toString(36)
660
+ .slice(2)}`);
661
+ return stageOrCleanup(tmpRoot, async () => {
662
+ const tmpHome = join(tmpRoot, "home");
663
+ await mkdir(tmpHome, { recursive: true });
664
+ const realPaths = userGlobalMcpClientPaths(home);
665
+ for (let i = 0; i < USER_GLOBAL_MCP_CLIENTS.length; i++) {
666
+ const client = USER_GLOBAL_MCP_CLIENTS[i];
667
+ const realEntry = realPaths[i];
668
+ if (!client || !realEntry)
669
+ continue;
670
+ const dest = join(tmpHome, ...client.file);
671
+ try {
672
+ await mkdir(dirname(dest), { recursive: true });
673
+ await copyFile(realEntry.path, dest);
674
+ }
675
+ catch {
676
+ }
677
+ }
678
+ return { tmpHome, tmpRoot };
679
+ });
680
+ }
681
+ async function runClaudeHooksStep(workingDir, opts, dryRun) {
682
+ const primitive = opts.revertClaudeHooks ?? revertClaudeHooks;
683
+ if (dryRun) {
684
+ try {
685
+ const tmpProj = await stageClaudeHookFiles(workingDir);
686
+ try {
687
+ const result = await primitive({ projectDir: tmpProj.projectDir });
688
+ return claudeHooksResultToStep(result, true);
689
+ }
690
+ finally {
691
+ await rm(tmpProj.tmpRoot, { recursive: true, force: true }).catch(() => undefined);
692
+ }
693
+ }
694
+ catch (err) {
695
+ return {
696
+ name: "claude-hooks",
697
+ status: "error",
698
+ details: { error: errMsg(err), dryRun: true },
699
+ };
700
+ }
701
+ }
702
+ try {
703
+ const result = await primitive({ projectDir: workingDir });
704
+ return claudeHooksResultToStep(result, false);
705
+ }
706
+ catch (err) {
707
+ return {
708
+ name: "claude-hooks",
709
+ status: "error",
710
+ details: { error: errMsg(err) },
711
+ };
712
+ }
713
+ }
714
+ function claudeHooksResultToStep(result, dryRun) {
715
+ const dryRunTag = dryRun ? { dryRun: true } : {};
716
+ switch (result.status) {
717
+ case "reverted":
718
+ return {
719
+ name: "claude-hooks",
720
+ status: "reverted",
721
+ details: { ...result.details, ...dryRunTag },
722
+ };
723
+ case "already-reverted":
724
+ return {
725
+ name: "claude-hooks",
726
+ status: "already-reverted",
727
+ details: { ...result.details, ...dryRunTag },
728
+ };
729
+ case "manual":
730
+ return {
731
+ name: "claude-hooks",
732
+ status: "manual",
733
+ details: { ...result.details, ...dryRunTag },
734
+ remediation: `Hook script(s) were customized after init wrote them, so the ` +
735
+ `reverter left them in place: ${result.details.scriptsLeftAlone.join(", ")}. ` +
736
+ `If you no longer need the customized version, delete the file ` +
737
+ `manually. Settings entries pointing at the script(s) ` +
738
+ (result.details.settingsFilesModified.length > 0
739
+ ? `were removed from ${result.details.settingsFilesModified.join(", ")}.`
740
+ : `were not touched in this run.`),
741
+ };
742
+ case "skipped":
743
+ return {
744
+ name: "claude-hooks",
745
+ status: "skipped",
746
+ details: { ...result.details, ...dryRunTag },
747
+ };
748
+ }
749
+ }
750
+ async function stageClaudeHookFiles(projectDir) {
751
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-claude-hooks-preview-${Date.now()}-${Math.random()
752
+ .toString(36)
753
+ .slice(2)}`);
754
+ return stageOrCleanup(tmpRoot, async () => {
755
+ await mkdir(tmpRoot, { recursive: true });
756
+ const targets = [
757
+ join(".claude", "hooks", "rn-error-notify.sh"),
758
+ join(".claude", "hooks", "rn-flow-progress-notify.sh"),
759
+ join(".claude", "settings.json"),
760
+ join(".claude", "settings.local.json"),
761
+ ];
762
+ for (const rel of targets) {
763
+ const src = join(projectDir, rel);
764
+ const dest = join(tmpRoot, rel);
765
+ try {
766
+ await mkdir(dirname(dest), { recursive: true });
767
+ await copyFile(src, dest);
768
+ }
769
+ catch {
770
+ }
771
+ }
772
+ return { projectDir: tmpRoot, tmpRoot };
773
+ });
774
+ }
775
+ async function runGitignoreStep(workingDir, opts, dryRun) {
776
+ const primitive = opts.revertGitignore ?? revertGitignore;
777
+ if (dryRun) {
778
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-gitignore-preview-${Date.now()}-${Math.random()
779
+ .toString(36)
780
+ .slice(2)}`);
781
+ try {
782
+ try {
783
+ await mkdir(tmpRoot, { recursive: true });
784
+ try {
785
+ await copyFile(join(workingDir, ".gitignore"), join(tmpRoot, ".gitignore"));
786
+ }
787
+ catch {
788
+ }
789
+ const result = await primitive(tmpRoot);
790
+ return gitignoreResultToStep(result, true);
791
+ }
792
+ finally {
793
+ await rm(tmpRoot, { recursive: true, force: true }).catch(() => undefined);
794
+ }
795
+ }
796
+ catch (err) {
797
+ return {
798
+ name: "gitignore",
799
+ status: "error",
800
+ details: { error: errMsg(err), dryRun: true },
801
+ };
802
+ }
803
+ }
804
+ try {
805
+ const result = await primitive(workingDir);
806
+ return gitignoreResultToStep(result, false);
807
+ }
808
+ catch (err) {
809
+ return {
810
+ name: "gitignore",
811
+ status: "error",
812
+ details: { error: errMsg(err) },
813
+ };
814
+ }
815
+ }
816
+ function gitignoreResultToStep(result, dryRun) {
817
+ const dryRunTag = dryRun ? { dryRun: true } : {};
818
+ switch (result.status) {
819
+ case "reverted":
820
+ return {
821
+ name: "gitignore",
822
+ status: "reverted",
823
+ details: {
824
+ path: result.path,
825
+ removedLines: result.removedLines,
826
+ ...dryRunTag,
827
+ },
828
+ };
829
+ case "already-reverted":
830
+ return {
831
+ name: "gitignore",
832
+ status: "already-reverted",
833
+ details: { path: result.path, ...dryRunTag },
834
+ };
835
+ case "no-gitignore":
836
+ return {
837
+ name: "gitignore",
838
+ status: "already-reverted",
839
+ details: { reason: "no .gitignore", ...dryRunTag },
840
+ };
841
+ }
842
+ }
843
+ async function runSignalHostStep(opts, dryRun) {
844
+ const primitive = opts.revertSignalHostEnv ?? revertSignalHostEnv;
845
+ const home = opts.homeDir?.();
846
+ if (dryRun) {
847
+ try {
848
+ const fakeHome = await mirrorShellProfile(home);
849
+ try {
850
+ const result = await primitive({ home: fakeHome.tmpHome });
851
+ return signalHostResultToStep(result, true);
852
+ }
853
+ finally {
854
+ await rm(fakeHome.tmpRoot, { recursive: true, force: true }).catch(() => undefined);
855
+ }
856
+ }
857
+ catch (err) {
858
+ return {
859
+ name: "signal-host",
860
+ status: "error",
861
+ details: { error: errMsg(err), dryRun: true },
862
+ };
863
+ }
864
+ }
865
+ try {
866
+ const result = await primitive({ home });
867
+ return signalHostResultToStep(result, false);
868
+ }
869
+ catch (err) {
870
+ return {
871
+ name: "signal-host",
872
+ status: "error",
873
+ details: { error: errMsg(err) },
874
+ };
875
+ }
876
+ }
877
+ function signalHostResultToStep(result, dryRun) {
878
+ const dryRunTag = dryRun ? { dryRun: true } : {};
879
+ switch (result.status) {
880
+ case "reverted":
881
+ return {
882
+ name: "signal-host",
883
+ status: "reverted",
884
+ details: {
885
+ path: result.path,
886
+ shell: result.shell,
887
+ removedIp: result.removedIp,
888
+ ...dryRunTag,
889
+ },
890
+ };
891
+ case "already-reverted":
892
+ return {
893
+ name: "signal-host",
894
+ status: "already-reverted",
895
+ details: { path: result.path, shell: result.shell, ...dryRunTag },
896
+ };
897
+ case "no-profile":
898
+ return {
899
+ name: "signal-host",
900
+ status: "already-reverted",
901
+ details: { reason: "no shell profile", path: result.path, ...dryRunTag },
902
+ };
903
+ }
904
+ }
905
+ async function mirrorShellProfile(home) {
906
+ const realHome = home ?? homedir();
907
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-shell-preview-${Date.now()}-${Math.random()
908
+ .toString(36)
909
+ .slice(2)}`);
910
+ return stageOrCleanup(tmpRoot, async () => {
911
+ const tmpHome = join(tmpRoot, "home");
912
+ await mkdir(tmpHome, { recursive: true });
913
+ for (const rc of [".zshrc", ".bashrc"]) {
914
+ try {
915
+ await copyFile(join(realHome, rc), join(tmpHome, rc));
916
+ }
917
+ catch {
918
+ }
919
+ }
920
+ return { tmpHome, tmpRoot };
921
+ });
922
+ }
923
+ async function runCeraphDirStep(workingDir, opts, dryRun) {
924
+ const primitive = opts.revertCeraphDir ?? revertCeraphDir;
925
+ if (!opts.purgeImages) {
926
+ try {
927
+ const preview = await primitive({
928
+ projectDir: workingDir,
929
+ confirm: false,
930
+ });
931
+ if (preview.status === "already-reverted") {
932
+ return {
933
+ name: "ceraph-dir",
934
+ status: "already-reverted",
935
+ details: { path: preview.path, ...(dryRun ? { dryRun: true } : {}) },
936
+ };
937
+ }
938
+ if (preview.status === "skipped-needs-confirmation") {
939
+ return {
940
+ name: "ceraph-dir",
941
+ status: "skipped",
942
+ details: {
943
+ path: preview.path,
944
+ paths: preview.paths,
945
+ ...(dryRun ? { dryRun: true } : {}),
946
+ },
947
+ remediation: "Re-run with --purge-images to delete .ceraph/ (camera images, " +
948
+ "design snapshots, run artifacts). Default leaves them in place " +
949
+ "so a re-install picks up where you left off.",
950
+ };
951
+ }
952
+ return {
953
+ name: "ceraph-dir",
954
+ status: "error",
955
+ details: {
956
+ unexpectedStatus: preview.status,
957
+ ...(dryRun ? { dryRun: true } : {}),
958
+ },
959
+ remediation: "ceraph-dir preview returned an unexpected status; pass " +
960
+ "--purge-images explicitly to delete .ceraph/.",
961
+ };
962
+ }
963
+ catch (err) {
964
+ return {
965
+ name: "ceraph-dir",
966
+ status: "error",
967
+ details: { error: errMsg(err), ...(dryRun ? { dryRun: true } : {}) },
968
+ };
969
+ }
970
+ }
971
+ if (dryRun) {
972
+ try {
973
+ const preview = await primitive({
974
+ projectDir: workingDir,
975
+ confirm: false,
976
+ });
977
+ if (preview.status === "already-reverted") {
978
+ return {
979
+ name: "ceraph-dir",
980
+ status: "already-reverted",
981
+ details: { path: preview.path, dryRun: true },
982
+ };
983
+ }
984
+ if (preview.status === "skipped-needs-confirmation") {
985
+ return {
986
+ name: "ceraph-dir",
987
+ status: "reverted",
988
+ details: {
989
+ path: preview.path,
990
+ paths: preview.paths,
991
+ dryRun: true,
992
+ },
993
+ };
994
+ }
995
+ return {
996
+ name: "ceraph-dir",
997
+ status: "error",
998
+ details: { unexpectedStatus: preview.status, dryRun: true },
999
+ };
1000
+ }
1001
+ catch (err) {
1002
+ return {
1003
+ name: "ceraph-dir",
1004
+ status: "error",
1005
+ details: { error: errMsg(err), dryRun: true },
1006
+ };
1007
+ }
1008
+ }
1009
+ try {
1010
+ const result = await primitive({
1011
+ projectDir: workingDir,
1012
+ confirm: true,
1013
+ });
1014
+ switch (result.status) {
1015
+ case "deleted":
1016
+ return {
1017
+ name: "ceraph-dir",
1018
+ status: "reverted",
1019
+ details: {
1020
+ path: result.path,
1021
+ deletedEntries: result.deletedEntries,
1022
+ },
1023
+ };
1024
+ case "already-reverted":
1025
+ return {
1026
+ name: "ceraph-dir",
1027
+ status: "already-reverted",
1028
+ details: { path: result.path },
1029
+ };
1030
+ case "skipped-needs-confirmation":
1031
+ return {
1032
+ name: "ceraph-dir",
1033
+ status: "skipped",
1034
+ details: { path: result.path, unexpected: true },
1035
+ };
1036
+ }
1037
+ }
1038
+ catch (err) {
1039
+ return {
1040
+ name: "ceraph-dir",
1041
+ status: "error",
1042
+ details: { error: errMsg(err) },
1043
+ };
1044
+ }
1045
+ }
1046
+ async function runAuthStep(opts, dryRun) {
1047
+ const primitive = opts.revertAuth ?? revertAuth;
1048
+ const home = opts.homeDir?.();
1049
+ if (!opts.global) {
1050
+ try {
1051
+ const result = await primitive({ global: false, home });
1052
+ return {
1053
+ name: "auth",
1054
+ status: "skipped",
1055
+ details: { path: result.path, ...(dryRun ? { dryRun: true } : {}) },
1056
+ remediation: "Re-run with --global to delete ~/.ceraph/auth.json. " +
1057
+ "Note: this token is shared across every Ceraph install on " +
1058
+ "this machine — removing it logs you out everywhere.",
1059
+ };
1060
+ }
1061
+ catch (err) {
1062
+ return {
1063
+ name: "auth",
1064
+ status: "error",
1065
+ details: { error: errMsg(err), ...(dryRun ? { dryRun: true } : {}) },
1066
+ };
1067
+ }
1068
+ }
1069
+ if (dryRun) {
1070
+ try {
1071
+ const realHome = home ?? homedir();
1072
+ const path = join(realHome, ".ceraph", "auth.json");
1073
+ let exists = false;
1074
+ try {
1075
+ await stat(path);
1076
+ exists = true;
1077
+ }
1078
+ catch {
1079
+ exists = false;
1080
+ }
1081
+ return {
1082
+ name: "auth",
1083
+ status: exists ? "reverted" : "already-reverted",
1084
+ details: { path, dryRun: true, wouldDelete: exists },
1085
+ };
1086
+ }
1087
+ catch (err) {
1088
+ return {
1089
+ name: "auth",
1090
+ status: "error",
1091
+ details: { error: errMsg(err), dryRun: true },
1092
+ };
1093
+ }
1094
+ }
1095
+ try {
1096
+ const result = await primitive({ global: true, home });
1097
+ switch (result.status) {
1098
+ case "deleted":
1099
+ return {
1100
+ name: "auth",
1101
+ status: "reverted",
1102
+ details: { path: result.path },
1103
+ };
1104
+ case "already-reverted":
1105
+ return {
1106
+ name: "auth",
1107
+ status: "already-reverted",
1108
+ details: { path: result.path },
1109
+ };
1110
+ case "skipped":
1111
+ return {
1112
+ name: "auth",
1113
+ status: "skipped",
1114
+ details: { path: result.path, unexpected: true },
1115
+ };
1116
+ }
1117
+ }
1118
+ catch (err) {
1119
+ return {
1120
+ name: "auth",
1121
+ status: "error",
1122
+ details: { error: errMsg(err) },
1123
+ };
1124
+ }
1125
+ }
1126
+ async function runPackageStep(workingDir, opts, dryRun) {
1127
+ const primitive = opts.revertPackage ?? revertPackage;
1128
+ const spawnRemove = opts.spawnRemove;
1129
+ if (dryRun) {
1130
+ try {
1131
+ const result = await primitive(workingDir, {
1132
+ spawnRemove: NOOP_SPAWN,
1133
+ });
1134
+ return packageResultToStep(result, true);
1135
+ }
1136
+ catch (err) {
1137
+ return {
1138
+ name: "package",
1139
+ status: "error",
1140
+ details: { error: errMsg(err), dryRun: true },
1141
+ };
1142
+ }
1143
+ }
1144
+ try {
1145
+ const result = await primitive(workingDir, spawnRemove ? { spawnRemove } : {});
1146
+ return packageResultToStep(result, false);
1147
+ }
1148
+ catch (err) {
1149
+ return {
1150
+ name: "package",
1151
+ status: "error",
1152
+ details: { error: errMsg(err) },
1153
+ };
1154
+ }
1155
+ }
1156
+ function packageResultToStep(result, dryRun) {
1157
+ const dryRunTag = dryRun ? { dryRun: true } : {};
1158
+ switch (result.status) {
1159
+ case "removed":
1160
+ return {
1161
+ name: "package",
1162
+ status: "reverted",
1163
+ details: {
1164
+ packageManager: result.pm,
1165
+ removedPackages: result.removedPackages,
1166
+ command: result.command,
1167
+ ...dryRunTag,
1168
+ },
1169
+ };
1170
+ case "nothing-to-remove":
1171
+ return {
1172
+ name: "package",
1173
+ status: "already-reverted",
1174
+ details: { packageManager: result.pm, ...dryRunTag },
1175
+ };
1176
+ case "command-failed":
1177
+ return {
1178
+ name: "package",
1179
+ status: "error",
1180
+ details: {
1181
+ packageManager: result.pm,
1182
+ command: result.command,
1183
+ exitCode: result.exitCode,
1184
+ ...dryRunTag,
1185
+ },
1186
+ remediation: `${result.pm} ${result.command?.args.join(" ") ?? ""} exited ` +
1187
+ `with code ${result.exitCode}. Fix the error above and re-run ` +
1188
+ `\`npx @ceraph/react-native-mcp uninstall\`.`,
1189
+ };
1190
+ }
1191
+ }
1192
+ function errMsg(err) {
1193
+ return err instanceof Error ? err.message : String(err);
1194
+ }
1195
+ async function stageOrCleanup(tmpRoot, fn) {
1196
+ try {
1197
+ return await fn();
1198
+ }
1199
+ catch (err) {
1200
+ try {
1201
+ await rm(tmpRoot, { recursive: true, force: true });
1202
+ }
1203
+ catch {
1204
+ }
1205
+ throw err;
1206
+ }
1207
+ }
1208
+ async function stageSingleFileInTmp(absPath, projectDir) {
1209
+ const tmpRoot = join(tmpdir(), `ceraph-uninstall-boot-preview-${Date.now()}-${Math.random()
1210
+ .toString(36)
1211
+ .slice(2)}`);
1212
+ return stageOrCleanup(tmpRoot, async () => {
1213
+ const rel = relative(projectDir, absPath);
1214
+ const safeRel = isAbsolute(rel) ? "App.tsx" : rel;
1215
+ const dest = join(tmpRoot, safeRel);
1216
+ await mkdir(dirname(dest), { recursive: true });
1217
+ await copyFile(absPath, dest);
1218
+ return { projectDir: tmpRoot, tmpRoot };
1219
+ });
1220
+ }
1221
+ function finalize(workingDir, steps) {
1222
+ const warnings = [];
1223
+ const manualSteps = [];
1224
+ let reverted = 0;
1225
+ let skipped = 0;
1226
+ for (const step of steps) {
1227
+ if (step.status === "reverted")
1228
+ reverted++;
1229
+ else if (step.status === "skipped" || step.status === "already-reverted") {
1230
+ skipped++;
1231
+ }
1232
+ if (step.status === "warning" && step.remediation) {
1233
+ warnings.push(step.remediation);
1234
+ }
1235
+ if (step.status === "manual" && step.remediation) {
1236
+ manualSteps.push(step.remediation);
1237
+ }
1238
+ }
1239
+ return {
1240
+ workingDir,
1241
+ steps,
1242
+ summary: { reverted, skipped, warnings, manualSteps },
1243
+ };
1244
+ }