adb-ready 0.6.0 → 0.8.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/CHANGELOG.md +39 -1
- package/COMPATIBILITY.md +31 -0
- package/README.md +18 -1
- package/dist/cli.js +2730 -80
- package/docs/automation.md +89 -0
- package/docs/completions.md +61 -0
- package/docs/configuration.md +6 -0
- package/docs/firebase-test-lab.md +126 -0
- package/docs/gradle-managed-devices.md +105 -0
- package/docs/target-pools.md +151 -0
- package/docs/targets-and-wireless.md +21 -0
- package/docs/troubleshooting.md +3 -0
- package/examples/README.md +6 -0
- package/examples/ci-emulator/README.md +32 -0
- package/examples/ci-emulator/adb-ready.config.json +20 -0
- package/examples/ci-emulator/dev-service.mjs +14 -0
- package/examples/ci-emulator/verify-emulator.mjs +108 -0
- package/examples/ci-physical/README.md +51 -0
- package/examples/ci-physical/adb-ready.config.json +20 -0
- package/examples/ci-physical/dev-service.mjs +14 -0
- package/examples/ci-physical/github-actions.yml +56 -0
- package/examples/ci-physical/run-job.mjs +77 -0
- package/examples/ci-physical/verify-device.mjs +90 -0
- package/examples/target-pools/README.md +22 -0
- package/examples/target-pools/adb-ready.config.json +29 -0
- package/package.json +2 -2
- package/schema/agent-tools-v1.json +1 -1
- package/schema/config-v1.schema.json +86 -0
- package/skills/adb-ready/SKILL.md +2 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let stopping = false;
|
|
2
|
+
|
|
3
|
+
const keepAlive = setInterval(() => undefined, 60_000);
|
|
4
|
+
|
|
5
|
+
function stop() {
|
|
6
|
+
if (stopping) return;
|
|
7
|
+
stopping = true;
|
|
8
|
+
process.stderr.write("CI development service stopped.\n");
|
|
9
|
+
clearInterval(keepAlive);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process.once("SIGINT", stop);
|
|
13
|
+
process.once("SIGTERM", stop);
|
|
14
|
+
process.stderr.write("CI development service ready.\n");
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
8
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {NodeJS.ProcessEnv} env
|
|
12
|
+
* @param {string} name
|
|
13
|
+
*/
|
|
14
|
+
function requiredEnvironment(env, name) {
|
|
15
|
+
const value = env[name];
|
|
16
|
+
if (typeof value !== "string" || value.trim() === "" || /[\r\n\0]/u.test(value)) {
|
|
17
|
+
throw new Error(`${name} must be a non-empty single-line value.`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} executable
|
|
24
|
+
* @param {string} serial
|
|
25
|
+
* @param {string[]} args
|
|
26
|
+
*/
|
|
27
|
+
async function defaultAdb(executable, serial, args) {
|
|
28
|
+
const result = await execFile(executable, ["-s", serial, ...args], {
|
|
29
|
+
encoding: "buffer",
|
|
30
|
+
maxBuffer: 12 * 1024 * 1024,
|
|
31
|
+
timeout: 30_000,
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
return Buffer.from(result.stdout);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{
|
|
39
|
+
* env?: NodeJS.ProcessEnv;
|
|
40
|
+
* runAdb?: (args: string[]) => Promise<Buffer>;
|
|
41
|
+
* }} [options]
|
|
42
|
+
*/
|
|
43
|
+
export async function verifyEmulator(options = {}) {
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const serial = requiredEnvironment(env, "ADB_READY_TARGET_SERIAL");
|
|
46
|
+
const androidSerial = requiredEnvironment(env, "ANDROID_SERIAL");
|
|
47
|
+
const outputDirectory = requiredEnvironment(env, "ADB_READY_VERIFIER_OUTPUT_DIR");
|
|
48
|
+
if (serial !== androidSerial) {
|
|
49
|
+
throw new Error("ADB Ready did not bind the verifier to one target.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const executable = env.ADB_READY_ADB_PATH?.trim() || "adb";
|
|
53
|
+
/** @param {string[]} args */
|
|
54
|
+
const runDefaultAdb = (args) => defaultAdb(executable, serial, args);
|
|
55
|
+
const runAdb = options.runAdb ?? runDefaultAdb;
|
|
56
|
+
/** @param {string[]} args */
|
|
57
|
+
const text = async (args) => (await runAdb(args)).toString("utf8").trim();
|
|
58
|
+
|
|
59
|
+
if ((await text(["get-state"])) !== "device") {
|
|
60
|
+
throw new Error("The selected emulator is not in the device state.");
|
|
61
|
+
}
|
|
62
|
+
if ((await text(["shell", "getprop", "sys.boot_completed"])) !== "1") {
|
|
63
|
+
throw new Error("The selected emulator did not complete Android boot.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const apiLevel = await text(["shell", "getprop", "ro.build.version.sdk"]);
|
|
67
|
+
const abi = await text(["shell", "getprop", "ro.product.cpu.abi"]);
|
|
68
|
+
if (!/^\d+$/u.test(apiLevel) || abi === "") {
|
|
69
|
+
throw new Error("The selected emulator did not expose a valid Android identity.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await runAdb(["shell", "am", "start", "-W", "-a", "android.settings.SETTINGS"]);
|
|
73
|
+
const windowState = await runAdb(["shell", "dumpsys", "window", "displays"]);
|
|
74
|
+
const screenshot = await runAdb(["exec-out", "screencap", "-p"]);
|
|
75
|
+
if (
|
|
76
|
+
screenshot.length <= PNG_SIGNATURE.length ||
|
|
77
|
+
!screenshot.subarray(0, 8).equals(PNG_SIGNATURE)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error("The selected emulator did not return a valid PNG screenshot.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const destination = path.resolve(outputDirectory);
|
|
83
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
84
|
+
await Promise.all([
|
|
85
|
+
writeFile(path.join(destination, "screen.png"), screenshot, { mode: 0o600 }),
|
|
86
|
+
writeFile(path.join(destination, "window.txt"), windowState, { mode: 0o600 }),
|
|
87
|
+
writeFile(
|
|
88
|
+
path.join(destination, "verification.json"),
|
|
89
|
+
`${JSON.stringify({ schemaVersion: 1, targetBound: true, state: "device", bootCompleted: true, apiLevel, abi, screenshot: "screen.png" }, null, 2)}\n`,
|
|
90
|
+
{ mode: 0o600 },
|
|
91
|
+
),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
return { apiLevel, abi, screenshotBytes: screenshot.length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const direct = process.argv[1] === fileURLToPath(import.meta.url);
|
|
98
|
+
if (direct) {
|
|
99
|
+
try {
|
|
100
|
+
const result = await verifyEmulator();
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
`Verified the selected Android emulator (API ${result.apiLevel}, ${result.abi}) with a ${String(result.screenshotBytes)} byte screenshot.\n`,
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Self-hosted physical Android target
|
|
2
|
+
|
|
3
|
+
This recipe runs one finite ADB Ready verification against one physical Android
|
|
4
|
+
target attached to a self-hosted GitHub Actions runner. It is intentionally a
|
|
5
|
+
manual workflow: never let untrusted pull-request code execute on a machine
|
|
6
|
+
that can control a real device.
|
|
7
|
+
|
|
8
|
+
## Runner contract
|
|
9
|
+
|
|
10
|
+
1. Register a dedicated self-hosted runner and add the custom
|
|
11
|
+
`android-device` label.
|
|
12
|
+
2. Install a current Android SDK Platform-Tools release and make `adb`
|
|
13
|
+
available to the runner account.
|
|
14
|
+
3. Enable USB debugging, connect the target, unlock it, and approve that
|
|
15
|
+
runner's RSA key before unattended work begins.
|
|
16
|
+
4. Set the repository variable `ADB_READY_DEVICE_SERIAL` to the exact serial
|
|
17
|
+
shown by `adb devices -l`.
|
|
18
|
+
5. Copy [`github-actions.yml`](./github-actions.yml) into your repository's
|
|
19
|
+
`.github/workflows/` directory and adapt the project command, readiness, and
|
|
20
|
+
verifier to your application.
|
|
21
|
+
|
|
22
|
+
The example uses a static GitHub concurrency group with cancellation disabled,
|
|
23
|
+
so jobs queue instead of interrupting a device already in use. ADB Ready then
|
|
24
|
+
acquires its host-local target lease before any mutation. Another ADB Ready
|
|
25
|
+
process on the same runner receives `TARGET_BUSY`; it cannot share the phone.
|
|
26
|
+
The operator-provided runner label and serial are the physical-device contract;
|
|
27
|
+
the verifier does not guess form factor from undocumented Android properties.
|
|
28
|
+
|
|
29
|
+
## Failure and recovery
|
|
30
|
+
|
|
31
|
+
- A missing serial or detached phone fails before a verifier starts.
|
|
32
|
+
- `unauthorized` asks the operator to unlock the device and approve the RSA
|
|
33
|
+
prompt; ADB Ready does not bypass Android authorization.
|
|
34
|
+
- `offline` remains distinct from an absent device and is reported with an
|
|
35
|
+
actionable target diagnostic.
|
|
36
|
+
- A normal exit, verifier failure, timeout, or cancellation releases only the
|
|
37
|
+
current ADB Ready lease and resources created by that run.
|
|
38
|
+
- If the runner process disappears, a later process on the same host can
|
|
39
|
+
recover its dead-owner lease. After a host crash, the bounded lease TTL makes
|
|
40
|
+
stale ownership recoverable.
|
|
41
|
+
|
|
42
|
+
The recipe never calls `adb kill-server`, reconnects unrelated transports,
|
|
43
|
+
unlocks the phone, clears app data, or resets the device. Its verifier is
|
|
44
|
+
read-only and deliberately avoids screenshots because a personal device screen
|
|
45
|
+
can contain sensitive information. Project-specific test artifacts remain
|
|
46
|
+
local sensitive evidence and are uploaded for seven days by the example.
|
|
47
|
+
|
|
48
|
+
GitHub concurrency protects this one workflow. The ADB Ready lease protects
|
|
49
|
+
processes sharing one host. Neither is a distributed lock across multiple
|
|
50
|
+
runner machines; use one physical target per runner label until an explicit
|
|
51
|
+
shared coordination backend is configured.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/config-v1.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"dev": {
|
|
5
|
+
"preset": "custom",
|
|
6
|
+
"command": {
|
|
7
|
+
"executable": "node",
|
|
8
|
+
"args": ["examples/ci-physical/dev-service.mjs"]
|
|
9
|
+
},
|
|
10
|
+
"reversePorts": [],
|
|
11
|
+
"logs": false,
|
|
12
|
+
"cleanupPorts": true,
|
|
13
|
+
"watch": true,
|
|
14
|
+
"ready": {
|
|
15
|
+
"timeoutMs": 60000,
|
|
16
|
+
"pollIntervalMs": 1000,
|
|
17
|
+
"all": [{ "kind": "boot" }]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let stopping = false;
|
|
2
|
+
|
|
3
|
+
const keepAlive = setInterval(() => undefined, 60_000);
|
|
4
|
+
|
|
5
|
+
function stop() {
|
|
6
|
+
if (stopping) return;
|
|
7
|
+
stopping = true;
|
|
8
|
+
process.stderr.write("Physical-device CI service stopped.\n");
|
|
9
|
+
clearInterval(keepAlive);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
process.once("SIGINT", stop);
|
|
13
|
+
process.once("SIGTERM", stop);
|
|
14
|
+
process.stderr.write("Physical-device CI service ready.\n");
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Physical Android acceptance
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
|
|
9
|
+
# Queue jobs at GitHub's runner boundary. ADB Ready's host-local lease remains
|
|
10
|
+
# the final authority when another local process uses the same target.
|
|
11
|
+
concurrency:
|
|
12
|
+
group: physical-android-device
|
|
13
|
+
cancel-in-progress: false
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
finite-run:
|
|
17
|
+
name: Target-bound physical run
|
|
18
|
+
runs-on: [self-hosted, android-device]
|
|
19
|
+
timeout-minutes: 20
|
|
20
|
+
env:
|
|
21
|
+
ADB_READY_DEVICE_SERIAL: ${{ vars.ADB_READY_DEVICE_SERIAL }}
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Check out repository
|
|
25
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
26
|
+
with:
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
|
|
29
|
+
- name: Set up Node.js 22
|
|
30
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
31
|
+
with:
|
|
32
|
+
node-version: 22
|
|
33
|
+
package-manager-cache: false
|
|
34
|
+
|
|
35
|
+
- name: Set up Bun from packageManager
|
|
36
|
+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
37
|
+
|
|
38
|
+
- name: Install and build ADB Ready
|
|
39
|
+
run: |
|
|
40
|
+
bun install --frozen-lockfile
|
|
41
|
+
bun run build
|
|
42
|
+
|
|
43
|
+
- name: Run one bounded physical-device verification
|
|
44
|
+
run: node examples/ci-physical/run-job.mjs
|
|
45
|
+
|
|
46
|
+
- name: Preserve normalized result and evidence
|
|
47
|
+
if: always()
|
|
48
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
49
|
+
with:
|
|
50
|
+
name: adb-ready-physical-evidence
|
|
51
|
+
path: |
|
|
52
|
+
.adb-ready/ci-result.ndjson
|
|
53
|
+
.adb-ready/artifacts/**
|
|
54
|
+
include-hidden-files: true
|
|
55
|
+
if-no-files-found: warn
|
|
56
|
+
retention-days: 7
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, mkdirSync, openSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const RESULT_PATH = path.join(".adb-ready", "ci-result.ndjson");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {NodeJS.ProcessEnv} env
|
|
10
|
+
*/
|
|
11
|
+
export function physicalRunArguments(env = process.env) {
|
|
12
|
+
const serial = env.ADB_READY_DEVICE_SERIAL;
|
|
13
|
+
if (typeof serial !== "string" || serial.trim() === "" || /[\r\n\0]/u.test(serial)) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"ADB_READY_DEVICE_SERIAL must name one pre-authorized Android target on this runner.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
"dist/cli.js",
|
|
20
|
+
"run",
|
|
21
|
+
"--config",
|
|
22
|
+
"examples/ci-physical/adb-ready.config.json",
|
|
23
|
+
"--device",
|
|
24
|
+
serial,
|
|
25
|
+
"--run-timeout",
|
|
26
|
+
"10m",
|
|
27
|
+
"--format",
|
|
28
|
+
"ndjson",
|
|
29
|
+
"--non-interactive",
|
|
30
|
+
"--",
|
|
31
|
+
process.execPath,
|
|
32
|
+
"examples/ci-physical/verify-device.mjs",
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {NodeJS.ProcessEnv} env */
|
|
37
|
+
export async function runPhysicalJob(env = process.env) {
|
|
38
|
+
const args = physicalRunArguments(env);
|
|
39
|
+
mkdirSync(path.dirname(RESULT_PATH), { recursive: true, mode: 0o700 });
|
|
40
|
+
const output = openSync(RESULT_PATH, "w", 0o600);
|
|
41
|
+
const child = spawn(process.execPath, args, {
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
env,
|
|
44
|
+
stdio: ["ignore", output, "inherit"],
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** @param {NodeJS.Signals} signal */
|
|
49
|
+
const forward = (signal) => {
|
|
50
|
+
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
51
|
+
};
|
|
52
|
+
const forwardInterrupt = () => forward("SIGINT");
|
|
53
|
+
const forwardTermination = () => forward("SIGTERM");
|
|
54
|
+
process.once("SIGINT", forwardInterrupt);
|
|
55
|
+
process.once("SIGTERM", forwardTermination);
|
|
56
|
+
try {
|
|
57
|
+
return await new Promise((resolve, reject) => {
|
|
58
|
+
child.once("error", reject);
|
|
59
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
60
|
+
});
|
|
61
|
+
} finally {
|
|
62
|
+
process.removeListener("SIGINT", forwardInterrupt);
|
|
63
|
+
process.removeListener("SIGTERM", forwardTermination);
|
|
64
|
+
closeSync(output);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const direct = process.argv[1] === fileURLToPath(import.meta.url);
|
|
69
|
+
if (direct) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await runPhysicalJob();
|
|
72
|
+
process.exitCode = result.code ?? (result.signal === "SIGINT" ? 130 : 1);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
75
|
+
process.exitCode = 2;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {NodeJS.ProcessEnv} env
|
|
11
|
+
* @param {string} name
|
|
12
|
+
*/
|
|
13
|
+
function requiredEnvironment(env, name) {
|
|
14
|
+
const value = env[name];
|
|
15
|
+
if (typeof value !== "string" || value.trim() === "" || /[\r\n\0]/u.test(value)) {
|
|
16
|
+
throw new Error(`${name} must be a non-empty single-line value.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} executable
|
|
23
|
+
* @param {string} serial
|
|
24
|
+
* @param {string[]} args
|
|
25
|
+
*/
|
|
26
|
+
async function defaultAdb(executable, serial, args) {
|
|
27
|
+
const result = await execFile(executable, ["-s", serial, ...args], {
|
|
28
|
+
encoding: "buffer",
|
|
29
|
+
maxBuffer: 1024 * 1024,
|
|
30
|
+
timeout: 30_000,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
return Buffer.from(result.stdout);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{
|
|
38
|
+
* env?: NodeJS.ProcessEnv;
|
|
39
|
+
* runAdb?: (args: string[]) => Promise<Buffer>;
|
|
40
|
+
* }} [options]
|
|
41
|
+
*/
|
|
42
|
+
export async function verifyPhysicalDevice(options = {}) {
|
|
43
|
+
const env = options.env ?? process.env;
|
|
44
|
+
const serial = requiredEnvironment(env, "ADB_READY_TARGET_SERIAL");
|
|
45
|
+
const androidSerial = requiredEnvironment(env, "ANDROID_SERIAL");
|
|
46
|
+
const outputDirectory = requiredEnvironment(env, "ADB_READY_VERIFIER_OUTPUT_DIR");
|
|
47
|
+
if (serial !== androidSerial) {
|
|
48
|
+
throw new Error("ADB Ready did not bind the verifier to one target.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const executable = env.ADB_READY_ADB_PATH?.trim() || "adb";
|
|
52
|
+
/** @param {string[]} args */
|
|
53
|
+
const runDefaultAdb = (args) => defaultAdb(executable, serial, args);
|
|
54
|
+
const runAdb = options.runAdb ?? runDefaultAdb;
|
|
55
|
+
/** @param {string[]} args */
|
|
56
|
+
const text = async (args) => (await runAdb(args)).toString("utf8").trim();
|
|
57
|
+
|
|
58
|
+
if ((await text(["get-state"])) !== "device") {
|
|
59
|
+
throw new Error("The selected physical target is not in the device state.");
|
|
60
|
+
}
|
|
61
|
+
if ((await text(["shell", "getprop", "sys.boot_completed"])) !== "1") {
|
|
62
|
+
throw new Error("The selected physical target did not complete Android boot.");
|
|
63
|
+
}
|
|
64
|
+
const apiLevel = await text(["shell", "getprop", "ro.build.version.sdk"]);
|
|
65
|
+
if (!/^\d+$/u.test(apiLevel)) {
|
|
66
|
+
throw new Error("The selected physical target did not expose a valid Android API level.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const destination = path.resolve(outputDirectory);
|
|
70
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
71
|
+
await writeFile(
|
|
72
|
+
path.join(destination, "verification.json"),
|
|
73
|
+
`${JSON.stringify({ schemaVersion: 1, targetBound: true, state: "device", bootCompleted: true, apiLevel }, null, 2)}\n`,
|
|
74
|
+
{ mode: 0o600 },
|
|
75
|
+
);
|
|
76
|
+
return { apiLevel };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const direct = process.argv[1] === fileURLToPath(import.meta.url);
|
|
80
|
+
if (direct) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await verifyPhysicalDevice();
|
|
83
|
+
process.stdout.write(
|
|
84
|
+
`Verified the selected physical Android target (API ${result.apiLevel}).\n`,
|
|
85
|
+
);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Explicit target-pool example
|
|
2
|
+
|
|
3
|
+
Replace the serial and AVD name with resources owned by this host, then copy
|
|
4
|
+
the configuration to the project root.
|
|
5
|
+
|
|
6
|
+
Validate and preview before allocating either target:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
adb-ready config validate
|
|
10
|
+
adb-ready run --pool smoke --dry-run --json -- npm run test:e2e
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run the bounded verifier on both members:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
adb-ready run --pool smoke -- npm run test:e2e
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The AVD must already exist. The built-in lease backend coordinates only
|
|
20
|
+
processes sharing this host and user state directory. See
|
|
21
|
+
[Target pools and fan-out](../../docs/target-pools.md) before adapting this for
|
|
22
|
+
multiple CI runners or a remote ADB server.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/config-v1.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"dev": {
|
|
5
|
+
"preset": "expo",
|
|
6
|
+
"reversePorts": [8081]
|
|
7
|
+
},
|
|
8
|
+
"targets": {
|
|
9
|
+
"pools": {
|
|
10
|
+
"smoke": {
|
|
11
|
+
"maxConcurrency": 2,
|
|
12
|
+
"failFast": false,
|
|
13
|
+
"leaseWaitMs": 30000,
|
|
14
|
+
"members": [
|
|
15
|
+
{
|
|
16
|
+
"id": "usb-phone",
|
|
17
|
+
"kind": "adb",
|
|
18
|
+
"serial": "REPLACE_WITH_EXACT_SERIAL"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "api36-emulator",
|
|
22
|
+
"kind": "avd",
|
|
23
|
+
"name": "REPLACE_WITH_EXISTING_AVD"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adb-ready",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Agent-ready Android CLI for reliable ADB sessions, app automation, and verified evidence.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"@yarnpkg/cli-dist": "4.18.0",
|
|
100
100
|
"npm": "12.0.2",
|
|
101
101
|
"publint": "0.3.24",
|
|
102
|
-
"runpalette": "0.
|
|
102
|
+
"runpalette": "0.5.0",
|
|
103
103
|
"tinyexec": "1.3.1",
|
|
104
104
|
"typescript": "7.0.2",
|
|
105
105
|
"yarn": "1.22.22"
|
|
@@ -107,6 +107,9 @@
|
|
|
107
107
|
"type": "string",
|
|
108
108
|
"minLength": 1
|
|
109
109
|
}
|
|
110
|
+
},
|
|
111
|
+
"pools": {
|
|
112
|
+
"$ref": "#/$defs/targetPools"
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
},
|
|
@@ -147,9 +150,92 @@
|
|
|
147
150
|
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$"
|
|
148
151
|
},
|
|
149
152
|
"additionalProperties": { "type": "string", "minLength": 1 }
|
|
153
|
+
},
|
|
154
|
+
"pools": {
|
|
155
|
+
"$ref": "#/$defs/targetPools"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"targetPools": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"propertyNames": {
|
|
162
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$"
|
|
163
|
+
},
|
|
164
|
+
"additionalProperties": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"additionalProperties": false,
|
|
167
|
+
"required": ["members", "maxConcurrency"],
|
|
168
|
+
"properties": {
|
|
169
|
+
"members": {
|
|
170
|
+
"type": "array",
|
|
171
|
+
"minItems": 1,
|
|
172
|
+
"maxItems": 64,
|
|
173
|
+
"items": { "$ref": "#/$defs/targetPoolMember" }
|
|
174
|
+
},
|
|
175
|
+
"maxConcurrency": { "type": "integer", "minimum": 1, "maximum": 32 },
|
|
176
|
+
"failFast": { "type": "boolean", "default": false },
|
|
177
|
+
"leaseWaitMs": {
|
|
178
|
+
"type": "integer",
|
|
179
|
+
"minimum": 0,
|
|
180
|
+
"maximum": 3600000,
|
|
181
|
+
"default": 30000
|
|
182
|
+
}
|
|
150
183
|
}
|
|
151
184
|
}
|
|
152
185
|
},
|
|
186
|
+
"targetPoolMember": {
|
|
187
|
+
"oneOf": [
|
|
188
|
+
{
|
|
189
|
+
"type": "object",
|
|
190
|
+
"additionalProperties": false,
|
|
191
|
+
"required": ["id", "kind", "serial"],
|
|
192
|
+
"properties": {
|
|
193
|
+
"id": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$" },
|
|
194
|
+
"kind": { "const": "adb" },
|
|
195
|
+
"serial": { "type": "string", "minLength": 1 },
|
|
196
|
+
"required": { "type": "boolean", "default": true }
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"type": "object",
|
|
201
|
+
"additionalProperties": false,
|
|
202
|
+
"required": ["id", "kind", "name"],
|
|
203
|
+
"properties": {
|
|
204
|
+
"id": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$" },
|
|
205
|
+
"kind": { "const": "avd" },
|
|
206
|
+
"name": { "type": "string", "minLength": 1 },
|
|
207
|
+
"required": { "type": "boolean", "default": true }
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"type": "object",
|
|
212
|
+
"additionalProperties": false,
|
|
213
|
+
"required": ["id", "kind", "host", "serial"],
|
|
214
|
+
"properties": {
|
|
215
|
+
"id": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$" },
|
|
216
|
+
"kind": { "const": "remote-adb" },
|
|
217
|
+
"host": { "type": "string", "minLength": 1 },
|
|
218
|
+
"port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 5037 },
|
|
219
|
+
"serial": { "type": "string", "minLength": 1 },
|
|
220
|
+
"required": { "type": "boolean", "default": true }
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"type": "object",
|
|
225
|
+
"additionalProperties": false,
|
|
226
|
+
"required": ["id", "kind", "model", "version"],
|
|
227
|
+
"properties": {
|
|
228
|
+
"id": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$" },
|
|
229
|
+
"kind": { "const": "firebase" },
|
|
230
|
+
"model": { "type": "string", "minLength": 1 },
|
|
231
|
+
"version": { "type": "string", "minLength": 1 },
|
|
232
|
+
"locale": { "type": "string", "minLength": 1, "default": "en" },
|
|
233
|
+
"orientation": { "enum": ["landscape", "portrait"], "default": "portrait" },
|
|
234
|
+
"required": { "type": "boolean", "default": true }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
},
|
|
153
239
|
"app": {
|
|
154
240
|
"type": "object",
|
|
155
241
|
"additionalProperties": false,
|
|
@@ -3,7 +3,7 @@ name: adb-ready
|
|
|
3
3
|
description: Prepare, automate, debug, and verify one Android target through the ADB Ready MCP server. Use for Android development sessions, app lifecycle work, UI automation, failure evidence, screenshots, and agent-driven device checks.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- Generated by adb-ready 0.
|
|
6
|
+
<!-- Generated by adb-ready 0.8.0; do not edit. -->
|
|
7
7
|
|
|
8
8
|
# ADB Ready
|
|
9
9
|
|
|
@@ -51,4 +51,4 @@ Use the typed `adb-ready` MCP tools. Do not replace them with generic shell or r
|
|
|
51
51
|
- `session`: 12 tools
|
|
52
52
|
- `ui`: 29 tools
|
|
53
53
|
|
|
54
|
-
Contract: ADB Ready 0.
|
|
54
|
+
Contract: ADB Ready 0.8.0, agent schema v1.
|