@nbakka/mcp-appium 1.0.28 → 2.0.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.
package/lib/ios.js ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.IosManager = exports.IosRobot = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = require("os");
9
+ const crypto_1 = require("crypto");
10
+ const fs_1 = require("fs");
11
+ const child_process_1 = require("child_process");
12
+ const net_1 = require("net");
13
+ const webdriver_agent_1 = require("./webdriver-agent");
14
+ const robot_1 = require("./robot");
15
+ const WDA_PORT = 8100;
16
+ const IOS_TUNNEL_PORT = 60105;
17
+ const getGoIosPath = () => {
18
+ if (process.env.GO_IOS_PATH) {
19
+ return process.env.GO_IOS_PATH;
20
+ }
21
+ // fallback to go-ios in PATH via `npm install -g go-ios`
22
+ return "ios";
23
+ };
24
+ class IosRobot {
25
+ deviceId;
26
+ constructor(deviceId) {
27
+ this.deviceId = deviceId;
28
+ }
29
+ isListeningOnPort(port) {
30
+ return new Promise((resolve, reject) => {
31
+ const client = new net_1.Socket();
32
+ client.connect(port, "localhost", () => {
33
+ client.destroy();
34
+ resolve(true);
35
+ });
36
+ client.on("error", (err) => {
37
+ resolve(false);
38
+ });
39
+ });
40
+ }
41
+ async isTunnelRunning() {
42
+ return await this.isListeningOnPort(IOS_TUNNEL_PORT);
43
+ }
44
+ async isWdaForwardRunning() {
45
+ return await this.isListeningOnPort(WDA_PORT);
46
+ }
47
+ async assertTunnelRunning() {
48
+ if (await this.isTunnelRequired()) {
49
+ if (!(await this.isTunnelRunning())) {
50
+ throw new robot_1.ActionableError("iOS tunnel is not running, please see https://github.com/mobile-next/mobile-mcp/wiki/");
51
+ }
52
+ }
53
+ }
54
+ async wda() {
55
+ await this.assertTunnelRunning();
56
+ if (!(await this.isWdaForwardRunning())) {
57
+ throw new robot_1.ActionableError("Port forwarding to WebDriverAgent is not running (tunnel okay), please see https://github.com/mobile-next/mobile-mcp/wiki/");
58
+ }
59
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
60
+ if (!(await wda.isRunning())) {
61
+ throw new robot_1.ActionableError("WebDriverAgent is not running on device (tunnel okay, port forwarding okay), please see https://github.com/mobile-next/mobile-mcp/wiki/");
62
+ }
63
+ return wda;
64
+ }
65
+ async ios(...args) {
66
+ return (0, child_process_1.execFileSync)(getGoIosPath(), ["--udid", this.deviceId, ...args], {}).toString();
67
+ }
68
+ async getIosVersion() {
69
+ const output = await this.ios("info");
70
+ const json = JSON.parse(output);
71
+ return json.ProductVersion;
72
+ }
73
+ async isTunnelRequired() {
74
+ const version = await this.getIosVersion();
75
+ const args = version.split(".");
76
+ return parseInt(args[0], 10) >= 17;
77
+ }
78
+ async getScreenSize() {
79
+ const wda = await this.wda();
80
+ return await wda.getScreenSize();
81
+ }
82
+ async swipe(direction) {
83
+ const wda = await this.wda();
84
+ await wda.swipe(direction);
85
+ }
86
+ async listApps() {
87
+ await this.assertTunnelRunning();
88
+ const output = await this.ios("apps", "--all", "--list");
89
+ return output
90
+ .split("\n")
91
+ .map(line => {
92
+ const [packageName, appName] = line.split(" ");
93
+ return {
94
+ packageName,
95
+ appName,
96
+ };
97
+ });
98
+ }
99
+ async launchApp(packageName) {
100
+ await this.assertTunnelRunning();
101
+ await this.ios("launch", packageName);
102
+ }
103
+ async terminateApp(packageName) {
104
+ await this.assertTunnelRunning();
105
+ await this.ios("kill", packageName);
106
+ }
107
+ async openUrl(url) {
108
+ const wda = await this.wda();
109
+ await wda.openUrl(url);
110
+ }
111
+ async sendKeys(text) {
112
+ const wda = await this.wda();
113
+ await wda.sendKeys(text);
114
+ }
115
+ async pressButton(button) {
116
+ const wda = await this.wda();
117
+ await wda.pressButton(button);
118
+ }
119
+ async tap(x, y) {
120
+ const wda = await this.wda();
121
+ await wda.tap(x, y);
122
+ }
123
+ async getElementsOnScreen() {
124
+ const wda = await this.wda();
125
+ return await wda.getElementsOnScreen();
126
+ }
127
+ async getScreenshot() {
128
+ await this.assertTunnelRunning();
129
+ const tmpFilename = path_1.default.join((0, os_1.tmpdir)(), `screenshot-${(0, crypto_1.randomBytes)(8).toString("hex")}.png`);
130
+ await this.ios("screenshot", "--output", tmpFilename);
131
+ const buffer = (0, fs_1.readFileSync)(tmpFilename);
132
+ (0, fs_1.unlinkSync)(tmpFilename);
133
+ return buffer;
134
+ }
135
+ async setOrientation(orientation) {
136
+ const wda = await this.wda();
137
+ await wda.setOrientation(orientation);
138
+ }
139
+ async getOrientation() {
140
+ const wda = await this.wda();
141
+ return await wda.getOrientation();
142
+ }
143
+ }
144
+ exports.IosRobot = IosRobot;
145
+ class IosManager {
146
+ async isGoIosInstalled() {
147
+ try {
148
+ const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["version"], { stdio: ["pipe", "pipe", "ignore"] }).toString();
149
+ const json = JSON.parse(output);
150
+ return json.version !== undefined && (json.version.startsWith("v") || json.version === "local-build");
151
+ }
152
+ catch (error) {
153
+ return false;
154
+ }
155
+ }
156
+ async getDeviceName(deviceId) {
157
+ const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
158
+ const json = JSON.parse(output);
159
+ return json.DeviceName;
160
+ }
161
+ async listDevices() {
162
+ if (!(await this.isGoIosInstalled())) {
163
+ console.error("go-ios is not installed, no physical iOS devices can be detected");
164
+ return [];
165
+ }
166
+ const output = (0, child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
167
+ const json = JSON.parse(output);
168
+ const devices = json.deviceList.map(async (device) => ({
169
+ deviceId: device,
170
+ deviceName: await this.getDeviceName(device),
171
+ }));
172
+ return Promise.all(devices);
173
+ }
174
+ }
175
+ exports.IosManager = IosManager;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SimctlManager = exports.Simctl = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const webdriver_agent_1 = require("./webdriver-agent");
6
+ const robot_1 = require("./robot");
7
+ const TIMEOUT = 30000;
8
+ const WDA_PORT = 8100;
9
+ const MAX_BUFFER_SIZE = 1024 * 1024 * 4;
10
+ class Simctl {
11
+ simulatorUuid;
12
+ constructor(simulatorUuid) {
13
+ this.simulatorUuid = simulatorUuid;
14
+ }
15
+ async wda() {
16
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
17
+ if (!(await wda.isRunning())) {
18
+ throw new robot_1.ActionableError("WebDriverAgent is not running on simulator, please see https://github.com/mobile-next/mobile-mcp/wiki/");
19
+ }
20
+ return wda;
21
+ }
22
+ simctl(...args) {
23
+ return (0, child_process_1.execFileSync)("xcrun", ["simctl", ...args], {
24
+ timeout: TIMEOUT,
25
+ maxBuffer: MAX_BUFFER_SIZE,
26
+ });
27
+ }
28
+ async getScreenshot() {
29
+ return this.simctl("io", this.simulatorUuid, "screenshot", "-");
30
+ }
31
+ async openUrl(url) {
32
+ const wda = await this.wda();
33
+ await wda.openUrl(url);
34
+ // alternative: this.simctl("openurl", this.simulatorUuid, url);
35
+ }
36
+ async launchApp(packageName) {
37
+ this.simctl("launch", this.simulatorUuid, packageName);
38
+ }
39
+ async terminateApp(packageName) {
40
+ this.simctl("terminate", this.simulatorUuid, packageName);
41
+ }
42
+ static parseIOSAppData(inputText) {
43
+ const result = [];
44
+ let ParseState;
45
+ (function (ParseState) {
46
+ ParseState[ParseState["LOOKING_FOR_APP"] = 0] = "LOOKING_FOR_APP";
47
+ ParseState[ParseState["IN_APP"] = 1] = "IN_APP";
48
+ ParseState[ParseState["IN_PROPERTY"] = 2] = "IN_PROPERTY";
49
+ })(ParseState || (ParseState = {}));
50
+ let state = ParseState.LOOKING_FOR_APP;
51
+ let currentApp = {};
52
+ let appIdentifier = "";
53
+ const lines = inputText.split("\n");
54
+ for (let line of lines) {
55
+ line = line.trim();
56
+ if (line === "") {
57
+ continue;
58
+ }
59
+ switch (state) {
60
+ case ParseState.LOOKING_FOR_APP:
61
+ // look for app identifier pattern: "com.example.app" = {
62
+ const appMatch = line.match(/^"?([^"=]+)"?\s*=\s*\{/);
63
+ if (appMatch) {
64
+ appIdentifier = appMatch[1].trim();
65
+ currentApp = {
66
+ CFBundleIdentifier: appIdentifier,
67
+ };
68
+ state = ParseState.IN_APP;
69
+ }
70
+ break;
71
+ case ParseState.IN_APP:
72
+ if (line === "};") {
73
+ result.push(currentApp);
74
+ currentApp = {};
75
+ state = ParseState.LOOKING_FOR_APP;
76
+ }
77
+ else {
78
+ // look for property: PropertyName = Value;
79
+ const propertyMatch = line.match(/^([^=]+)\s*=\s*(.+?);\s*$/);
80
+ if (propertyMatch) {
81
+ const propName = propertyMatch[1].trim();
82
+ let propValue = propertyMatch[2].trim();
83
+ // remove quotes if present (they're optional)
84
+ if (propValue.startsWith('"') && propValue.endsWith('"')) {
85
+ propValue = propValue.substring(1, propValue.length - 1);
86
+ }
87
+ // add property to current app
88
+ currentApp[propName] = propValue;
89
+ }
90
+ else if (line.endsWith("{")) {
91
+ // nested property like GroupContainers = {
92
+ state = ParseState.IN_PROPERTY;
93
+ }
94
+ }
95
+ break;
96
+ case ParseState.IN_PROPERTY:
97
+ if (line === "};") {
98
+ // end of nested property
99
+ state = ParseState.IN_APP;
100
+ }
101
+ // skip content of nested properties, we don't care of those right now
102
+ break;
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ async listApps() {
108
+ const text = this.simctl("listapps", this.simulatorUuid).toString();
109
+ const apps = Simctl.parseIOSAppData(text);
110
+ return apps.map(app => ({
111
+ packageName: app.CFBundleIdentifier,
112
+ appName: app.CFBundleDisplayName,
113
+ }));
114
+ }
115
+ async getScreenSize() {
116
+ const wda = await this.wda();
117
+ return wda.getScreenSize();
118
+ }
119
+ async sendKeys(keys) {
120
+ const wda = await this.wda();
121
+ return wda.sendKeys(keys);
122
+ }
123
+ async swipe(direction) {
124
+ const wda = await this.wda();
125
+ return wda.swipe(direction);
126
+ }
127
+ async tap(x, y) {
128
+ const wda = await this.wda();
129
+ return wda.tap(x, y);
130
+ }
131
+ async pressButton(button) {
132
+ const wda = await this.wda();
133
+ return wda.pressButton(button);
134
+ }
135
+ async getElementsOnScreen() {
136
+ const wda = await this.wda();
137
+ return wda.getElementsOnScreen();
138
+ }
139
+ async setOrientation(orientation) {
140
+ const wda = await this.wda();
141
+ return wda.setOrientation(orientation);
142
+ }
143
+ async getOrientation() {
144
+ const wda = await this.wda();
145
+ return wda.getOrientation();
146
+ }
147
+ }
148
+ exports.Simctl = Simctl;
149
+ class SimctlManager {
150
+ listSimulators() {
151
+ // detect if this is a mac
152
+ if (process.platform !== "darwin") {
153
+ // don't even try to run xcrun
154
+ return [];
155
+ }
156
+ try {
157
+ const text = (0, child_process_1.execFileSync)("xcrun", ["simctl", "list", "devices", "-j"]).toString();
158
+ const json = JSON.parse(text);
159
+ return Object.values(json.devices).flatMap(device => {
160
+ return device.map(d => {
161
+ return {
162
+ name: d.name,
163
+ uuid: d.udid,
164
+ state: d.state,
165
+ };
166
+ });
167
+ });
168
+ }
169
+ catch (error) {
170
+ console.error("Error listing simulators", error);
171
+ return [];
172
+ }
173
+ }
174
+ listBootedSimulators() {
175
+ return this.listSimulators()
176
+ .filter(simulator => simulator.state === "Booted");
177
+ }
178
+ getSimulator(uuid) {
179
+ return new Simctl(uuid);
180
+ }
181
+ }
182
+ exports.SimctlManager = SimctlManager;
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 fs_1 = require("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, 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;
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;