@rvaim/deveco-mobile-mcp 0.1.0-rvaim.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.
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Simctl = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ const logger_1 = require("./logger");
9
+ const webdriver_agent_1 = require("./webdriver-agent");
10
+ const robot_1 = require("./robot");
11
+ const utils_1 = require("./utils");
12
+ const TIMEOUT = 30000;
13
+ const WDA_PORT = 8100;
14
+ const MAX_BUFFER_SIZE = 1024 * 1024 * 8;
15
+ class Simctl {
16
+ simulatorUuid;
17
+ constructor(simulatorUuid) {
18
+ this.simulatorUuid = simulatorUuid;
19
+ }
20
+ async isWdaInstalled() {
21
+ const apps = await this.listApps();
22
+ return apps.map(app => app.packageName).includes("com.facebook.WebDriverAgentRunner.xctrunner");
23
+ }
24
+ async startWda() {
25
+ if (!(await this.isWdaInstalled())) {
26
+ // wda is not even installed, won't attempt to start it
27
+ return;
28
+ }
29
+ (0, logger_1.trace)("Starting WebDriverAgent");
30
+ const webdriverPackageName = "com.facebook.WebDriverAgentRunner.xctrunner";
31
+ this.simctl("launch", this.simulatorUuid, webdriverPackageName);
32
+ // now we wait for wda to have a successful status
33
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
34
+ // wait up to 10 seconds for wda to start
35
+ const timeout = +new Date() + 10 * 1000;
36
+ while (+new Date() < timeout) {
37
+ // cross fingers and see if wda is already running
38
+ if (await wda.isRunning()) {
39
+ (0, logger_1.trace)("WebDriverAgent is now running");
40
+ return;
41
+ }
42
+ // wait 100ms before trying again
43
+ await new Promise(resolve => setTimeout(resolve, 100));
44
+ }
45
+ (0, logger_1.trace)("Could not start WebDriverAgent in time, giving up");
46
+ }
47
+ async wda() {
48
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
49
+ if (!(await wda.isRunning())) {
50
+ await this.startWda();
51
+ if (!(await wda.isRunning())) {
52
+ throw new robot_1.ActionableError("WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/");
53
+ }
54
+ // was successfully started
55
+ }
56
+ return wda;
57
+ }
58
+ simctl(...args) {
59
+ return (0, node_child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
60
+ timeout: TIMEOUT,
61
+ maxBuffer: MAX_BUFFER_SIZE,
62
+ });
63
+ }
64
+ async getScreenshot() {
65
+ const wda = await this.wda();
66
+ return await wda.getScreenshot();
67
+ // alternative: return this.simctl("io", this.simulatorUuid, "screenshot", "-");
68
+ }
69
+ async openUrl(url) {
70
+ const wda = await this.wda();
71
+ await wda.openUrl(url);
72
+ // alternative: this.simctl("openurl", this.simulatorUuid, url);
73
+ }
74
+ async launchApp(packageName, locale) {
75
+ (0, utils_1.validatePackageName)(packageName);
76
+ const args = ["launch", this.simulatorUuid, packageName];
77
+ if (locale) {
78
+ (0, utils_1.validateLocale)(locale);
79
+ const locales = locale.split(",").map(l => l.trim());
80
+ args.push("-AppleLanguages", `(${locales.join(", ")})`);
81
+ args.push("-AppleLocale", locales[0]);
82
+ }
83
+ this.simctl(...args);
84
+ }
85
+ async terminateApp(packageName) {
86
+ (0, utils_1.validatePackageName)(packageName);
87
+ this.simctl("terminate", this.simulatorUuid, packageName);
88
+ }
89
+ findAppBundle(dir) {
90
+ const entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (entry.isDirectory() && entry.name.endsWith(".app")) {
93
+ return (0, node_path_1.join)(dir, entry.name);
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ validateZipPaths(zipPath) {
99
+ const output = (0, node_child_process_1.execFileSync)("/usr/bin/zipinfo", ["-1", zipPath], {
100
+ timeout: TIMEOUT,
101
+ maxBuffer: MAX_BUFFER_SIZE,
102
+ }).toString();
103
+ const invalidPath = output
104
+ .split("\n")
105
+ .map(s => s.trim())
106
+ .filter(s => s)
107
+ .find(s => s.startsWith("/") || s.includes(".."));
108
+ if (invalidPath) {
109
+ throw new robot_1.ActionableError(`Security violation: File path '${invalidPath}' contains invalid characters`);
110
+ }
111
+ }
112
+ async installApp(path) {
113
+ let tempDir = null;
114
+ let installPath = path;
115
+ try {
116
+ // zip files need to be extracted prior to installation
117
+ if ((0, node_path_1.extname)(path).toLowerCase() === ".zip") {
118
+ (0, logger_1.trace)(`Detected .zip file, validating contents`);
119
+ // before extracting, let's make sure there's no zip-slip bombs here
120
+ this.validateZipPaths(path);
121
+ tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "ios-app-"));
122
+ try {
123
+ (0, node_child_process_1.execFileSync)("unzip", ["-q", path, "-d", tempDir], {
124
+ timeout: TIMEOUT,
125
+ });
126
+ }
127
+ catch (error) {
128
+ throw new robot_1.ActionableError(`Failed to unzip file: ${error.message}`);
129
+ }
130
+ const appBundle = this.findAppBundle(tempDir);
131
+ if (!appBundle) {
132
+ throw new robot_1.ActionableError("No .app bundle found in the .zip file, please visit wiki at https://github.com/mobile-next/mobile-mcp/wiki for assistance.");
133
+ }
134
+ installPath = appBundle;
135
+ (0, logger_1.trace)(`Found .app bundle at: ${(0, node_path_1.basename)(appBundle)}`);
136
+ }
137
+ // continue with installation
138
+ this.simctl("install", this.simulatorUuid, installPath);
139
+ }
140
+ catch (error) {
141
+ const stdout = error.stdout ? error.stdout.toString() : "";
142
+ const stderr = error.stderr ? error.stderr.toString() : "";
143
+ const output = (stdout + stderr).trim();
144
+ throw new robot_1.ActionableError(output || error.message);
145
+ }
146
+ finally {
147
+ // Clean up temporary directory if it was created
148
+ if (tempDir) {
149
+ try {
150
+ (0, logger_1.trace)(`Cleaning up temporary directory`);
151
+ (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
152
+ }
153
+ catch (cleanupError) {
154
+ (0, logger_1.trace)(`Warning: Failed to cleanup temporary directory: ${cleanupError}`);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ async uninstallApp(bundleId) {
160
+ try {
161
+ this.simctl("uninstall", this.simulatorUuid, bundleId);
162
+ }
163
+ catch (error) {
164
+ const stdout = error.stdout ? error.stdout.toString() : "";
165
+ const stderr = error.stderr ? error.stderr.toString() : "";
166
+ const output = (stdout + stderr).trim();
167
+ throw new robot_1.ActionableError(output || error.message);
168
+ }
169
+ }
170
+ async listApps() {
171
+ const text = this.simctl("listapps", this.simulatorUuid).toString();
172
+ const result = (0, node_child_process_1.execFileSync)("plutil", ["-convert", "json", "-o", "-", "-r", "-"], {
173
+ input: text,
174
+ });
175
+ const output = JSON.parse(result.toString());
176
+ return Object.values(output).map(app => ({
177
+ packageName: app.CFBundleIdentifier,
178
+ appName: app.CFBundleDisplayName,
179
+ }));
180
+ }
181
+ async getScreenSize() {
182
+ const wda = await this.wda();
183
+ return wda.getScreenSize();
184
+ }
185
+ async sendKeys(keys) {
186
+ const wda = await this.wda();
187
+ return wda.sendKeys(keys);
188
+ }
189
+ async swipe(direction) {
190
+ const wda = await this.wda();
191
+ return wda.swipe(direction);
192
+ }
193
+ async swipeFromCoordinate(x, y, direction, distance) {
194
+ const wda = await this.wda();
195
+ return wda.swipeFromCoordinate(x, y, direction, distance);
196
+ }
197
+ async tap(x, y) {
198
+ const wda = await this.wda();
199
+ return wda.tap(x, y);
200
+ }
201
+ async doubleTap(x, y) {
202
+ const wda = await this.wda();
203
+ await wda.doubleTap(x, y);
204
+ }
205
+ async longPress(x, y, duration) {
206
+ const wda = await this.wda();
207
+ return wda.longPress(x, y, duration);
208
+ }
209
+ async pressButton(button) {
210
+ const wda = await this.wda();
211
+ return wda.pressButton(button);
212
+ }
213
+ async getElementsOnScreen() {
214
+ const wda = await this.wda();
215
+ return wda.getElementsOnScreen();
216
+ }
217
+ async setOrientation(orientation) {
218
+ const wda = await this.wda();
219
+ return wda.setOrientation(orientation);
220
+ }
221
+ async getOrientation() {
222
+ const wda = await this.wda();
223
+ return wda.getOrientation();
224
+ }
225
+ /**
226
+ * 获取模拟器日志 (log stream)
227
+ * @param options - 可选的过滤选项
228
+ */
229
+ async getDeviceLogs(options) {
230
+ // iOS 模拟器使用 simctl spawn log 命令获取日志
231
+ try {
232
+ const args = ["spawn", this.simulatorUuid, "log", "show", "--last", "1m"];
233
+ // 添加行数限制
234
+ const lines = options?.lines || 100;
235
+ args.push("--info"); // 包含 info 级别及以上
236
+ const output = (0, node_child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
237
+ maxBuffer: 1024 * 1024 * 8,
238
+ timeout: 15000,
239
+ }).toString();
240
+ // 按行分割并过滤
241
+ let lines_array = output.split("\n");
242
+ // 按标签过滤
243
+ if (options?.bundleName) {
244
+ lines_array = lines_array.filter(line => line.includes(options.bundleName));
245
+ }
246
+ // 按 PID 过滤
247
+ if (options?.pid) {
248
+ lines_array = lines_array.filter(line => line.includes(`[${options.pid}]`));
249
+ }
250
+ // 按级别过滤
251
+ if (options?.level) {
252
+ const levelPattern = options.level.toLowerCase();
253
+ lines_array = lines_array.filter(line => {
254
+ const lowerLine = line.toLowerCase();
255
+ return lowerLine.includes(levelPattern);
256
+ });
257
+ }
258
+ // 限制行数
259
+ return lines_array.slice(-lines).join("\n");
260
+ }
261
+ catch (error) {
262
+ throw new robot_1.ActionableError(`Failed to get simulator logs: ${error.message}`);
263
+ }
264
+ }
265
+ }
266
+ exports.Simctl = Simctl;
package/lib/jpeg.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JPEG = void 0;
4
+ class JPEG {
5
+ buffer;
6
+ constructor(buffer) {
7
+ this.buffer = buffer;
8
+ }
9
+ getDimensions() {
10
+ // JPEG signature: FF D8
11
+ if (this.buffer[0] !== 0xff || this.buffer[1] !== 0xd8) {
12
+ throw new Error("Not a valid JPEG file");
13
+ }
14
+ let offset = 2;
15
+ while (offset < this.buffer.length - 1) {
16
+ // Find marker
17
+ if (this.buffer[offset] !== 0xff) {
18
+ offset++;
19
+ continue;
20
+ }
21
+ const marker = this.buffer[offset + 1];
22
+ // SOF0 (Baseline DCT), SOF1, SOF2 (Progressive DCT), etc.
23
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
24
+ // SOF marker found
25
+ // Structure: FF marker length(2) precision(1) height(2) width(2) ...
26
+ const height = this.buffer.readUInt16BE(offset + 5);
27
+ const width = this.buffer.readUInt16BE(offset + 7);
28
+ return { width, height };
29
+ }
30
+ // Skip to next marker
31
+ if (marker === 0xd8 || marker === 0xd9) {
32
+ // SOI or EOI, no length field
33
+ offset += 2;
34
+ }
35
+ else if (marker === 0xff) {
36
+ // Padding byte
37
+ offset++;
38
+ }
39
+ else {
40
+ // Read length and skip
41
+ const length = this.buffer.readUInt16BE(offset + 2);
42
+ offset += 2 + length;
43
+ }
44
+ }
45
+ throw new Error("Could not find JPEG dimensions");
46
+ }
47
+ }
48
+ exports.JPEG = JPEG;
package/lib/logger.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.error = exports.trace = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const writeLog = (message) => {
6
+ if (process.env.LOG_FILE) {
7
+ const logfile = process.env.LOG_FILE;
8
+ const timestamp = new Date().toISOString();
9
+ const levelStr = "INFO";
10
+ const logMessage = `[${timestamp}] ${levelStr} ${message}`;
11
+ (0, node_fs_1.appendFileSync)(logfile, logMessage + "\n");
12
+ }
13
+ console.error(message);
14
+ };
15
+ const trace = (message) => {
16
+ writeLog(message);
17
+ };
18
+ exports.trace = trace;
19
+ const error = (message) => {
20
+ writeLog(message);
21
+ };
22
+ exports.error = error;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MobileDevice = void 0;
4
+ const mobilecli_1 = require("./mobilecli");
5
+ class MobileDevice {
6
+ deviceId;
7
+ mobilecli;
8
+ constructor(deviceId) {
9
+ this.deviceId = deviceId;
10
+ this.mobilecli = new mobilecli_1.Mobilecli();
11
+ }
12
+ runCommand(args) {
13
+ const fullArgs = [...args, "--device", this.deviceId];
14
+ return this.mobilecli.executeCommand(fullArgs);
15
+ }
16
+ async getScreenSize() {
17
+ const response = JSON.parse(this.runCommand(["device", "info"]));
18
+ if (response.data.device.screenSize) {
19
+ return response.data.device.screenSize;
20
+ }
21
+ return { width: 0, height: 0, scale: 1.0 };
22
+ }
23
+ async swipe(direction) {
24
+ const screenSize = await this.getScreenSize();
25
+ const centerX = Math.floor(screenSize.width / 2);
26
+ const centerY = Math.floor(screenSize.height / 2);
27
+ const distance = 400; // Default distance in pixels
28
+ let startX = centerX;
29
+ let startY = centerY;
30
+ let endX = centerX;
31
+ let endY = centerY;
32
+ switch (direction) {
33
+ case "up":
34
+ startY = centerY + distance / 2;
35
+ endY = centerY - distance / 2;
36
+ break;
37
+ case "down":
38
+ startY = centerY - distance / 2;
39
+ endY = centerY + distance / 2;
40
+ break;
41
+ case "left":
42
+ startX = centerX + distance / 2;
43
+ endX = centerX - distance / 2;
44
+ break;
45
+ case "right":
46
+ startX = centerX - distance / 2;
47
+ endX = centerX + distance / 2;
48
+ break;
49
+ }
50
+ this.runCommand(["io", "swipe", `${startX},${startY},${endX},${endY}`]);
51
+ }
52
+ async swipeFromCoordinate(x, y, direction, distance) {
53
+ const swipeDistance = distance || 400;
54
+ let endX = x;
55
+ let endY = y;
56
+ switch (direction) {
57
+ case "up":
58
+ endY = y - swipeDistance;
59
+ break;
60
+ case "down":
61
+ endY = y + swipeDistance;
62
+ break;
63
+ case "left":
64
+ endX = x - swipeDistance;
65
+ break;
66
+ case "right":
67
+ endX = x + swipeDistance;
68
+ break;
69
+ }
70
+ this.runCommand(["io", "swipe", `${x},${y},${endX},${endY}`]);
71
+ }
72
+ async getScreenshot() {
73
+ const fullArgs = ["screenshot", "--device", this.deviceId, "--format", "png", "--output", "-"];
74
+ return this.mobilecli.executeCommandBuffer(fullArgs);
75
+ }
76
+ async listApps() {
77
+ const response = JSON.parse(this.runCommand(["apps", "list"]));
78
+ return response.data.map(app => ({
79
+ appName: app.appName || app.packageName,
80
+ packageName: app.packageName,
81
+ }));
82
+ }
83
+ async launchApp(packageName, locale) {
84
+ const args = ["apps", "launch", packageName];
85
+ if (locale) {
86
+ args.push("--locale", locale);
87
+ }
88
+ this.runCommand(args);
89
+ }
90
+ async terminateApp(packageName) {
91
+ this.runCommand(["apps", "terminate", packageName]);
92
+ }
93
+ async installApp(path) {
94
+ this.runCommand(["apps", "install", path]);
95
+ }
96
+ async uninstallApp(bundleId) {
97
+ this.runCommand(["apps", "uninstall", bundleId]);
98
+ }
99
+ async openUrl(url) {
100
+ this.runCommand(["url", url]);
101
+ }
102
+ async sendKeys(text) {
103
+ this.runCommand(["io", "text", text]);
104
+ }
105
+ async pressButton(button) {
106
+ this.runCommand(["io", "button", button]);
107
+ }
108
+ async tap(x, y) {
109
+ this.runCommand(["io", "tap", `${x},${y}`]);
110
+ }
111
+ async doubleTap(x, y) {
112
+ // TODO: should move into mobilecli itself as "io doubletap"
113
+ await this.tap(x, y);
114
+ await this.tap(x, y);
115
+ }
116
+ async longPress(x, y, duration) {
117
+ this.runCommand(["io", "longpress", `${x},${y}`, "--duration", `${duration}`]);
118
+ }
119
+ async getElementsOnScreen() {
120
+ const response = JSON.parse(this.runCommand(["dump", "ui"]));
121
+ return response.data.elements.map(element => ({
122
+ type: element.type,
123
+ label: element.label,
124
+ text: element.text,
125
+ name: element.name,
126
+ value: element.value,
127
+ identifier: element.identifier,
128
+ rect: element.rect,
129
+ focused: element.focused,
130
+ }));
131
+ }
132
+ async setOrientation(orientation) {
133
+ this.runCommand(["device", "orientation", "set", orientation]);
134
+ }
135
+ async getOrientation() {
136
+ const response = JSON.parse(this.runCommand(["device", "orientation", "get"]));
137
+ return response.data.orientation;
138
+ }
139
+ async getDeviceLogs(options) {
140
+ const args = ["logs", "get"];
141
+ if (options?.bundleName) {
142
+ args.push("--tag", options.bundleName);
143
+ }
144
+ if (options?.pid) {
145
+ args.push("--pid", options.pid.toString());
146
+ }
147
+ if (options?.level) {
148
+ args.push("--level", options.level);
149
+ }
150
+ if (options?.lines) {
151
+ args.push("--lines", options.lines.toString());
152
+ }
153
+ if (options?.clear) {
154
+ args.push("--clear");
155
+ }
156
+ return this.runCommand(args);
157
+ }
158
+ }
159
+ exports.MobileDevice = MobileDevice;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Mobilecli = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const node_child_process_1 = require("node:child_process");
7
+ const TIMEOUT = 30000;
8
+ const MAX_BUFFER_SIZE = 1024 * 1024 * 8;
9
+ class Mobilecli {
10
+ path = null;
11
+ constructor() { }
12
+ getPath() {
13
+ if (!this.path) {
14
+ this.path = Mobilecli.getMobilecliPath();
15
+ }
16
+ return this.path;
17
+ }
18
+ executeCommand(args) {
19
+ const path = this.getPath();
20
+ return (0, node_child_process_1.execFileSync)(path, args, { encoding: "utf8" }).toString().trim();
21
+ }
22
+ spawnCommand(args) {
23
+ const binaryPath = this.getPath();
24
+ return (0, node_child_process_1.spawn)(binaryPath, args, {
25
+ stdio: ["ignore", "ignore", "ignore"],
26
+ });
27
+ }
28
+ executeCommandBuffer(args) {
29
+ const path = this.getPath();
30
+ return (0, node_child_process_1.execFileSync)(path, args, {
31
+ encoding: "buffer",
32
+ maxBuffer: MAX_BUFFER_SIZE,
33
+ timeout: TIMEOUT,
34
+ });
35
+ }
36
+ static getMobilecliPath() {
37
+ if (process.env.MOBILECLI_PATH) {
38
+ return process.env.MOBILECLI_PATH;
39
+ }
40
+ const platform = process.platform;
41
+ const arch = process.arch;
42
+ const normalizedPlatform = platform === "win32" ? "windows" : platform;
43
+ const normalizedArch = arch === "arm64" ? "arm64" : "amd64";
44
+ const ext = platform === "win32" ? ".exe" : "";
45
+ const binaryName = `mobilecli-${normalizedPlatform}-${normalizedArch}${ext}`;
46
+ // Check if mobile-mcp is installed as a package
47
+ const currentPath = __filename;
48
+ const pathParts = currentPath.split(node_path_1.sep);
49
+ const lastNodeModulesIndex = pathParts.lastIndexOf("node_modules");
50
+ if (lastNodeModulesIndex !== -1) {
51
+ // We're inside node_modules, go to the last node_modules in the path
52
+ const nodeModulesParts = pathParts.slice(0, lastNodeModulesIndex + 1);
53
+ const lastNodeModulesPath = nodeModulesParts.join(node_path_1.sep);
54
+ const mobilecliPath = (0, node_path_1.join)(lastNodeModulesPath, "@mobilenext", "mobilecli", "bin", binaryName);
55
+ if ((0, node_fs_1.existsSync)(mobilecliPath)) {
56
+ return mobilecliPath;
57
+ }
58
+ }
59
+ // Not in node_modules, look one directory up from current script
60
+ const scriptDir = (0, node_path_1.dirname)(__filename);
61
+ const parentDir = (0, node_path_1.dirname)(scriptDir);
62
+ const mobilecliPath = (0, node_path_1.join)(parentDir, "node_modules", "@mobilenext", "mobilecli", "bin", binaryName);
63
+ if ((0, node_fs_1.existsSync)(mobilecliPath)) {
64
+ return mobilecliPath;
65
+ }
66
+ throw new Error(`Could not find mobilecli binary for platform: ${platform}`);
67
+ }
68
+ getVersion() {
69
+ try {
70
+ const output = this.executeCommand(["--version"]);
71
+ if (output.startsWith("mobilecli version ")) {
72
+ return output.substring("mobilecli version ".length);
73
+ }
74
+ return "failed";
75
+ }
76
+ catch (error) {
77
+ return "failed " + error.message;
78
+ }
79
+ }
80
+ fleetListDevices() {
81
+ return this.executeCommand(["fleet", "list-devices"]);
82
+ }
83
+ fleetAllocate(platform) {
84
+ return this.executeCommand(["fleet", "allocate", "--platform", platform]);
85
+ }
86
+ fleetRelease(deviceId) {
87
+ return this.executeCommand(["fleet", "release", "--device", deviceId]);
88
+ }
89
+ getDevices(options) {
90
+ const args = ["devices"];
91
+ if (options) {
92
+ if (options.includeOffline) {
93
+ args.push("--include-offline");
94
+ }
95
+ if (options.platform) {
96
+ if (options.platform !== "ios" && options.platform !== "android") {
97
+ throw new Error(`Invalid platform: ${options.platform}. Must be "ios" or "android"`);
98
+ }
99
+ args.push("--platform", options.platform);
100
+ }
101
+ if (options.type) {
102
+ if (options.type !== "real" && options.type !== "emulator" && options.type !== "simulator") {
103
+ throw new Error(`Invalid type: ${options.type}. Must be "real", "emulator", or "simulator"`);
104
+ }
105
+ args.push("--type", options.type);
106
+ }
107
+ }
108
+ const mobilecliOutput = this.executeCommand(args);
109
+ return JSON.parse(mobilecliOutput);
110
+ }
111
+ }
112
+ exports.Mobilecli = Mobilecli;
package/lib/png.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PNG = void 0;
4
+ class PNG {
5
+ buffer;
6
+ constructor(buffer) {
7
+ this.buffer = buffer;
8
+ }
9
+ getDimensions() {
10
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
11
+ if (!this.buffer.subarray(0, 8).equals(pngSignature)) {
12
+ throw new Error("Not a valid PNG file");
13
+ }
14
+ const width = this.buffer.readUInt32BE(16);
15
+ const height = this.buffer.readUInt32BE(20);
16
+ return { width, height };
17
+ }
18
+ }
19
+ exports.PNG = PNG;
package/lib/robot.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActionableError = void 0;
4
+ class ActionableError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ }
8
+ }
9
+ exports.ActionableError = ActionableError;