mobile-automator 0.28.0 → 0.28.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-automator",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "Host-agnostic mauto CLI for AI-driven mobile QA automation — analyzes a mobile project and drives devices through mobile-mcp.",
|
|
5
5
|
"homepage": "https://github.com/sh3lan93/mobile-automator#readme",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jest": "^29.7.0",
|
|
43
43
|
"jest-environment-jsdom": "^29.7.0",
|
|
44
|
-
"js-yaml": "^4.3.2"
|
|
45
|
-
"toml": "^3.0.0"
|
|
44
|
+
"js-yaml": "^4.3.2"
|
|
46
45
|
},
|
|
47
46
|
"engines": {
|
|
48
47
|
"node": ">=22.12.0"
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
const { Command } = require('commander');
|
|
5
6
|
|
|
6
7
|
const { version: PKG_VERSION } = require('../package.json');
|
|
@@ -190,9 +191,12 @@ async function handleElements({ deviceBridge }) {
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
// Relative paths resolve against the caller's cwd HERE, not in the daemon: the
|
|
195
|
+
// daemon runs from the project root, which is not where a verb run from a
|
|
196
|
+
// subdirectory meant `./shot.png` (#214).
|
|
197
|
+
async function handleScreenshot({ deviceBridge, cwd = process.cwd() }, destPath) {
|
|
194
198
|
try {
|
|
195
|
-
const savedPath = await deviceBridge.screenshot(destPath);
|
|
199
|
+
const savedPath = await deviceBridge.screenshot(path.resolve(cwd, destPath));
|
|
196
200
|
return { envelope: ok({ path: savedPath }), exitKind: 'ok' };
|
|
197
201
|
} catch (err) {
|
|
198
202
|
return {
|
|
@@ -380,7 +384,10 @@ async function handleLaunch({ deviceBridge }, appId) {
|
|
|
380
384
|
}
|
|
381
385
|
}
|
|
382
386
|
|
|
383
|
-
|
|
387
|
+
// Resolved against the caller's cwd for the same reason as handleScreenshot
|
|
388
|
+
// (#214). The empty-path guard runs first: path.resolve(cwd, '') is the cwd
|
|
389
|
+
// itself, which would turn a missing argument into a bogus install attempt.
|
|
390
|
+
async function handleInstall({ deviceBridge, cwd = process.cwd() }, appPath) {
|
|
384
391
|
const p = String(appPath == null ? '' : appPath);
|
|
385
392
|
if (!p) {
|
|
386
393
|
return {
|
|
@@ -388,9 +395,10 @@ async function handleInstall({ deviceBridge }, appPath) {
|
|
|
388
395
|
exitKind: 'invalid_input',
|
|
389
396
|
};
|
|
390
397
|
}
|
|
398
|
+
const abs = path.resolve(cwd, p);
|
|
391
399
|
try {
|
|
392
|
-
await deviceBridge.installApp(
|
|
393
|
-
return { envelope: ok({ installed:
|
|
400
|
+
await deviceBridge.installApp(abs);
|
|
401
|
+
return { envelope: ok({ installed: abs }), exitKind: 'ok' };
|
|
394
402
|
} catch (err) {
|
|
395
403
|
return deviceFail(err);
|
|
396
404
|
}
|
|
@@ -1807,7 +1815,7 @@ function buildProgram(deps = {}) {
|
|
|
1807
1815
|
.command('install <path>')
|
|
1808
1816
|
.description('Install an app from a local .apk/.app/.ipa path')
|
|
1809
1817
|
.option('--device <id>', 'target device id')
|
|
1810
|
-
.action(withEnvelope((
|
|
1818
|
+
.action(withEnvelope((appPath, opts) => withBridge(opts.device, (bridge) => handleInstall({ deviceBridge: bridge }, appPath))));
|
|
1811
1819
|
|
|
1812
1820
|
program
|
|
1813
1821
|
.command('uninstall <appId>')
|
|
@@ -66,6 +66,14 @@ function deviceNotFoundHint(id) {
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// B3 (#214): mobile-mcp's validateOutputPath only allows screenshots under the
|
|
70
|
+
// engine's cwd or the temp dir, and its message calls the former "the current
|
|
71
|
+
// directory". The engine runs from the project root (the daemon is spawned
|
|
72
|
+
// there), which is not necessarily the directory the user ran the verb from.
|
|
73
|
+
const OUTSIDE_ALLOWED_DIRS_HINT =
|
|
74
|
+
'Screenshots must be saved under the project root (the directory containing mobile-automator/) ' +
|
|
75
|
+
'or the system temp directory. Choose a path inside one of them and retry.';
|
|
76
|
+
|
|
69
77
|
function engineHint(message, { mobilecliVersion = resolveMobilecliVersion } = {}) {
|
|
70
78
|
try {
|
|
71
79
|
if (typeof message !== 'string' || !message) return null;
|
|
@@ -74,6 +82,7 @@ function engineHint(message, { mobilecliVersion = resolveMobilecliVersion } = {}
|
|
|
74
82
|
}
|
|
75
83
|
const notFound = /Device "([^"]+)" not found/.exec(message);
|
|
76
84
|
if (notFound) return deviceNotFoundHint(notFound[1]);
|
|
85
|
+
if (/not in the list of allowed directories/.test(message)) return OUTSIDE_ALLOWED_DIRS_HINT;
|
|
77
86
|
return null;
|
|
78
87
|
} catch {
|
|
79
88
|
return null;
|
|
@@ -46,6 +46,31 @@ function resolveServerEntry() {
|
|
|
46
46
|
return path.join(pkgDir, binRel);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// The environment mauto's own engine process is spawned with. Pure: returns a
|
|
50
|
+
// new object and never mutates baseEnv.
|
|
51
|
+
//
|
|
52
|
+
// MOBILEMCP_ALLOW_UNSAFE_URLS defaults to "1" (#212). mobile-mcp rejects every
|
|
53
|
+
// non-http(s) URL in mobile_open_url unless it is set, which blocks the
|
|
54
|
+
// documented `open_url` deep-link capability (myapp://...). That guard exists to
|
|
55
|
+
// stop an LLM whose ONLY reach is MCP from opening arbitrary schemes; an agent
|
|
56
|
+
// driving mauto already has a shell and could open the same URL via adb or
|
|
57
|
+
// simctl, so under mauto it protects nothing. The URL reaches the device as an
|
|
58
|
+
// argv element on the default mobilecli robot (no shell), and the legacy adb
|
|
59
|
+
// robot escapes shell metacharacters. A user-set value wins: the engine only
|
|
60
|
+
// accepts the exact string "1", so MOBILEMCP_ALLOW_UNSAFE_URLS=0 restores the
|
|
61
|
+
// guard. The env is read when the engine spawns, so a running session daemon
|
|
62
|
+
// must be ended (`mauto session end`) before a change takes effect.
|
|
63
|
+
function buildEngineEnv({ device = null, baseEnv = process.env } = {}) {
|
|
64
|
+
const env = {
|
|
65
|
+
...baseEnv,
|
|
66
|
+
MOBILEMCP_ALLOW_UNSAFE_URLS: baseEnv.MOBILEMCP_ALLOW_UNSAFE_URLS ?? '1',
|
|
67
|
+
};
|
|
68
|
+
// mobile-mcp reads the target device from the environment / its own
|
|
69
|
+
// selection; we pass it through when provided so callers can pin a device.
|
|
70
|
+
if (device) env.MOBILE_MCP_DEVICE = device;
|
|
71
|
+
return env;
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
// The SDK transport, remembering the engine pid the moment the child is spawned
|
|
50
75
|
// (#156). It has to be remembered: on a failed connect the SDK runs
|
|
51
76
|
// `void this.close()`, and StdioClientTransport.close() clears its pid
|
|
@@ -84,9 +109,7 @@ async function createCall({
|
|
|
84
109
|
const transport = new Transport({
|
|
85
110
|
command,
|
|
86
111
|
args,
|
|
87
|
-
|
|
88
|
-
// selection; we pass it through when provided so callers can pin a device.
|
|
89
|
-
env: device ? { ...process.env, MOBILE_MCP_DEVICE: device } : process.env,
|
|
112
|
+
env: buildEngineEnv({ device }),
|
|
90
113
|
});
|
|
91
114
|
|
|
92
115
|
const client = new sdk.Client({ name: 'mauto', version: '0.1.0' }, { capabilities: {} });
|
|
@@ -255,4 +278,5 @@ module.exports = {
|
|
|
255
278
|
resolveServerEntry,
|
|
256
279
|
CONNECT_TIMEOUT_MS,
|
|
257
280
|
ENGINE_REAP_MAX_MS,
|
|
281
|
+
buildEngineEnv,
|
|
258
282
|
};
|
|
@@ -54,7 +54,12 @@ async function spawnDaemon({
|
|
|
54
54
|
try {
|
|
55
55
|
// The root in argv is an identity marker only (see paths.daemonArgv): env
|
|
56
56
|
// stays the daemon's source of truth, but `ps` can only see argv.
|
|
57
|
+
// cwd: the engine inherits the daemon's cwd, and mobile-mcp's
|
|
58
|
+
// validateOutputPath only allows screenshots under its own process.cwd()
|
|
59
|
+
// (or the temp dir). Pin it to the project root so the allow-list never
|
|
60
|
+
// depends on which subdirectory happened to spawn the daemon (#214).
|
|
57
61
|
const child = spawn(process.execPath, [DAEMON_BIN, ...paths.daemonArgv(projectRoot)], {
|
|
62
|
+
cwd: projectRoot,
|
|
58
63
|
detached: true,
|
|
59
64
|
stdio: log.stdio,
|
|
60
65
|
env,
|
|
@@ -112,7 +112,7 @@ Translate user language to action types and `mauto` verbs:
|
|
|
112
112
|
| **"allow permission", "grant the permission"** | **`grant_permission`** | **`mauto press grant_permission`** | **Semantic — set optional `permission_name` if specific.** |
|
|
113
113
|
| **"deny permission", "don't allow"** | **`deny_permission`** | **`mauto press deny_permission`** | **Semantic.** |
|
|
114
114
|
| "navigate to [tab]" | `tap` | `mauto elements` to find the tab, then `mauto tap --at <x,y>` | |
|
|
115
|
-
| "open URL", "navigate to URL" | `open_url` | `mauto
|
|
115
|
+
| "open URL", "open deep link", "navigate to URL" | `open_url` | `mauto open-url <url>` | |
|
|
116
116
|
| "wait until visible" | `wait_for_element` | Poll `mauto elements` until present | Set `wait_config.type: "element_visible"` |
|
|
117
117
|
| "wait until gone" | `wait_for_element_gone` | Poll `mauto elements` until absent | |
|
|
118
118
|
| "wait until loaded", "wait for shimmer to stop" | `wait_for_loading_complete` | Poll `mauto screenshot <path>` + visual check | Set `wait_config.indicator` to match project's loading style |
|
|
@@ -241,7 +241,7 @@ Translate user language to `mauto` verbs and schema actions:
|
|
|
241
241
|
| "scroll to", "scroll until visible", "scroll down to find" | `scroll_to_element` | `mauto swipe --direction <dir>` (repeated) | Target is the element to scroll to |
|
|
242
242
|
| "go back", "press back" | `press_button` | `mauto press <button>` | Use the back button |
|
|
243
243
|
| "navigate to [tab]" | `tap` | `mauto elements` to find the tab, then `mauto tap --at <x,y>` | |
|
|
244
|
-
| "open URL", "navigate to URL" | `open_url` | `mauto
|
|
244
|
+
| "open URL", "open deep link", "navigate to URL" | `open_url` | `mauto open-url <url>` | |
|
|
245
245
|
| "wait until visible", "wait for [element] to appear" | `wait_for_element` | Poll `mauto elements` until present | Set `wait_config.type: "element_visible"` |
|
|
246
246
|
| "wait until gone", "wait for [element] to disappear" | `wait_for_element_gone` | Poll `mauto elements` until absent | Set `wait_config.type: "element_gone"` |
|
|
247
247
|
| "wait until loaded", "wait for shimmer to stop", "wait for loading to complete" | `wait_for_loading_complete` | Poll `mauto screenshot <path>` + visual check | Set `wait_config.indicator` to match project's loading style (shimmer/spinner/skeleton) |
|