@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,185 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+ async function fileExists(path) {
4
+ try {
5
+ await stat(path);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ async function readJsonFile(path) {
13
+ try {
14
+ const raw = await readFile(path, "utf-8");
15
+ const parsed = JSON.parse(raw);
16
+ if (!parsed || typeof parsed !== "object")
17
+ return null;
18
+ return parsed;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ function packageHasRnDeps(pkg) {
25
+ if (!pkg)
26
+ return { expo: false, rn: false };
27
+ const dep = pkg.dependencies ?? {};
28
+ const dev = pkg.devDependencies ?? {};
29
+ const all = { ...dep, ...dev };
30
+ return {
31
+ expo: Boolean(all["expo"] || all["expo-router"]),
32
+ rn: Boolean(all["react-native"]),
33
+ };
34
+ }
35
+ async function readPnpmWorkspaceGlobs(projectDir) {
36
+ const file = join(projectDir, "pnpm-workspace.yaml");
37
+ let raw;
38
+ try {
39
+ raw = await readFile(file, "utf-8");
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ const globs = [];
45
+ let inPackages = false;
46
+ for (const line of raw.split(/\r?\n/)) {
47
+ const trimmed = line.replace(/#.*$/, "").trimEnd();
48
+ if (trimmed.length === 0)
49
+ continue;
50
+ if (/^packages\s*:\s*$/.test(trimmed)) {
51
+ inPackages = true;
52
+ continue;
53
+ }
54
+ if (!line.startsWith(" ") && !line.startsWith("-") && trimmed.endsWith(":")) {
55
+ inPackages = false;
56
+ continue;
57
+ }
58
+ if (!inPackages)
59
+ continue;
60
+ const m = trimmed.match(/^\s*-\s*['"]?([^'"]+?)['"]?\s*$/);
61
+ if (m && m[1])
62
+ globs.push(m[1]);
63
+ }
64
+ return globs;
65
+ }
66
+ async function expandGlob(projectDir, pattern) {
67
+ const cleaned = pattern.replace(/^\.\//, "").replace(/\/+$/, "");
68
+ if (cleaned.length === 0)
69
+ return [];
70
+ if (!cleaned.includes("*")) {
71
+ const abs = join(projectDir, cleaned);
72
+ const ok = await fileExists(abs);
73
+ return ok ? [abs] : [];
74
+ }
75
+ const m = cleaned.match(/^([^*]+)\/\*$/);
76
+ if (!m || !m[1])
77
+ return [];
78
+ const parentRel = m[1].replace(/\/+$/, "");
79
+ const parentAbs = join(projectDir, parentRel);
80
+ let entries;
81
+ try {
82
+ entries = await readdir(parentAbs, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ const out = [];
88
+ for (const entry of entries) {
89
+ if (!entry.isDirectory())
90
+ continue;
91
+ if (entry.name.startsWith("."))
92
+ continue;
93
+ out.push(join(parentAbs, entry.name));
94
+ }
95
+ return out;
96
+ }
97
+ export async function readWorkspaceGlobs(projectDir) {
98
+ const globs = new Set();
99
+ let foundManifest = false;
100
+ const rootPkg = await readJsonFile(join(projectDir, "package.json"));
101
+ if (rootPkg?.workspaces) {
102
+ foundManifest = true;
103
+ if (Array.isArray(rootPkg.workspaces)) {
104
+ for (const g of rootPkg.workspaces)
105
+ globs.add(g);
106
+ }
107
+ else if (rootPkg.workspaces &&
108
+ typeof rootPkg.workspaces === "object" &&
109
+ Array.isArray(rootPkg.workspaces.packages)) {
110
+ for (const g of rootPkg.workspaces.packages) {
111
+ globs.add(g);
112
+ }
113
+ }
114
+ }
115
+ const pnpmGlobs = await readPnpmWorkspaceGlobs(projectDir);
116
+ if (pnpmGlobs != null) {
117
+ foundManifest = true;
118
+ for (const g of pnpmGlobs)
119
+ globs.add(g);
120
+ }
121
+ const lernaJson = await readJsonFile(join(projectDir, "lerna.json"));
122
+ if (lernaJson) {
123
+ const lernaPkgs = lernaJson.packages;
124
+ if (Array.isArray(lernaPkgs)) {
125
+ foundManifest = true;
126
+ for (const g of lernaPkgs) {
127
+ if (typeof g === "string")
128
+ globs.add(g);
129
+ }
130
+ }
131
+ else if (await fileExists(join(projectDir, "lerna.json"))) {
132
+ foundManifest = true;
133
+ }
134
+ }
135
+ if (!foundManifest)
136
+ return null;
137
+ return Array.from(globs);
138
+ }
139
+ export async function detectMonorepoSubpackages(projectDir) {
140
+ const cwdPkg = await readJsonFile(join(projectDir, "package.json"));
141
+ const cwdHas = packageHasRnDeps(cwdPkg);
142
+ if (cwdHas.expo || cwdHas.rn) {
143
+ return {
144
+ kind: "none",
145
+ matches: [],
146
+ rootIsRnApp: true,
147
+ isMonorepo: (await readWorkspaceGlobs(projectDir)) != null,
148
+ };
149
+ }
150
+ const globs = await readWorkspaceGlobs(projectDir);
151
+ if (globs == null) {
152
+ return { kind: "none", matches: [], rootIsRnApp: false, isMonorepo: false };
153
+ }
154
+ const candidates = new Set();
155
+ for (const pattern of globs) {
156
+ for (const dir of await expandGlob(projectDir, pattern)) {
157
+ candidates.add(dir);
158
+ }
159
+ }
160
+ const matches = [];
161
+ for (const absPath of candidates) {
162
+ const pkgPath = join(absPath, "package.json");
163
+ const pkg = await readJsonFile(pkgPath);
164
+ const has = packageHasRnDeps(pkg);
165
+ const hasAppJson = await fileExists(join(absPath, "app.json"));
166
+ const signals = [];
167
+ if (has.expo)
168
+ signals.push("expo");
169
+ if (has.rn)
170
+ signals.push("react-native");
171
+ if (hasAppJson)
172
+ signals.push("app.json");
173
+ if (signals.length === 0)
174
+ continue;
175
+ const rel = relative(projectDir, absPath).split(sep).join("/");
176
+ matches.push({
177
+ absPath,
178
+ relPath: rel.length > 0 ? rel : ".",
179
+ signals,
180
+ });
181
+ }
182
+ matches.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
183
+ const kind = matches.length === 0 ? "none" : matches.length === 1 ? "single" : "multi";
184
+ return { kind, matches, rootIsRnApp: false, isMonorepo: true };
185
+ }
@@ -0,0 +1,52 @@
1
+ export interface StoredAuth {
2
+ accessToken: string;
3
+ refreshToken?: string;
4
+ expiresAt: number;
5
+ githubUser: {
6
+ login: string;
7
+ id: number;
8
+ };
9
+ probeSecretHash?: string;
10
+ storedAt: string;
11
+ }
12
+ export interface DeviceFlowStart {
13
+ deviceCode: string;
14
+ userCode: string;
15
+ verificationUri: string;
16
+ expiresIn: number;
17
+ intervalSeconds: number;
18
+ startedAt: number;
19
+ }
20
+ export type DeviceFlowPollResult = {
21
+ kind: "pending";
22
+ } | {
23
+ kind: "slow_down";
24
+ intervalSeconds: number;
25
+ } | {
26
+ kind: "expired";
27
+ } | {
28
+ kind: "denied";
29
+ } | {
30
+ kind: "success";
31
+ accessToken: string;
32
+ };
33
+ export interface OAuthDeps {
34
+ fetchFn?: typeof fetch;
35
+ clientId?: string;
36
+ }
37
+ export declare function hashToken(token: string): string;
38
+ export declare function startDeviceFlow(deps?: OAuthDeps): Promise<DeviceFlowStart>;
39
+ export declare function pollDeviceFlow(deviceCode: string, deps?: OAuthDeps): Promise<DeviceFlowPollResult>;
40
+ export declare function finalizeAndPersist(accessToken: string, deps?: OAuthDeps): Promise<StoredAuth>;
41
+ export declare function readExistingAuth(): Promise<StoredAuth | null>;
42
+ export interface PollUntilTokenOptions extends OAuthDeps {
43
+ nowFn?: () => number;
44
+ delayFn?: (ms: number) => Promise<void>;
45
+ onPrompt?: (codes: {
46
+ verificationUri: string;
47
+ userCode: string;
48
+ }) => void;
49
+ onTick?: (remainingSeconds: number) => void;
50
+ noBrowser?: boolean;
51
+ }
52
+ export declare function pollUntilToken(opts?: PollUntilTokenOptions): Promise<StoredAuth>;
@@ -0,0 +1,220 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, dirname } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { createHash } from "node:crypto";
6
+ import { platform } from "node:os";
7
+ const DEFAULT_CLIENT_ID = "Ov23lix6jqKVuLz3ydM4";
8
+ const GITHUB_API = "https://github.com";
9
+ const GITHUB_USER_API = "https://api.github.com/user";
10
+ const OAUTH_SCOPES = "read:user read:org repo";
11
+ function getClientId() {
12
+ return process.env.CERAPH_GITHUB_CLIENT_ID ?? DEFAULT_CLIENT_ID;
13
+ }
14
+ function getAuthFilePath() {
15
+ return join(homedir(), ".ceraph", "auth.json");
16
+ }
17
+ export function hashToken(token) {
18
+ return createHash("sha256").update(token, "utf8").digest("hex");
19
+ }
20
+ export async function startDeviceFlow(deps = {}) {
21
+ const fetchFn = deps.fetchFn ?? fetch;
22
+ const clientId = deps.clientId ?? getClientId();
23
+ const res = await fetchFn(`${GITHUB_API}/login/device/code`, {
24
+ method: "POST",
25
+ headers: {
26
+ "content-type": "application/x-www-form-urlencoded",
27
+ accept: "application/json",
28
+ },
29
+ body: `client_id=${encodeURIComponent(clientId)}&scope=${encodeURIComponent(OAUTH_SCOPES)}`,
30
+ });
31
+ if (!res.ok) {
32
+ throw new Error(`GitHub device code request failed: HTTP ${res.status}`);
33
+ }
34
+ const body = (await res.json());
35
+ return {
36
+ deviceCode: body.device_code,
37
+ userCode: body.user_code,
38
+ verificationUri: body.verification_uri,
39
+ expiresIn: body.expires_in,
40
+ intervalSeconds: Math.max(1, body.interval),
41
+ startedAt: Date.now(),
42
+ };
43
+ }
44
+ export async function pollDeviceFlow(deviceCode, deps = {}) {
45
+ const fetchFn = deps.fetchFn ?? fetch;
46
+ const clientId = deps.clientId ?? getClientId();
47
+ const res = await fetchFn(`${GITHUB_API}/login/oauth/access_token`, {
48
+ method: "POST",
49
+ headers: {
50
+ "content-type": "application/x-www-form-urlencoded",
51
+ accept: "application/json",
52
+ },
53
+ body: `client_id=${encodeURIComponent(clientId)}` +
54
+ `&device_code=${encodeURIComponent(deviceCode)}` +
55
+ `&grant_type=urn:ietf:params:oauth:grant-type:device_code`,
56
+ });
57
+ const body = (await res.json());
58
+ if (body.access_token) {
59
+ return { kind: "success", accessToken: body.access_token };
60
+ }
61
+ switch (body.error) {
62
+ case "authorization_pending":
63
+ return { kind: "pending" };
64
+ case "slow_down":
65
+ return { kind: "slow_down", intervalSeconds: 5 };
66
+ case "expired_token":
67
+ return { kind: "expired" };
68
+ case "access_denied":
69
+ return { kind: "denied" };
70
+ default:
71
+ return { kind: "expired" };
72
+ }
73
+ }
74
+ export async function finalizeAndPersist(accessToken, deps = {}) {
75
+ const fetchFn = deps.fetchFn ?? fetch;
76
+ const res = await fetchFn(GITHUB_USER_API, {
77
+ headers: {
78
+ authorization: `Bearer ${accessToken}`,
79
+ accept: "application/vnd.github+json",
80
+ "user-agent": "ceraph-react-native-mcp",
81
+ },
82
+ });
83
+ if (!res.ok) {
84
+ throw new Error(`GitHub /user returned HTTP ${res.status}`);
85
+ }
86
+ const user = (await res.json());
87
+ const stored = {
88
+ accessToken,
89
+ expiresAt: 0,
90
+ githubUser: { login: user.login, id: user.id },
91
+ probeSecretHash: hashToken(accessToken),
92
+ storedAt: new Date().toISOString(),
93
+ };
94
+ await writeAuthFile(stored);
95
+ return stored;
96
+ }
97
+ async function writeAuthFile(stored) {
98
+ const filePath = getAuthFilePath();
99
+ await fs.mkdir(dirname(filePath), { recursive: true });
100
+ const data = JSON.stringify(stored, null, 2) + "\n";
101
+ const tmpPath = `${filePath}.tmp.${process.pid}.${Math.random()
102
+ .toString(36)
103
+ .slice(2)}`;
104
+ try {
105
+ await fs.writeFile(tmpPath, data, { mode: 0o600 });
106
+ await fs.chmod(tmpPath, 0o600);
107
+ await fs.rename(tmpPath, filePath);
108
+ }
109
+ catch (err) {
110
+ try {
111
+ await fs.unlink(tmpPath);
112
+ }
113
+ catch {
114
+ }
115
+ throw err;
116
+ }
117
+ try {
118
+ await fs.chmod(dirname(filePath), 0o700);
119
+ }
120
+ catch {
121
+ }
122
+ }
123
+ export async function readExistingAuth() {
124
+ let raw;
125
+ try {
126
+ raw = await fs.readFile(getAuthFilePath(), "utf-8");
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ let parsed;
132
+ try {
133
+ parsed = JSON.parse(raw);
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ if (!isValidStoredAuth(parsed))
139
+ return null;
140
+ if (parsed.accessToken === "__keychain__")
141
+ return null;
142
+ if (parsed.expiresAt !== 0 && Date.now() >= parsed.expiresAt)
143
+ return null;
144
+ return parsed;
145
+ }
146
+ function isValidStoredAuth(value) {
147
+ if (!value || typeof value !== "object")
148
+ return false;
149
+ const v = value;
150
+ if (typeof v.accessToken !== "string" || v.accessToken.length === 0) {
151
+ return false;
152
+ }
153
+ if (typeof v.expiresAt !== "number")
154
+ return false;
155
+ if (!v.githubUser || typeof v.githubUser !== "object")
156
+ return false;
157
+ const user = v.githubUser;
158
+ if (typeof user.login !== "string" || typeof user.id !== "number")
159
+ return false;
160
+ if (typeof v.storedAt !== "string")
161
+ return false;
162
+ return true;
163
+ }
164
+ export async function pollUntilToken(opts = {}) {
165
+ const nowFn = opts.nowFn ?? (() => Date.now());
166
+ const delayFn = opts.delayFn ?? defaultDelay;
167
+ while (true) {
168
+ const start = await startDeviceFlow(opts);
169
+ opts.onPrompt?.({
170
+ verificationUri: start.verificationUri,
171
+ userCode: start.userCode,
172
+ });
173
+ if (!opts.noBrowser)
174
+ openBrowser(start.verificationUri);
175
+ const startedAt = nowFn();
176
+ const expiresAt = startedAt + start.expiresIn * 1000;
177
+ let intervalMs = start.intervalSeconds * 1000;
178
+ let restart = false;
179
+ while (nowFn() < expiresAt) {
180
+ const remaining = Math.max(0, Math.round((expiresAt - nowFn()) / 1000));
181
+ opts.onTick?.(remaining);
182
+ await delayFn(intervalMs);
183
+ const result = await pollDeviceFlow(start.deviceCode, opts);
184
+ if (result.kind === "success") {
185
+ return await finalizeAndPersist(result.accessToken, opts);
186
+ }
187
+ if (result.kind === "pending")
188
+ continue;
189
+ if (result.kind === "slow_down") {
190
+ await delayFn(result.intervalSeconds * 1000);
191
+ intervalMs = Math.max(intervalMs, result.intervalSeconds * 1000);
192
+ continue;
193
+ }
194
+ if (result.kind === "denied") {
195
+ throw new Error("GitHub authorization denied");
196
+ }
197
+ restart = true;
198
+ break;
199
+ }
200
+ if (!restart) {
201
+ restart = true;
202
+ }
203
+ }
204
+ }
205
+ async function defaultDelay(ms) {
206
+ return new Promise((r) => setTimeout(r, ms));
207
+ }
208
+ function openBrowser(url) {
209
+ try {
210
+ const p = platform();
211
+ const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
212
+ const args = p === "win32" ? ["/c", "start", "", url] : [url];
213
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
214
+ child.on("error", () => {
215
+ });
216
+ child.unref();
217
+ }
218
+ catch {
219
+ }
220
+ }
@@ -0,0 +1,11 @@
1
+ export type PackageManager = "pnpm" | "yarn" | "bun" | "npm";
2
+ export declare const LOCKFILES: ReadonlyArray<{
3
+ file: string;
4
+ pm: PackageManager;
5
+ }>;
6
+ export declare function detectPackageManager(projectDir: string): Promise<PackageManager>;
7
+ export declare function installArgv(pm: PackageManager, pkg?: string): {
8
+ bin: string;
9
+ args: string[];
10
+ };
11
+ export declare function isPackageInstalled(projectDir: string, pkg?: string): Promise<boolean>;
@@ -0,0 +1,60 @@
1
+ import { readFile, access } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export const LOCKFILES = [
4
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
5
+ { file: "yarn.lock", pm: "yarn" },
6
+ { file: "bun.lockb", pm: "bun" },
7
+ { file: "bun.lock", pm: "bun" },
8
+ { file: "package-lock.json", pm: "npm" },
9
+ ];
10
+ async function fileExists(path) {
11
+ try {
12
+ await access(path);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export async function detectPackageManager(projectDir) {
20
+ for (const { file, pm } of LOCKFILES) {
21
+ if (await fileExists(join(projectDir, file))) {
22
+ return pm;
23
+ }
24
+ }
25
+ return "npm";
26
+ }
27
+ export function installArgv(pm, pkg = "@ceraph/react-native-mcp") {
28
+ switch (pm) {
29
+ case "pnpm":
30
+ return { bin: "pnpm", args: ["add", pkg] };
31
+ case "yarn":
32
+ return { bin: "yarn", args: ["add", pkg] };
33
+ case "bun":
34
+ return { bin: "bun", args: ["add", pkg] };
35
+ case "npm":
36
+ return { bin: "npm", args: ["install", pkg] };
37
+ }
38
+ }
39
+ export async function isPackageInstalled(projectDir, pkg = "@ceraph/react-native-mcp") {
40
+ let raw;
41
+ try {
42
+ raw = await readFile(join(projectDir, "package.json"), "utf-8");
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(raw);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ if (!parsed || typeof parsed !== "object")
55
+ return false;
56
+ const pj = parsed;
57
+ const dep = pj.dependencies;
58
+ const dev = pj.devDependencies;
59
+ return Boolean(dep?.[pkg] || dev?.[pkg]);
60
+ }
@@ -0,0 +1,12 @@
1
+ export interface PromptDeps {
2
+ write?: (msg: string) => void;
3
+ ask?: (question: string) => Promise<string>;
4
+ nonInteractive?: boolean;
5
+ }
6
+ export declare function confirm(question: string, options?: {
7
+ defaultYes?: boolean;
8
+ } & PromptDeps): Promise<boolean>;
9
+ export type ReplaceChoice = "yes" | "no" | "show";
10
+ export declare function chooseReplaceAction(question: string, options?: PromptDeps): Promise<ReplaceChoice>;
11
+ export declare function print(msg: string, deps?: PromptDeps): void;
12
+ export declare function chooseFromList(question: string, options: ReadonlyArray<string>, deps?: PromptDeps): Promise<number | null>;
@@ -0,0 +1,68 @@
1
+ import { createInterface } from "node:readline";
2
+ function isNonInteractive(deps) {
3
+ if (deps.nonInteractive !== undefined)
4
+ return deps.nonInteractive;
5
+ if (process.env.CERAPH_NON_INTERACTIVE === "1")
6
+ return true;
7
+ return !process.stdin.isTTY;
8
+ }
9
+ function defaultAsk(question) {
10
+ return new Promise((resolve) => {
11
+ const rl = createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout,
14
+ });
15
+ rl.question(question, (answer) => {
16
+ rl.close();
17
+ resolve(answer);
18
+ });
19
+ });
20
+ }
21
+ export async function confirm(question, options = {}) {
22
+ const defaultYes = options.defaultYes ?? true;
23
+ if (isNonInteractive(options))
24
+ return defaultYes;
25
+ const ask = options.ask ?? defaultAsk;
26
+ const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
27
+ const answer = (await ask(question + suffix)).trim().toLowerCase();
28
+ if (answer.length === 0)
29
+ return defaultYes;
30
+ return answer === "y" || answer === "yes";
31
+ }
32
+ export async function chooseReplaceAction(question, options = {}) {
33
+ if (isNonInteractive(options))
34
+ return "yes";
35
+ const ask = options.ask ?? defaultAsk;
36
+ const answer = (await ask(question + " [Y/n/show diff] ")).trim().toLowerCase();
37
+ if (answer.length === 0)
38
+ return "yes";
39
+ if (answer.startsWith("s"))
40
+ return "show";
41
+ if (answer.startsWith("n"))
42
+ return "no";
43
+ return "yes";
44
+ }
45
+ export function print(msg, deps = {}) {
46
+ const write = deps.write ?? ((m) => process.stdout.write(m));
47
+ write(msg);
48
+ }
49
+ export async function chooseFromList(question, options, deps = {}) {
50
+ if (options.length === 0)
51
+ return null;
52
+ if (isNonInteractive(deps))
53
+ return 0;
54
+ const ask = deps.ask ?? defaultAsk;
55
+ const lines = [question, ""];
56
+ options.forEach((opt, i) => {
57
+ lines.push(` ${i + 1}. ${opt}`);
58
+ });
59
+ lines.push("");
60
+ const prompt = lines.join("\n") + `Select [1-${options.length}]: `;
61
+ const answer = (await ask(prompt)).trim();
62
+ if (answer.length === 0)
63
+ return 0;
64
+ const idx = Number.parseInt(answer, 10) - 1;
65
+ if (Number.isNaN(idx) || idx < 0 || idx >= options.length)
66
+ return null;
67
+ return idx;
68
+ }
@@ -0,0 +1,22 @@
1
+ export interface ShellProfileTarget {
2
+ path: string;
3
+ shell: "zsh" | "bash";
4
+ }
5
+ export declare function pickShellProfile(deps?: {
6
+ env?: NodeJS.ProcessEnv;
7
+ home?: string;
8
+ }): ShellProfileTarget;
9
+ export interface WriteSignalHostEnvResult {
10
+ path: string;
11
+ shell: "zsh" | "bash";
12
+ state: "added" | "updated" | "unchanged";
13
+ }
14
+ export interface WriteSignalHostEnvDeps {
15
+ readFile?: (path: string) => Promise<string>;
16
+ writeFile?: (path: string, content: string) => Promise<void>;
17
+ mkdir?: (path: string, opts: {
18
+ recursive: true;
19
+ }) => Promise<unknown>;
20
+ target?: ShellProfileTarget;
21
+ }
22
+ export declare function writeSignalHostEnv(ip: string, deps?: WriteSignalHostEnvDeps): Promise<WriteSignalHostEnvResult>;