@ceraph/react-native-mcp 0.2.2 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +116 -15
- package/README.md +79 -77
- package/assets/default.png +0 -0
- package/dist/app-lifecycle.d.ts +50 -0
- package/dist/app-lifecycle.js +487 -0
- package/dist/camera-image-writer.d.ts +43 -0
- package/dist/camera-image-writer.js +280 -0
- package/dist/camera-registry-sync.d.ts +18 -0
- package/dist/camera-registry-sync.js +117 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +41 -9
- package/dist/device-autonomy.d.ts +30 -0
- package/dist/device-autonomy.js +117 -0
- package/dist/error-parser.d.ts +6 -26
- package/dist/error-parser.js +4 -74
- package/dist/expo-manager.d.ts +2 -74
- package/dist/expo-manager.js +11 -125
- package/dist/index.d.ts +0 -7
- package/dist/index.js +1266 -56
- package/dist/init/ast-camera.d.ts +29 -0
- package/dist/init/ast-camera.js +267 -0
- package/dist/init/ast-layout.d.ts +15 -0
- package/dist/init/ast-layout.js +167 -0
- package/dist/init/claude-hook-constants.d.ts +9 -0
- package/dist/init/claude-hook-constants.js +91 -0
- package/dist/init/lan-ip.d.ts +11 -0
- package/dist/init/lan-ip.js +51 -0
- package/dist/init/monorepo.d.ts +13 -0
- package/dist/init/monorepo.js +185 -0
- package/dist/init/oauth.d.ts +52 -0
- package/dist/init/oauth.js +220 -0
- package/dist/init/package-manager.d.ts +11 -0
- package/dist/init/package-manager.js +60 -0
- package/dist/init/prompt.d.ts +12 -0
- package/dist/init/prompt.js +68 -0
- package/dist/init/shell-profile.d.ts +22 -0
- package/dist/init/shell-profile.js +85 -0
- package/dist/init/steps.d.ts +135 -0
- package/dist/init/steps.js +399 -0
- package/dist/init/url-scheme.d.ts +42 -0
- package/dist/init/url-scheme.js +187 -0
- package/dist/init/walkthrough.d.ts +76 -0
- package/dist/init/walkthrough.js +340 -0
- package/dist/init.d.ts +7 -7
- package/dist/init.js +280 -120
- package/dist/iproxy-manager.d.ts +32 -0
- package/dist/iproxy-manager.js +216 -0
- package/dist/mac-caffeinate.d.ts +10 -0
- package/dist/mac-caffeinate.js +56 -0
- package/dist/permission-interceptor.d.ts +29 -0
- package/dist/permission-interceptor.js +185 -0
- package/dist/prebuild-detector.d.ts +0 -30
- package/dist/prebuild-detector.js +1 -42
- package/dist/preflight.d.ts +34 -0
- package/dist/preflight.js +847 -0
- package/dist/screen.d.ts +132 -43
- package/dist/screen.js +668 -94
- package/dist/shim/boot.d.ts +41 -0
- package/dist/shim/boot.js +141 -0
- package/dist/shim/camera.d.ts +22 -0
- package/dist/shim/camera.js +62 -0
- package/dist/shim/config.d.ts +6 -0
- package/dist/shim/config.js +56 -0
- package/dist/shim/deep-link.d.ts +1 -0
- package/dist/shim/deep-link.js +25 -0
- package/dist/shim/dev-guard.d.ts +1 -0
- package/dist/shim/dev-guard.js +3 -0
- package/dist/shim/error-handler.d.ts +20 -0
- package/dist/shim/error-handler.js +66 -0
- package/dist/shim/fetch-interceptor.d.ts +13 -0
- package/dist/shim/fetch-interceptor.js +93 -0
- package/dist/shim/index.d.ts +6 -0
- package/dist/shim/index.js +6 -0
- package/dist/shim/keep-awake.d.ts +13 -0
- package/dist/shim/keep-awake.js +118 -0
- package/dist/shim/reload.d.ts +23 -0
- package/dist/shim/reload.js +76 -0
- package/dist/shim/signal-capture.d.ts +11 -0
- package/dist/shim/signal-capture.js +15 -0
- package/dist/shim/signal-transport.d.ts +17 -0
- package/dist/shim/signal-transport.js +43 -0
- package/dist/signal-listener.d.ts +27 -0
- package/dist/signal-listener.js +135 -0
- package/dist/simulator-boot.d.ts +52 -0
- package/dist/simulator-boot.js +227 -0
- package/dist/target.d.ts +48 -0
- package/dist/target.js +267 -0
- package/dist/uninstall/cli-runner.d.ts +32 -0
- package/dist/uninstall/cli-runner.js +223 -0
- package/dist/uninstall/footprint.d.ts +40 -0
- package/dist/uninstall/footprint.js +288 -0
- package/dist/uninstall/mcp-tools.d.ts +14 -0
- package/dist/uninstall/mcp-tools.js +175 -0
- package/dist/uninstall/revert-auth.d.ts +22 -0
- package/dist/uninstall/revert-auth.js +31 -0
- package/dist/uninstall/revert-boot.d.ts +24 -0
- package/dist/uninstall/revert-boot.js +242 -0
- package/dist/uninstall/revert-camera.d.ts +12 -0
- package/dist/uninstall/revert-camera.js +199 -0
- package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
- package/dist/uninstall/revert-ceraph-dir.js +38 -0
- package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
- package/dist/uninstall/revert-claude-hooks.js +191 -0
- package/dist/uninstall/revert-gitignore.d.ts +17 -0
- package/dist/uninstall/revert-gitignore.js +43 -0
- package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
- package/dist/uninstall/revert-mcp-clients.js +194 -0
- package/dist/uninstall/revert-package.d.ts +34 -0
- package/dist/uninstall/revert-package.js +98 -0
- package/dist/uninstall/revert-scheme.d.ts +36 -0
- package/dist/uninstall/revert-scheme.js +139 -0
- package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
- package/dist/uninstall/revert-signal-host-env.js +61 -0
- package/dist/uninstall/walkthrough.d.ts +80 -0
- package/dist/uninstall/walkthrough.js +1244 -0
- package/dist/utils/atomic-write.d.ts +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/wait-for-device.d.ts +68 -0
- package/dist/wait-for-device.js +368 -0
- package/dist/wda-manager.d.ts +38 -0
- package/dist/wda-manager.js +186 -0
- package/dist/wda-simulator.d.ts +28 -0
- package/dist/wda-simulator.js +257 -0
- package/package.json +59 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const DEFAULT_LOCAL = 8100;
|
|
3
|
+
const DEFAULT_DEVICE = 8100;
|
|
4
|
+
const MAX_LOG_LINES = 50;
|
|
5
|
+
function defaultWhich(bin) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let stdout = "";
|
|
8
|
+
let settled = false;
|
|
9
|
+
const child = spawn("which", [bin], {
|
|
10
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11
|
+
});
|
|
12
|
+
child.stdout.on("data", (chunk) => {
|
|
13
|
+
stdout += chunk.toString();
|
|
14
|
+
});
|
|
15
|
+
child.on("error", () => {
|
|
16
|
+
if (!settled) {
|
|
17
|
+
settled = true;
|
|
18
|
+
resolve(null);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
child.on("exit", (code) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
if (code !== 0)
|
|
26
|
+
resolve(null);
|
|
27
|
+
else {
|
|
28
|
+
const trimmed = stdout.trim();
|
|
29
|
+
resolve(trimmed.length > 0 ? trimmed : null);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export class IproxyManager {
|
|
35
|
+
child = null;
|
|
36
|
+
udid = null;
|
|
37
|
+
localPort;
|
|
38
|
+
devicePort;
|
|
39
|
+
stderrBuffer = [];
|
|
40
|
+
spawnFn;
|
|
41
|
+
whichFn;
|
|
42
|
+
startInFlight = null;
|
|
43
|
+
constructor(opts = {}) {
|
|
44
|
+
this.localPort = opts.localPort ?? DEFAULT_LOCAL;
|
|
45
|
+
this.devicePort = opts.devicePort ?? DEFAULT_DEVICE;
|
|
46
|
+
this.spawnFn = opts.spawnFn ?? spawn;
|
|
47
|
+
this.whichFn = opts.whichFn ?? defaultWhich;
|
|
48
|
+
}
|
|
49
|
+
isRunning() {
|
|
50
|
+
return this.child !== null && this.child.exitCode === null;
|
|
51
|
+
}
|
|
52
|
+
async start(udid) {
|
|
53
|
+
while (true) {
|
|
54
|
+
if (this.isRunning()) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: "already-running",
|
|
58
|
+
pid: this.child?.pid,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!this.startInFlight)
|
|
62
|
+
break;
|
|
63
|
+
await this.startInFlight.catch(() => undefined);
|
|
64
|
+
}
|
|
65
|
+
const myInFlight = this.startInner(udid);
|
|
66
|
+
this.startInFlight = myInFlight;
|
|
67
|
+
try {
|
|
68
|
+
return await myInFlight;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (this.startInFlight === myInFlight) {
|
|
72
|
+
this.startInFlight = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async startInner(udid) {
|
|
77
|
+
if (this.isRunning()) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: "already-running",
|
|
81
|
+
pid: this.child?.pid,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const binPath = await this.whichFn("iproxy");
|
|
85
|
+
if (!binPath) {
|
|
86
|
+
return { ok: false, reason: "not-installed" };
|
|
87
|
+
}
|
|
88
|
+
this.udid = udid;
|
|
89
|
+
this.stderrBuffer = [];
|
|
90
|
+
let child;
|
|
91
|
+
try {
|
|
92
|
+
child = this.spawnFn(binPath, [
|
|
93
|
+
String(this.localPort),
|
|
94
|
+
String(this.devicePort),
|
|
95
|
+
"-u",
|
|
96
|
+
udid,
|
|
97
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
this.appendStderr(`iproxy spawn threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
+
return { ok: false, reason: "spawn-failed", stderr: [...this.stderrBuffer] };
|
|
102
|
+
}
|
|
103
|
+
child.stderr?.on("data", (chunk) => {
|
|
104
|
+
const text = chunk.toString();
|
|
105
|
+
for (const line of text.split(/\r?\n/)) {
|
|
106
|
+
if (line.trim().length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
this.appendStderr(line);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
child.stdout?.on("data", (chunk) => {
|
|
112
|
+
const text = chunk.toString();
|
|
113
|
+
for (const line of text.split(/\r?\n/)) {
|
|
114
|
+
if (line.trim().length === 0)
|
|
115
|
+
continue;
|
|
116
|
+
this.appendStderr(`[stdout] ${line}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const earlyExit = await this.detectEarlyExit(child, 50);
|
|
120
|
+
if (earlyExit) {
|
|
121
|
+
this.child = null;
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: "spawn-failed",
|
|
125
|
+
stderr: [...this.stderrBuffer],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
this.child = child;
|
|
129
|
+
child.on("exit", () => {
|
|
130
|
+
if (this.child === child) {
|
|
131
|
+
this.child = null;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
pid: child.pid,
|
|
137
|
+
stderr: [...this.stderrBuffer],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async stop() {
|
|
141
|
+
if (this.startInFlight) {
|
|
142
|
+
await this.startInFlight.catch(() => undefined);
|
|
143
|
+
}
|
|
144
|
+
const child = this.child;
|
|
145
|
+
if (!child)
|
|
146
|
+
return;
|
|
147
|
+
this.child = null;
|
|
148
|
+
if (child.exitCode !== null)
|
|
149
|
+
return;
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
let settled = false;
|
|
152
|
+
let killTimer = null;
|
|
153
|
+
let fallbackTimer = null;
|
|
154
|
+
const finish = () => {
|
|
155
|
+
if (settled)
|
|
156
|
+
return;
|
|
157
|
+
settled = true;
|
|
158
|
+
if (killTimer)
|
|
159
|
+
clearTimeout(killTimer);
|
|
160
|
+
if (fallbackTimer)
|
|
161
|
+
clearTimeout(fallbackTimer);
|
|
162
|
+
resolve();
|
|
163
|
+
};
|
|
164
|
+
child.once("exit", finish);
|
|
165
|
+
try {
|
|
166
|
+
child.kill("SIGTERM");
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
finish();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
killTimer = setTimeout(() => {
|
|
173
|
+
if (settled)
|
|
174
|
+
return;
|
|
175
|
+
try {
|
|
176
|
+
child.kill("SIGKILL");
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
}
|
|
180
|
+
fallbackTimer = setTimeout(finish, 100);
|
|
181
|
+
fallbackTimer.unref();
|
|
182
|
+
}, 1000);
|
|
183
|
+
killTimer.unref();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async restart(udid) {
|
|
187
|
+
await this.stop();
|
|
188
|
+
return this.start(udid ?? this.udid ?? "");
|
|
189
|
+
}
|
|
190
|
+
getStderr() {
|
|
191
|
+
return [...this.stderrBuffer];
|
|
192
|
+
}
|
|
193
|
+
appendStderr(line) {
|
|
194
|
+
this.stderrBuffer.push(line);
|
|
195
|
+
if (this.stderrBuffer.length > MAX_LOG_LINES) {
|
|
196
|
+
this.stderrBuffer.shift();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
detectEarlyExit(child, withinMs) {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
let settled = false;
|
|
202
|
+
const finish = (value) => {
|
|
203
|
+
if (settled)
|
|
204
|
+
return;
|
|
205
|
+
settled = true;
|
|
206
|
+
resolve(value);
|
|
207
|
+
};
|
|
208
|
+
const onExit = () => finish(true);
|
|
209
|
+
child.once("exit", onExit);
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
child.off("exit", onExit);
|
|
212
|
+
finish(false);
|
|
213
|
+
}, withinMs).unref();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
export interface CaffeinateResult {
|
|
3
|
+
applied: boolean;
|
|
4
|
+
reason: "started" | "already-active" | "not-darwin" | "spawn-failed";
|
|
5
|
+
}
|
|
6
|
+
export type Spawner = typeof nodeSpawn;
|
|
7
|
+
export declare function _resetForTesting(): void;
|
|
8
|
+
export declare function _setSpawnerForTesting(fn: Spawner | null): void;
|
|
9
|
+
export declare function enableCaffeinate(): CaffeinateResult;
|
|
10
|
+
export declare function disableCaffeinate(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
let child = null;
|
|
3
|
+
let active = false;
|
|
4
|
+
let spawnerOverride = null;
|
|
5
|
+
export function _resetForTesting() {
|
|
6
|
+
child = null;
|
|
7
|
+
active = false;
|
|
8
|
+
spawnerOverride = null;
|
|
9
|
+
}
|
|
10
|
+
export function _setSpawnerForTesting(fn) {
|
|
11
|
+
spawnerOverride = fn;
|
|
12
|
+
}
|
|
13
|
+
export function enableCaffeinate() {
|
|
14
|
+
if (process.platform !== "darwin") {
|
|
15
|
+
return { applied: false, reason: "not-darwin" };
|
|
16
|
+
}
|
|
17
|
+
if (active && child && !child.killed) {
|
|
18
|
+
return { applied: true, reason: "already-active" };
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const spawnFn = spawnerOverride ?? nodeSpawn;
|
|
22
|
+
const proc = spawnFn("caffeinate", ["-di", "-w", String(process.pid)], {
|
|
23
|
+
detached: true,
|
|
24
|
+
stdio: "ignore",
|
|
25
|
+
});
|
|
26
|
+
if (typeof proc.unref === "function") {
|
|
27
|
+
proc.unref();
|
|
28
|
+
}
|
|
29
|
+
child = proc;
|
|
30
|
+
active = true;
|
|
31
|
+
const thisChild = proc;
|
|
32
|
+
proc.once("exit", () => {
|
|
33
|
+
if (child === thisChild) {
|
|
34
|
+
child = null;
|
|
35
|
+
active = false;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return { applied: true, reason: "started" };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { applied: false, reason: "spawn-failed" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function disableCaffeinate() {
|
|
45
|
+
if (!active)
|
|
46
|
+
return;
|
|
47
|
+
if (child) {
|
|
48
|
+
try {
|
|
49
|
+
child.kill("SIGTERM");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
child = null;
|
|
55
|
+
active = false;
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ScreenManager } from "./screen.js";
|
|
2
|
+
export type InterceptorMode = "auto-accept" | "ask" | "off";
|
|
3
|
+
export interface PermissionInterceptorOpts {
|
|
4
|
+
screen: ScreenManager;
|
|
5
|
+
mode: InterceptorMode;
|
|
6
|
+
onDialog?: (info: {
|
|
7
|
+
title: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}) => Promise<"accept" | "deny">;
|
|
10
|
+
}
|
|
11
|
+
export interface DialogInfo {
|
|
12
|
+
title: string;
|
|
13
|
+
body: string;
|
|
14
|
+
action: "accept" | "deny";
|
|
15
|
+
}
|
|
16
|
+
export interface InterceptResult {
|
|
17
|
+
handled: boolean;
|
|
18
|
+
dialog?: DialogInfo;
|
|
19
|
+
}
|
|
20
|
+
export declare class PermissionInterceptor {
|
|
21
|
+
private readonly screen;
|
|
22
|
+
private mode;
|
|
23
|
+
private readonly onDialog?;
|
|
24
|
+
constructor(opts: PermissionInterceptorOpts);
|
|
25
|
+
getMode(): InterceptorMode;
|
|
26
|
+
setMode(mode: InterceptorMode): void;
|
|
27
|
+
checkAndHandle(): Promise<InterceptResult>;
|
|
28
|
+
}
|
|
29
|
+
export declare function modeFromEnv(value: string | undefined): InterceptorMode;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const KNOWN_DIALOGS = [
|
|
2
|
+
{
|
|
3
|
+
id: "camera",
|
|
4
|
+
titlePattern: /Would Like to Access the Camera/i,
|
|
5
|
+
acceptButtons: ["OK", "Allow"],
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
id: "microphone",
|
|
9
|
+
titlePattern: /Would Like to Access the Microphone/i,
|
|
10
|
+
acceptButtons: ["OK", "Allow"],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "photos",
|
|
14
|
+
titlePattern: /Would Like to Access Your Photos/i,
|
|
15
|
+
acceptButtons: [
|
|
16
|
+
"Allow Access to All Photos",
|
|
17
|
+
"Allow Full Access",
|
|
18
|
+
"Allow",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "location",
|
|
23
|
+
titlePattern: /Allow .* to Use Your Location\??/i,
|
|
24
|
+
acceptButtons: [
|
|
25
|
+
"Allow While Using App",
|
|
26
|
+
"Allow Once",
|
|
27
|
+
"Allow",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "notifications",
|
|
32
|
+
titlePattern: /Would Like to Send You Notifications/i,
|
|
33
|
+
acceptButtons: ["Allow"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "local-network",
|
|
37
|
+
titlePattern: /Would Like to Find and Connect to Devices on Your Local Network/i,
|
|
38
|
+
acceptButtons: ["Allow", "OK"],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "bluetooth",
|
|
42
|
+
titlePattern: /Would Like to Use Bluetooth/i,
|
|
43
|
+
acceptButtons: ["OK", "Allow"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "tracking",
|
|
47
|
+
titlePattern: /Allow .* to track your activity|would like permission to track/i,
|
|
48
|
+
acceptButtons: ["Allow"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "faceid",
|
|
52
|
+
titlePattern: /Would Like to Use Face ID/i,
|
|
53
|
+
acceptButtons: ["OK", "Allow"],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const ALERT_TYPE_PATTERNS = [
|
|
57
|
+
/XCUIElementTypeAlert/i,
|
|
58
|
+
/SBAlertController/i,
|
|
59
|
+
];
|
|
60
|
+
function isAlertElement(el) {
|
|
61
|
+
const type = String(el.type ?? "");
|
|
62
|
+
return ALERT_TYPE_PATTERNS.some((p) => p.test(type));
|
|
63
|
+
}
|
|
64
|
+
function findAlert(root) {
|
|
65
|
+
if (isAlertElement(root))
|
|
66
|
+
return root;
|
|
67
|
+
const children = root.children;
|
|
68
|
+
if (Array.isArray(children)) {
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
const found = findAlert(child);
|
|
71
|
+
if (found)
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function collectAlertContents(root) {
|
|
78
|
+
const texts = [];
|
|
79
|
+
const buttons = [];
|
|
80
|
+
const visit = (el) => {
|
|
81
|
+
const type = String(el.type ?? "");
|
|
82
|
+
const label = String(el.label ?? el.name ?? "");
|
|
83
|
+
const value = String(el.value ?? "");
|
|
84
|
+
if (/XCUIElementTypeStaticText/i.test(type)) {
|
|
85
|
+
const text = label || value;
|
|
86
|
+
if (text)
|
|
87
|
+
texts.push(text);
|
|
88
|
+
}
|
|
89
|
+
else if (/XCUIElementTypeButton/i.test(type)) {
|
|
90
|
+
if (label)
|
|
91
|
+
buttons.push({ label, element: el });
|
|
92
|
+
}
|
|
93
|
+
else if (!type) {
|
|
94
|
+
}
|
|
95
|
+
const children = el.children;
|
|
96
|
+
if (Array.isArray(children)) {
|
|
97
|
+
for (const child of children)
|
|
98
|
+
visit(child);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
visit(root);
|
|
102
|
+
return { texts, buttons };
|
|
103
|
+
}
|
|
104
|
+
export class PermissionInterceptor {
|
|
105
|
+
screen;
|
|
106
|
+
mode;
|
|
107
|
+
onDialog;
|
|
108
|
+
constructor(opts) {
|
|
109
|
+
this.screen = opts.screen;
|
|
110
|
+
this.mode = opts.mode;
|
|
111
|
+
this.onDialog = opts.onDialog;
|
|
112
|
+
}
|
|
113
|
+
getMode() {
|
|
114
|
+
return this.mode;
|
|
115
|
+
}
|
|
116
|
+
setMode(mode) {
|
|
117
|
+
this.mode = mode;
|
|
118
|
+
}
|
|
119
|
+
async checkAndHandle() {
|
|
120
|
+
if (this.mode === "off") {
|
|
121
|
+
return { handled: false };
|
|
122
|
+
}
|
|
123
|
+
const src = await this.screen.getSource().catch(() => undefined);
|
|
124
|
+
if (!src?.success || !src.source) {
|
|
125
|
+
return { handled: false };
|
|
126
|
+
}
|
|
127
|
+
const alert = findAlert(src.source);
|
|
128
|
+
if (!alert)
|
|
129
|
+
return { handled: false };
|
|
130
|
+
const { texts, buttons } = collectAlertContents(alert);
|
|
131
|
+
if (texts.length === 0 && buttons.length === 0) {
|
|
132
|
+
return { handled: false };
|
|
133
|
+
}
|
|
134
|
+
const title = texts[0] ?? "";
|
|
135
|
+
const body = texts.slice(1).join(" ");
|
|
136
|
+
const pattern = KNOWN_DIALOGS.find((p) => p.titlePattern.test(title));
|
|
137
|
+
if (!pattern) {
|
|
138
|
+
return { handled: false };
|
|
139
|
+
}
|
|
140
|
+
let action = "accept";
|
|
141
|
+
if (this.mode === "ask") {
|
|
142
|
+
if (this.onDialog) {
|
|
143
|
+
try {
|
|
144
|
+
action = await this.onDialog({ title, body });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
action = "accept";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const buttonLabels = action === "accept"
|
|
152
|
+
? pattern.acceptButtons
|
|
153
|
+
: pattern.denyButtons ?? ["Don't Allow", "Cancel"];
|
|
154
|
+
const target = buttonLabels
|
|
155
|
+
.map((preferred) => buttons.find((b) => b.label.toLowerCase() === preferred.toLowerCase()))
|
|
156
|
+
.find((b) => !!b);
|
|
157
|
+
if (!target) {
|
|
158
|
+
return {
|
|
159
|
+
handled: false,
|
|
160
|
+
dialog: { title, body, action },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const tap = await this.screen.findAndTap({
|
|
164
|
+
accessibilityLabel: target.label,
|
|
165
|
+
});
|
|
166
|
+
if (!tap.success) {
|
|
167
|
+
return {
|
|
168
|
+
handled: false,
|
|
169
|
+
dialog: { title, body, action },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
handled: true,
|
|
174
|
+
dialog: { title, body, action },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function modeFromEnv(value) {
|
|
179
|
+
const v = (value ?? "").toLowerCase();
|
|
180
|
+
if (v === "off" || v === "false" || v === "0")
|
|
181
|
+
return "off";
|
|
182
|
+
if (v === "ask")
|
|
183
|
+
return "ask";
|
|
184
|
+
return "auto-accept";
|
|
185
|
+
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detects when a clean Expo prebuild is needed by comparing
|
|
3
|
-
* current project state against a cached snapshot from the last
|
|
4
|
-
* successful build.
|
|
5
|
-
*/
|
|
6
1
|
interface PrebuildCheckResult {
|
|
7
2
|
needsClean: boolean;
|
|
8
3
|
reasons: string[];
|
|
@@ -12,38 +7,13 @@ export declare class PrebuildDetector {
|
|
|
12
7
|
private projectDir;
|
|
13
8
|
private snapshotPath;
|
|
14
9
|
constructor(projectDir: string, cacheDir: string);
|
|
15
|
-
/**
|
|
16
|
-
* Check if a file exists.
|
|
17
|
-
*/
|
|
18
10
|
private fileExists;
|
|
19
|
-
/**
|
|
20
|
-
* Read and parse a JSON file, returning null if it doesn't exist or is invalid.
|
|
21
|
-
*/
|
|
22
11
|
private readJson;
|
|
23
|
-
/**
|
|
24
|
-
* Compute a simple hash of a file's contents for change detection.
|
|
25
|
-
* Uses a basic string hash rather than crypto to keep it lightweight.
|
|
26
|
-
*/
|
|
27
12
|
private hashFile;
|
|
28
|
-
/**
|
|
29
|
-
* Read the current project state for snapshot comparison.
|
|
30
|
-
*/
|
|
31
13
|
private getCurrentState;
|
|
32
|
-
/**
|
|
33
|
-
* Load the cached snapshot from the last successful build.
|
|
34
|
-
*/
|
|
35
14
|
private loadSnapshot;
|
|
36
|
-
/**
|
|
37
|
-
* Save a snapshot after a successful build.
|
|
38
|
-
*/
|
|
39
15
|
saveSnapshot(): Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
* Diff two dependency maps and return lists of added, removed, and changed entries.
|
|
42
|
-
*/
|
|
43
16
|
private diffDependencies;
|
|
44
|
-
/**
|
|
45
|
-
* Check whether a clean prebuild is likely needed.
|
|
46
|
-
*/
|
|
47
17
|
check(): Promise<PrebuildCheckResult>;
|
|
48
18
|
}
|
|
49
19
|
export {};
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detects when a clean Expo prebuild is needed by comparing
|
|
3
|
-
* current project state against a cached snapshot from the last
|
|
4
|
-
* successful build.
|
|
5
|
-
*/
|
|
6
1
|
import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
7
2
|
import { join } from "node:path";
|
|
8
3
|
export class PrebuildDetector {
|
|
@@ -14,9 +9,6 @@ export class PrebuildDetector {
|
|
|
14
9
|
this.cacheDir = cacheDir;
|
|
15
10
|
this.snapshotPath = join(cacheDir, "last-build-snapshot.json");
|
|
16
11
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Check if a file exists.
|
|
19
|
-
*/
|
|
20
12
|
async fileExists(path) {
|
|
21
13
|
try {
|
|
22
14
|
await access(path);
|
|
@@ -26,9 +18,6 @@ export class PrebuildDetector {
|
|
|
26
18
|
return false;
|
|
27
19
|
}
|
|
28
20
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Read and parse a JSON file, returning null if it doesn't exist or is invalid.
|
|
31
|
-
*/
|
|
32
21
|
async readJson(path) {
|
|
33
22
|
try {
|
|
34
23
|
const content = await readFile(path, "utf-8");
|
|
@@ -38,10 +27,6 @@ export class PrebuildDetector {
|
|
|
38
27
|
return null;
|
|
39
28
|
}
|
|
40
29
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Compute a simple hash of a file's contents for change detection.
|
|
43
|
-
* Uses a basic string hash rather than crypto to keep it lightweight.
|
|
44
|
-
*/
|
|
45
30
|
async hashFile(path) {
|
|
46
31
|
try {
|
|
47
32
|
const content = await readFile(path, "utf-8");
|
|
@@ -49,7 +34,7 @@ export class PrebuildDetector {
|
|
|
49
34
|
for (let i = 0; i < content.length; i++) {
|
|
50
35
|
const chr = content.charCodeAt(i);
|
|
51
36
|
hash = (hash << 5) - hash + chr;
|
|
52
|
-
hash |= 0;
|
|
37
|
+
hash |= 0;
|
|
53
38
|
}
|
|
54
39
|
return hash.toString(36);
|
|
55
40
|
}
|
|
@@ -57,15 +42,10 @@ export class PrebuildDetector {
|
|
|
57
42
|
return "missing";
|
|
58
43
|
}
|
|
59
44
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Read the current project state for snapshot comparison.
|
|
62
|
-
*/
|
|
63
45
|
async getCurrentState() {
|
|
64
46
|
const pkgJson = await this.readJson(join(this.projectDir, "package.json"));
|
|
65
|
-
// Try app.json first, fall back to app.config.js/ts content
|
|
66
47
|
let appConfig = await this.readJson(join(this.projectDir, "app.json"));
|
|
67
48
|
if (!appConfig) {
|
|
68
|
-
// For app.config.js/ts, we just hash the file instead of evaluating it
|
|
69
49
|
const configHash = await this.hashFile(join(this.projectDir, "app.config.js"));
|
|
70
50
|
const configTsHash = await this.hashFile(join(this.projectDir, "app.config.ts"));
|
|
71
51
|
appConfig = {
|
|
@@ -82,9 +62,6 @@ export class PrebuildDetector {
|
|
|
82
62
|
timestamp: new Date().toISOString(),
|
|
83
63
|
};
|
|
84
64
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Load the cached snapshot from the last successful build.
|
|
87
|
-
*/
|
|
88
65
|
async loadSnapshot() {
|
|
89
66
|
try {
|
|
90
67
|
const content = await readFile(this.snapshotPath, "utf-8");
|
|
@@ -94,18 +71,11 @@ export class PrebuildDetector {
|
|
|
94
71
|
return null;
|
|
95
72
|
}
|
|
96
73
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Save a snapshot after a successful build.
|
|
99
|
-
*/
|
|
100
74
|
async saveSnapshot() {
|
|
101
75
|
const state = await this.getCurrentState();
|
|
102
|
-
// Ensure cache directory exists
|
|
103
76
|
await mkdir(this.cacheDir, { recursive: true });
|
|
104
77
|
await writeFile(this.snapshotPath, JSON.stringify(state, null, 2), "utf-8");
|
|
105
78
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Diff two dependency maps and return lists of added, removed, and changed entries.
|
|
108
|
-
*/
|
|
109
79
|
diffDependencies(oldDeps, newDeps) {
|
|
110
80
|
const added = [];
|
|
111
81
|
const removed = [];
|
|
@@ -127,12 +97,8 @@ export class PrebuildDetector {
|
|
|
127
97
|
}
|
|
128
98
|
return { added, removed, changed };
|
|
129
99
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Check whether a clean prebuild is likely needed.
|
|
132
|
-
*/
|
|
133
100
|
async check() {
|
|
134
101
|
const reasons = [];
|
|
135
|
-
// Check if ios/ directory exists at all
|
|
136
102
|
const iosExists = await this.fileExists(join(this.projectDir, "ios"));
|
|
137
103
|
if (!iosExists) {
|
|
138
104
|
return {
|
|
@@ -142,7 +108,6 @@ export class PrebuildDetector {
|
|
|
142
108
|
],
|
|
143
109
|
};
|
|
144
110
|
}
|
|
145
|
-
// Load the last build snapshot
|
|
146
111
|
const snapshot = await this.loadSnapshot();
|
|
147
112
|
if (!snapshot) {
|
|
148
113
|
return {
|
|
@@ -153,9 +118,7 @@ export class PrebuildDetector {
|
|
|
153
118
|
],
|
|
154
119
|
};
|
|
155
120
|
}
|
|
156
|
-
// Get current state
|
|
157
121
|
const current = await this.getCurrentState();
|
|
158
|
-
// Compare dependencies
|
|
159
122
|
const depDiff = this.diffDependencies(snapshot.dependencies, current.dependencies);
|
|
160
123
|
if (depDiff.added.length > 0) {
|
|
161
124
|
reasons.push(`New dependencies added: ${depDiff.added.join(", ")}. ` +
|
|
@@ -166,7 +129,6 @@ export class PrebuildDetector {
|
|
|
166
129
|
"Stale native modules may cause build errors.");
|
|
167
130
|
}
|
|
168
131
|
if (depDiff.changed.length > 0) {
|
|
169
|
-
// Only flag native-related version changes as needing clean prebuild
|
|
170
132
|
const nativeIndicators = [
|
|
171
133
|
"react-native",
|
|
172
134
|
"expo",
|
|
@@ -179,7 +141,6 @@ export class PrebuildDetector {
|
|
|
179
141
|
"Clean prebuild recommended.");
|
|
180
142
|
}
|
|
181
143
|
}
|
|
182
|
-
// Compare devDependencies
|
|
183
144
|
const devDepDiff = this.diffDependencies(snapshot.devDependencies, current.devDependencies);
|
|
184
145
|
if (devDepDiff.added.length > 0 || devDepDiff.removed.length > 0) {
|
|
185
146
|
const nativeDevDeps = [
|
|
@@ -192,14 +153,12 @@ export class PrebuildDetector {
|
|
|
192
153
|
reasons.push(`Native dev dependencies changed: ${nativeDevDeps.join(", ")}.`);
|
|
193
154
|
}
|
|
194
155
|
}
|
|
195
|
-
// Compare app config
|
|
196
156
|
const configChanged = JSON.stringify(snapshot.appConfig) !==
|
|
197
157
|
JSON.stringify(current.appConfig);
|
|
198
158
|
if (configChanged) {
|
|
199
159
|
reasons.push("app.json / app.config.js has changed since last build. " +
|
|
200
160
|
"Expo plugins or native config may have changed.");
|
|
201
161
|
}
|
|
202
|
-
// Compare Podfile.lock hash
|
|
203
162
|
if (current.podfileLockHash === "missing") {
|
|
204
163
|
reasons.push("ios/Podfile.lock is missing. Pods may need to be installed.");
|
|
205
164
|
}
|