mobile-automator 0.29.0 → 0.30.0
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/README.md +3 -0
- package/package.json +6 -2
- package/src/cli.js +43 -6
- package/src/device/host-support.js +51 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Claude Code, Cursor, Gemini CLI, GitHub Copilot, and OpenAI Agents all have one-
|
|
|
8
8
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](#)
|
|
11
|
+
[](#)
|
|
11
12
|
[](#)
|
|
12
13
|
|
|
13
14
|
---
|
|
@@ -24,6 +25,8 @@ Claude Code, Cursor, Gemini CLI, GitHub Copilot, and OpenAI Agents all have one-
|
|
|
24
25
|
npm i -g mobile-automator # exposes `mauto` globally
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
> **Supported hosts:** macOS and Linux. Windows is not supported natively (npm refuses the install) — run `mauto` inside WSL2. See [TROUBLESHOOTING.md](TROUBLESHOOTING.md#-mauto-refuses-to-run-on-windows).
|
|
29
|
+
|
|
27
30
|
<details>
|
|
28
31
|
<summary>Or run it without installing / build from source</summary>
|
|
29
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-automator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
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": {
|
|
@@ -45,5 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22.12.0"
|
|
48
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"os": [
|
|
50
|
+
"darwin",
|
|
51
|
+
"linux"
|
|
52
|
+
]
|
|
49
53
|
}
|
package/src/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ const guideEmitter = require('./guide/emitter');
|
|
|
26
26
|
const { ADAPTERS } = require('./init/adapters');
|
|
27
27
|
const { isSemanticAction, ACTION_METHOD, selectResolver } = require('./device/semantic-press');
|
|
28
28
|
const connection = require('./device/connection');
|
|
29
|
+
const { isSupportedHost, unsupportedHostError } = require('./device/host-support');
|
|
29
30
|
const { record } = require('./observe/recorder');
|
|
30
31
|
const { resolveRunId } = require('./observe/settings');
|
|
31
32
|
const { runTracePath } = require('./observe/paths');
|
|
@@ -267,6 +268,19 @@ function handleValidate({ validator }, file, opts = {}) {
|
|
|
267
268
|
return { envelope: env, exitKind: 'invalid_input' };
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
// The unsupported-host gate (#165), shared by every site that reaches the
|
|
272
|
+
// session layer: connectBridge (all device verbs) and the `session` verbs,
|
|
273
|
+
// which talk to connection.* directly. Null on a supported host; otherwise an
|
|
274
|
+
// `environment` failure (exit 5). Deliberately NOT routed through deviceFail —
|
|
275
|
+
// a `device` kind is exactly the misdiagnosis this exists to prevent: on
|
|
276
|
+
// Windows the session layer's Unix socket, `ps` and process-group kill fail in
|
|
277
|
+
// ways that read as "no device connected".
|
|
278
|
+
function hostGate(platform) {
|
|
279
|
+
if (isSupportedHost(platform)) return null;
|
|
280
|
+
const err = unsupportedHostError(platform);
|
|
281
|
+
return { envelope: fail(err.kind, err.message, err.hint), exitKind: err.kind };
|
|
282
|
+
}
|
|
283
|
+
|
|
270
284
|
function deviceFail(err) {
|
|
271
285
|
// Respect a typed error's kind (e.g. the daemon's 'timeout') so the envelope
|
|
272
286
|
// and exit code reflect the real failure class; anything else defaults to
|
|
@@ -1576,6 +1590,8 @@ function buildProgram(deps = {}) {
|
|
|
1576
1590
|
emitters = makeEmitters({ projectRoot }),
|
|
1577
1591
|
// Sink used to emit the rendered envelope + drive the exit code.
|
|
1578
1592
|
emit = emitters.emit,
|
|
1593
|
+
// Host OS, for the unsupported-host gate (#165). Overridable in tests.
|
|
1594
|
+
platform = process.platform,
|
|
1579
1595
|
} = deps;
|
|
1580
1596
|
|
|
1581
1597
|
const program = new Command();
|
|
@@ -1672,6 +1688,18 @@ function buildProgram(deps = {}) {
|
|
|
1672
1688
|
return action(...args);
|
|
1673
1689
|
};
|
|
1674
1690
|
|
|
1691
|
+
// Gate for verbs that reach the device session layer (#165). Runs before
|
|
1692
|
+
// anything is spawned or connected; see hostGate. Emits and returns true when
|
|
1693
|
+
// the host is unsupported, so a caller can bail out.
|
|
1694
|
+
const refuseUnsupportedHost = () => {
|
|
1695
|
+
const refusal = hostGate(platform);
|
|
1696
|
+
if (!refusal) return false;
|
|
1697
|
+
emit(refusal, humanFlag());
|
|
1698
|
+
return true;
|
|
1699
|
+
};
|
|
1700
|
+
const requireSupportedHost = (action) => (...args) =>
|
|
1701
|
+
(refuseUnsupportedHost() ? undefined : action(...args));
|
|
1702
|
+
|
|
1675
1703
|
// Resolve which device a verb targets: explicit --device wins, else the
|
|
1676
1704
|
// persisted selection, else null (mobile-mcp auto-selects a single device).
|
|
1677
1705
|
// Keeping the no-selection fast path returning null preserves zero-config use.
|
|
@@ -1687,6 +1715,9 @@ function buildProgram(deps = {}) {
|
|
|
1687
1715
|
// (the single connection seam), so this keeps both the envelope boundary and
|
|
1688
1716
|
// the acquire-seam refactor.
|
|
1689
1717
|
const connectBridge = async (device, fn) => {
|
|
1718
|
+
// Before the factory: on an unsupported host there is nothing to connect,
|
|
1719
|
+
// and a connect failure would land below as a misleading `device` kind.
|
|
1720
|
+
if (refuseUnsupportedHost()) return;
|
|
1690
1721
|
let bridge;
|
|
1691
1722
|
let close;
|
|
1692
1723
|
try {
|
|
@@ -2106,26 +2137,26 @@ function buildProgram(deps = {}) {
|
|
|
2106
2137
|
.description('Start the device session daemon (subsequent verbs reuse its connection)')
|
|
2107
2138
|
.option('--device <id>', 'pin the daemon to a target device id')
|
|
2108
2139
|
.option('--idle <ms>', 'idle timeout in milliseconds before the daemon self-reaps')
|
|
2109
|
-
.action(withEnvelope(requireWorkspace(async (opts) => {
|
|
2140
|
+
.action(withEnvelope(requireSupportedHost(requireWorkspace(async (opts) => {
|
|
2110
2141
|
const r = await handleSessionStart({ projectRoot }, { device: opts.device, idle: opts.idle });
|
|
2111
2142
|
emit(r, humanFlag());
|
|
2112
|
-
})));
|
|
2143
|
+
}))));
|
|
2113
2144
|
|
|
2114
2145
|
session
|
|
2115
2146
|
.command('status')
|
|
2116
2147
|
.description('Report whether a device session daemon is running')
|
|
2117
|
-
.action(withEnvelope(async () => {
|
|
2148
|
+
.action(withEnvelope(requireSupportedHost(async () => {
|
|
2118
2149
|
const r = await handleSessionStatus({ projectRoot });
|
|
2119
2150
|
emit(r, humanFlag());
|
|
2120
|
-
}));
|
|
2151
|
+
})));
|
|
2121
2152
|
|
|
2122
2153
|
session
|
|
2123
2154
|
.command('end')
|
|
2124
2155
|
.description('Stop the device session daemon and remove its socket/pidfile')
|
|
2125
|
-
.action(withEnvelope(async () => {
|
|
2156
|
+
.action(withEnvelope(requireSupportedHost(async () => {
|
|
2126
2157
|
const r = await handleSessionEnd({ projectRoot });
|
|
2127
2158
|
emit(r, humanFlag());
|
|
2128
|
-
}));
|
|
2159
|
+
})));
|
|
2129
2160
|
|
|
2130
2161
|
// --- Issue #92: device discovery + persisted selection -------------------
|
|
2131
2162
|
|
|
@@ -2150,6 +2181,12 @@ function buildProgram(deps = {}) {
|
|
|
2150
2181
|
devices
|
|
2151
2182
|
.command('clear')
|
|
2152
2183
|
.description('Remove the persisted device selection')
|
|
2184
|
+
// Deliberately OUTSIDE the host gate (#165): its only session-layer probe,
|
|
2185
|
+
// sessionClient.isAlive, is a socket ping — it never runs `ps` or signals a
|
|
2186
|
+
// process, so it answers safely on an unsupported host too. That invariant
|
|
2187
|
+
// is executable, not just prose: the canary in
|
|
2188
|
+
// tests/unit/cli-host-support.test.js ("devices clear stays process-free")
|
|
2189
|
+
// fails if this verb ever starts spawning child processes.
|
|
2153
2190
|
.action(withEnvelope(async () => {
|
|
2154
2191
|
const r = await handleDevicesClear({ projectRoot });
|
|
2155
2192
|
emit(r, humanFlag());
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Which hosts can run the device session, and the error for the ones that
|
|
4
|
+
// cannot (#165). The session daemon binds a Unix domain socket, and the
|
|
5
|
+
// session layer identifies and reaps its daemon with `ps`, process-group kill
|
|
6
|
+
// (`kill(-pid)`) and POSIX signals. None of that exists on Windows, where it
|
|
7
|
+
// used to fail as a confusing `device` error — as if no phone were connected —
|
|
8
|
+
// rather than saying the host itself is unsupported.
|
|
9
|
+
//
|
|
10
|
+
// package.json `os` is the single source of truth: npm refuses the install on
|
|
11
|
+
// any other host, and this guard catches what slips past it (`--force`,
|
|
12
|
+
// `npm link`, a vendored copy). Pure — no I/O; the caller decides how to emit.
|
|
13
|
+
|
|
14
|
+
const pkg = require('../../package.json');
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_HOSTS = Object.freeze([...(pkg.os || [])]);
|
|
17
|
+
|
|
18
|
+
// Human names for the error text — the ONE copy; the docs-agreement lint test
|
|
19
|
+
// (tests/lint/host-support-agreement.test.js) consumes it rather than
|
|
20
|
+
// restating it. Deliberately no fallback: a host added to package.json `os`
|
|
21
|
+
// without a name here is a test failure and a crash at render time, not a raw
|
|
22
|
+
// platform id leaking into the user-facing error.
|
|
23
|
+
const HOST_NAMES = Object.freeze({ darwin: 'macOS', linux: 'Linux' });
|
|
24
|
+
|
|
25
|
+
const TROUBLESHOOTING_REF = 'see TROUBLESHOOTING.md ("mauto refuses to run on Windows")';
|
|
26
|
+
|
|
27
|
+
// Per-host guidance. Unknown hosts get the generic pointer instead of a
|
|
28
|
+
// Windows-specific instruction — a freebsd user must not be told about WSL2.
|
|
29
|
+
const HOST_HINTS = Object.freeze({
|
|
30
|
+
win32: `On Windows, install and run mauto inside WSL2 (a Linux distribution); ${TROUBLESHOOTING_REF}.`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// For hosts with no specific hint: point at the troubleshooting entry without
|
|
34
|
+
// naming an OS-specific route.
|
|
35
|
+
const GENERIC_HINT = `See TROUBLESHOOTING.md for where mauto runs and how to get there.`;
|
|
36
|
+
|
|
37
|
+
function isSupportedHost(platform = process.platform) {
|
|
38
|
+
return SUPPORTED_HOSTS.includes(platform);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unsupportedHostError(platform = process.platform) {
|
|
42
|
+
const names = SUPPORTED_HOSTS.map((p) => HOST_NAMES[p]).join(' or ');
|
|
43
|
+
const err = new Error(
|
|
44
|
+
`mauto's device session needs a POSIX host (${names}); "${platform}" is not supported`
|
|
45
|
+
);
|
|
46
|
+
err.kind = 'environment';
|
|
47
|
+
err.hint = HOST_HINTS[platform] || GENERIC_HINT;
|
|
48
|
+
return err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { SUPPORTED_HOSTS, HOST_NAMES, isSupportedHost, unsupportedHostError };
|