@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.
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 +59 -5
@@ -0,0 +1,175 @@
1
+ import { spawn } from "node:child_process";
2
+ import { z } from "zod";
3
+ import { getInstalledFootprint } from "./footprint.js";
4
+ import { runUninstallWalkthrough, } from "./walkthrough.js";
5
+ const MAX_TOOL_PAYLOAD_BYTES = 64 * 1024;
6
+ const MODULE_LOAD_CWD = process.cwd();
7
+ function defaultCwd() {
8
+ return MODULE_LOAD_CWD;
9
+ }
10
+ function toolResult(payload) {
11
+ return {
12
+ content: [
13
+ { type: "text", text: JSON.stringify(payload, null, 2) },
14
+ ],
15
+ };
16
+ }
17
+ const MAX_STRING_FIELD_BYTES = 2 * 1024;
18
+ function truncateString(s, max) {
19
+ if (s.length <= max)
20
+ return s;
21
+ const head = s.slice(0, Math.floor(max / 2) - 16);
22
+ const tail = s.slice(s.length - (Math.floor(max / 2) - 16));
23
+ return `${head} … <${s.length - max} bytes truncated> … ${tail}`;
24
+ }
25
+ export function compactWalkthroughResult(result, maxBytes = MAX_TOOL_PAYLOAD_BYTES) {
26
+ const full = JSON.stringify(result, null, 2);
27
+ if (full.length <= maxBytes)
28
+ return result;
29
+ const compactSteps = result.steps.map((step) => {
30
+ if (step.details === undefined)
31
+ return step;
32
+ const detailsJson = JSON.stringify(step.details);
33
+ return {
34
+ ...step,
35
+ details: {
36
+ truncated: true,
37
+ originalSize: detailsJson.length,
38
+ },
39
+ };
40
+ });
41
+ const afterPass1 = {
42
+ ...result,
43
+ steps: compactSteps,
44
+ };
45
+ const pass1Json = JSON.stringify(afterPass1, null, 2);
46
+ if (pass1Json.length <= maxBytes)
47
+ return afterPass1;
48
+ let lo = 32;
49
+ let hi = MAX_STRING_FIELD_BYTES;
50
+ let best = afterPass1;
51
+ while (lo <= hi) {
52
+ const mid = Math.floor((lo + hi) / 2);
53
+ const candidate = {
54
+ ...afterPass1,
55
+ workingDir: truncateString(afterPass1.workingDir, mid),
56
+ steps: afterPass1.steps.map((step) => ({
57
+ ...step,
58
+ remediation: step.remediation
59
+ ? truncateString(step.remediation, mid)
60
+ : step.remediation,
61
+ })),
62
+ summary: {
63
+ ...afterPass1.summary,
64
+ warnings: afterPass1.summary.warnings.map((w) => truncateString(w, mid)),
65
+ manualSteps: afterPass1.summary.manualSteps.map((m) => truncateString(m, mid)),
66
+ },
67
+ };
68
+ const size = JSON.stringify(candidate, null, 2).length;
69
+ if (size <= maxBytes) {
70
+ best = candidate;
71
+ lo = mid + 1;
72
+ }
73
+ else {
74
+ hi = mid - 1;
75
+ }
76
+ }
77
+ return best;
78
+ }
79
+ function toolError(toolName, err) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: `${toolName} failed: ${err instanceof Error ? err.message : String(err)}`,
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ function bufferedSpawnRemove(input) {
91
+ return new Promise((resolve) => {
92
+ const child = spawn(input.bin, input.args, {
93
+ cwd: input.cwd,
94
+ stdio: ["ignore", "pipe", "pipe"],
95
+ });
96
+ child.stdout?.on("data", () => undefined);
97
+ child.stderr?.on("data", () => undefined);
98
+ child.on("error", () => resolve({ exitCode: 127 }));
99
+ child.on("exit", (code) => resolve({ exitCode: code ?? 1 }));
100
+ });
101
+ }
102
+ export function registerUninstallTools(server) {
103
+ server.tool("ceraph_uninstall_status", "Snapshot what Ceraph has installed in this project — package " +
104
+ "presence, gitignore entries, MCP-client configs, root-layout " +
105
+ "boot wiring, <CeraphCamera> usage count, and whether the user-" +
106
+ "global auth token is present. Use this BEFORE ceraph_uninstall_run " +
107
+ "to show the user what would change. Returns the footprint without " +
108
+ "mutating anything.", {
109
+ projectDir: z
110
+ .string()
111
+ .optional()
112
+ .describe("Project directory to inspect. Defaults to the MCP server's " +
113
+ "cwd. In a monorepo with multiple RN subpackages, the tool " +
114
+ "returns `monorepoConflict` listing each candidate — pass " +
115
+ "the chosen subpackage's path here to disambiguate."),
116
+ }, async (args) => {
117
+ try {
118
+ const projectDir = args.projectDir ?? defaultCwd();
119
+ const footprint = await getInstalledFootprint(projectDir);
120
+ return toolResult(footprint);
121
+ }
122
+ catch (err) {
123
+ return toolError("ceraph_uninstall_status", err);
124
+ }
125
+ });
126
+ server.tool("ceraph_uninstall_run", "Uninstall Ceraph from this project. Reverts source-tree edits " +
127
+ "(<CeraphCamera> → <CameraView>, installCeraph() boot wiring, " +
128
+ "ceraph URL scheme), strips MCP client configs, removes gitignore " +
129
+ "entries + the shell signal-host export, and uninstalls the " +
130
+ "package via the project's package manager. Default leaves " +
131
+ "`.ceraph/` (test images + snapshots) and `~/.ceraph/auth.json` " +
132
+ "(user-global OAuth token) untouched. Pass `purgeImages` and " +
133
+ "`global` to nuke those too. Always call with `dryRun: true` " +
134
+ "first to preview the step list before a real run.", {
135
+ projectDir: z
136
+ .string()
137
+ .optional()
138
+ .describe("Project directory to operate on. Defaults to the MCP " +
139
+ "server's cwd. Required to disambiguate monorepo " +
140
+ "multi-match."),
141
+ purgeImages: z
142
+ .boolean()
143
+ .optional()
144
+ .describe("If true, also delete `.ceraph/` (camera test images + " +
145
+ "design snapshots + run artifacts). Default false."),
146
+ global: z
147
+ .boolean()
148
+ .optional()
149
+ .describe("If true, also delete `~/.ceraph/auth.json` (the user-global " +
150
+ "OAuth token; this logs the user out of every Ceraph " +
151
+ "install on this machine). Default false."),
152
+ dryRun: z
153
+ .boolean()
154
+ .optional()
155
+ .describe("If true, return the step list as a preview without " +
156
+ "mutating anything. Use this first to show the user what " +
157
+ "would happen."),
158
+ }, async (args) => {
159
+ try {
160
+ const projectDir = args.projectDir ?? defaultCwd();
161
+ const result = await runUninstallWalkthrough({
162
+ projectDir,
163
+ purgeImages: args.purgeImages === true,
164
+ global: args.global === true,
165
+ dryRun: args.dryRun === true,
166
+ nonInteractive: true,
167
+ spawnRemove: bufferedSpawnRemove,
168
+ });
169
+ return toolResult(compactWalkthroughResult(result));
170
+ }
171
+ catch (err) {
172
+ return toolError("ceraph_uninstall_run", err);
173
+ }
174
+ });
175
+ }
@@ -0,0 +1,22 @@
1
+ export type RevertAuthResult = {
2
+ status: "skipped";
3
+ path: string;
4
+ note: string;
5
+ } | {
6
+ status: "deleted";
7
+ path: string;
8
+ } | {
9
+ status: "already-reverted";
10
+ path: string;
11
+ };
12
+ export interface RevertAuthInput {
13
+ global: boolean;
14
+ }
15
+ export interface RevertAuthDeps {
16
+ unlink?: (p: string) => Promise<void>;
17
+ home?: string;
18
+ }
19
+ export declare function authJsonPath(deps?: {
20
+ home?: string;
21
+ }): string;
22
+ export declare function revertAuth(input: RevertAuthInput, deps?: RevertAuthDeps): Promise<RevertAuthResult>;
@@ -0,0 +1,31 @@
1
+ import { unlink } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function authJsonPath(deps = {}) {
5
+ return join(deps.home ?? homedir(), ".ceraph", "auth.json");
6
+ }
7
+ export async function revertAuth(input, deps = {}) {
8
+ const path = authJsonPath({ home: deps.home });
9
+ const unlinkFn = deps.unlink ?? ((p) => unlink(p));
10
+ if (!input.global) {
11
+ return {
12
+ status: "skipped",
13
+ path,
14
+ note: "Pass global: true to remove ~/.ceraph/auth.json — the token is user-global and removing it logs you out of every Ceraph install.",
15
+ };
16
+ }
17
+ try {
18
+ await unlinkFn(path);
19
+ return { status: "deleted", path };
20
+ }
21
+ catch (err) {
22
+ const isMissing = typeof err === "object" &&
23
+ err !== null &&
24
+ "code" in err &&
25
+ err.code === "ENOENT";
26
+ if (isMissing) {
27
+ return { status: "already-reverted", path };
28
+ }
29
+ throw err;
30
+ }
31
+ }
@@ -0,0 +1,24 @@
1
+ export type RevertBootTarget = {
2
+ kind: "expo-router";
3
+ filePath: string;
4
+ } | {
5
+ kind: "bare-rn";
6
+ filePath: string;
7
+ };
8
+ export type RevertBootResult = {
9
+ status: "reverted";
10
+ target: RevertBootTarget;
11
+ removedUseEffect: boolean;
12
+ removedImport: boolean;
13
+ } | {
14
+ status: "already-reverted";
15
+ target: RevertBootTarget;
16
+ } | {
17
+ status: "skipped";
18
+ target: RevertBootTarget;
19
+ reason: string;
20
+ } | {
21
+ status: "no-layout-found";
22
+ };
23
+ export declare function detectRevertTarget(projectDir: string): Promise<RevertBootTarget | null>;
24
+ export declare function revertBoot(projectDir: string): Promise<RevertBootResult>;
@@ -0,0 +1,242 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { IndentationText, Node, Project, SyntaxKind, } from "ts-morph";
4
+ const SHIM_MODULE = "@ceraph/react-native-mcp/shim";
5
+ const INSTALL_CALL = "installCeraph";
6
+ async function fileExists(path) {
7
+ try {
8
+ await readFile(path, "utf-8");
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export async function detectRevertTarget(projectDir) {
16
+ const expo = join(projectDir, "app", "_layout.tsx");
17
+ if (await fileExists(expo)) {
18
+ return { kind: "expo-router", filePath: expo };
19
+ }
20
+ const expoJsx = join(projectDir, "app", "_layout.jsx");
21
+ if (await fileExists(expoJsx)) {
22
+ return { kind: "expo-router", filePath: expoJsx };
23
+ }
24
+ for (const name of ["App.tsx", "App.jsx"]) {
25
+ const p = join(projectDir, name);
26
+ if (await fileExists(p)) {
27
+ return { kind: "bare-rn", filePath: p };
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ function findDefaultExportFunction(source) {
33
+ for (const fn of source.getFunctions()) {
34
+ if (fn.isDefaultExport())
35
+ return fn;
36
+ }
37
+ const exportAssignment = source
38
+ .getStatements()
39
+ .find((s) => Node.isExportAssignment(s));
40
+ if (exportAssignment && Node.isExportAssignment(exportAssignment)) {
41
+ const expr = exportAssignment.getExpression();
42
+ if (Node.isIdentifier(expr)) {
43
+ const symbol = expr.getSymbol();
44
+ if (symbol) {
45
+ const decl = symbol.getDeclarations()[0];
46
+ if (decl && Node.isVariableDeclaration(decl)) {
47
+ const init = decl.getInitializer();
48
+ if (init && Node.isArrowFunction(init))
49
+ return init;
50
+ if (init && Node.isFunctionExpression(init))
51
+ return init;
52
+ }
53
+ }
54
+ }
55
+ if (Node.isArrowFunction(expr))
56
+ return expr;
57
+ if (Node.isFunctionExpression(expr))
58
+ return expr;
59
+ }
60
+ return null;
61
+ }
62
+ function isSoloInstallCeraphStatement(stmt) {
63
+ if (Node.isExpressionStatement(stmt)) {
64
+ const expr = stmt.getExpression();
65
+ if (Node.isCallExpression(expr)) {
66
+ const callee = expr.getExpression();
67
+ if (Node.isIdentifier(callee) && callee.getText() === INSTALL_CALL) {
68
+ return true;
69
+ }
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ function classifyUseEffect(call) {
75
+ const args = call.getArguments();
76
+ if (args.length === 0)
77
+ return "unrelated";
78
+ const callback = args[0];
79
+ let body = null;
80
+ if (Node.isArrowFunction(callback) || Node.isFunctionExpression(callback)) {
81
+ const b = callback.getBody();
82
+ if (Node.isBlock(b))
83
+ body = b;
84
+ else {
85
+ if (Node.isCallExpression(b)) {
86
+ const callee = b.getExpression();
87
+ if (Node.isIdentifier(callee) && callee.getText() === INSTALL_CALL) {
88
+ return "installCeraph-only";
89
+ }
90
+ }
91
+ return "unrelated";
92
+ }
93
+ }
94
+ else {
95
+ return "unrelated";
96
+ }
97
+ if (!body)
98
+ return "unrelated";
99
+ const statements = body.getStatements();
100
+ if (statements.length === 0)
101
+ return "unrelated";
102
+ let hasInstallCeraph = false;
103
+ let hasOthers = false;
104
+ for (const s of statements) {
105
+ if (isSoloInstallCeraphStatement(s)) {
106
+ hasInstallCeraph = true;
107
+ }
108
+ else {
109
+ hasOthers = true;
110
+ }
111
+ }
112
+ if (!hasInstallCeraph)
113
+ return "unrelated";
114
+ return hasOthers ? "installCeraph-with-others" : "installCeraph-only";
115
+ }
116
+ function listUseEffectsInBody(fn) {
117
+ const block = fn.getBody();
118
+ if (!block || !Node.isBlock(block))
119
+ return [];
120
+ const out = [];
121
+ block.forEachDescendant((node) => {
122
+ if (Node.isCallExpression(node)) {
123
+ const callee = node.getExpression();
124
+ if (Node.isIdentifier(callee) && callee.getText() === "useEffect") {
125
+ out.push(node);
126
+ }
127
+ }
128
+ });
129
+ return out;
130
+ }
131
+ function dropUseEffectImportIfOrphan(source) {
132
+ const decl = source.getImportDeclaration("react");
133
+ if (!decl)
134
+ return false;
135
+ const named = decl.getNamedImports();
136
+ const target = named.find((n) => n.getName() === "useEffect");
137
+ if (!target)
138
+ return false;
139
+ const aliasNode = target.getAliasNode();
140
+ if (aliasNode)
141
+ return false;
142
+ let stillReferenced = false;
143
+ source.forEachDescendant((node) => {
144
+ if (stillReferenced)
145
+ return;
146
+ if (Node.isIdentifier(node) && node.getText() === "useEffect") {
147
+ const parent = node.getParent();
148
+ if (parent && (Node.isImportSpecifier(parent) || Node.isImportClause(parent))) {
149
+ return;
150
+ }
151
+ stillReferenced = true;
152
+ }
153
+ });
154
+ if (stillReferenced)
155
+ return false;
156
+ target.remove();
157
+ const remaining = decl.getNamedImports();
158
+ const hasDefault = decl.getDefaultImport() != null;
159
+ const hasNamespace = decl.getNamespaceImport() != null;
160
+ if (remaining.length === 0 && !hasDefault && !hasNamespace) {
161
+ decl.remove();
162
+ }
163
+ return true;
164
+ }
165
+ function removeInstallCeraphImport(source) {
166
+ const decl = source.getImportDeclaration(SHIM_MODULE);
167
+ if (!decl)
168
+ return false;
169
+ const named = decl.getNamedImports();
170
+ const target = named.find((n) => n.getName() === INSTALL_CALL);
171
+ if (!target)
172
+ return false;
173
+ target.remove();
174
+ const remaining = decl.getNamedImports();
175
+ const hasDefault = decl.getDefaultImport() != null;
176
+ if (remaining.length === 0 && !hasDefault) {
177
+ decl.remove();
178
+ }
179
+ return true;
180
+ }
181
+ export async function revertBoot(projectDir) {
182
+ const target = await detectRevertTarget(projectDir);
183
+ if (!target) {
184
+ return { status: "no-layout-found" };
185
+ }
186
+ const project = new Project({
187
+ useInMemoryFileSystem: false,
188
+ skipAddingFilesFromTsConfig: true,
189
+ skipFileDependencyResolution: true,
190
+ skipLoadingLibFiles: true,
191
+ compilerOptions: { allowJs: true, jsx: 1 },
192
+ manipulationSettings: { indentationText: IndentationText.TwoSpaces },
193
+ });
194
+ const source = project.addSourceFileAtPath(target.filePath);
195
+ const fn = findDefaultExportFunction(source);
196
+ if (!fn) {
197
+ const removedImport = removeInstallCeraphImport(source);
198
+ if (removedImport) {
199
+ await writeFile(target.filePath, source.getFullText(), "utf-8");
200
+ return {
201
+ status: "reverted",
202
+ target,
203
+ removedUseEffect: false,
204
+ removedImport: true,
205
+ };
206
+ }
207
+ return { status: "already-reverted", target };
208
+ }
209
+ const useEffects = listUseEffectsInBody(fn);
210
+ const candidates = useEffects.map((u) => ({ call: u, kind: classifyUseEffect(u) }));
211
+ const soloCandidates = candidates.filter((c) => c.kind === "installCeraph-only");
212
+ const mixedCandidates = candidates.filter((c) => c.kind === "installCeraph-with-others");
213
+ if (mixedCandidates.length > 0) {
214
+ return {
215
+ status: "skipped",
216
+ target,
217
+ reason: "useEffect contains other statements",
218
+ };
219
+ }
220
+ let removedUseEffect = false;
221
+ for (const c of soloCandidates) {
222
+ const stmt = c.call.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
223
+ if (stmt) {
224
+ stmt.remove();
225
+ removedUseEffect = true;
226
+ }
227
+ }
228
+ const removedImport = removeInstallCeraphImport(source);
229
+ if (removedUseEffect) {
230
+ dropUseEffectImportIfOrphan(source);
231
+ }
232
+ if (!removedUseEffect && !removedImport) {
233
+ return { status: "already-reverted", target };
234
+ }
235
+ await writeFile(target.filePath, source.getFullText(), "utf-8");
236
+ return {
237
+ status: "reverted",
238
+ target,
239
+ removedUseEffect,
240
+ removedImport,
241
+ };
242
+ }
@@ -0,0 +1,12 @@
1
+ export interface CameraFileRevertResult {
2
+ path: string;
3
+ relPath: string;
4
+ ceraphCamerasRemoved: number;
5
+ importDropped: boolean;
6
+ importRestored: boolean;
7
+ }
8
+ export interface RevertCameraResult {
9
+ files: CameraFileRevertResult[];
10
+ filesScanned: number;
11
+ }
12
+ export declare function revertCamera(projectDir: string): Promise<RevertCameraResult>;