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/dist/cli.js CHANGED
@@ -3298,13 +3298,13 @@ async function activateAgentTaskFromEnvironment(env) {
3298
3298
  }
3299
3299
 
3300
3300
  // src/cli/main.ts
3301
- import { randomUUID as randomUUID14 } from "node:crypto";
3302
- import path24 from "node:path";
3301
+ import { randomUUID as randomUUID17 } from "node:crypto";
3302
+ import path26 from "node:path";
3303
3303
  import process15 from "node:process";
3304
3304
  // package.json
3305
3305
  var package_default = {
3306
3306
  name: "adb-ready",
3307
- version: "0.6.0",
3307
+ version: "0.8.0",
3308
3308
  description: "Agent-ready Android CLI for reliable ADB sessions, app automation, and verified evidence.",
3309
3309
  private: false,
3310
3310
  type: "module",
@@ -3403,7 +3403,7 @@ var package_default = {
3403
3403
  "@yarnpkg/cli-dist": "4.18.0",
3404
3404
  npm: "12.0.2",
3405
3405
  publint: "0.3.24",
3406
- runpalette: "0.3.0",
3406
+ runpalette: "0.5.0",
3407
3407
  tinyexec: "1.3.1",
3408
3408
  typescript: "7.0.2",
3409
3409
  yarn: "1.22.22"
@@ -37551,6 +37551,7 @@ async function runProcess(request) {
37551
37551
  let spawnError;
37552
37552
  let streamError;
37553
37553
  return await new Promise((resolve) => {
37554
+ const ownsProcessGroup = request.killProcessGroup === true;
37554
37555
  const child = spawn2(request.executable, args, {
37555
37556
  cwd: request.cwd,
37556
37557
  env: request.inheritEnv === false ? request.env : { ...process5.env, ...request.env },
@@ -37560,21 +37561,41 @@ async function runProcess(request) {
37560
37561
  "pipe",
37561
37562
  "pipe"
37562
37563
  ],
37563
- windowsHide: true
37564
+ windowsHide: true,
37565
+ detached: ownsProcessGroup && process5.platform !== "win32"
37564
37566
  });
37565
37567
  const killSignal = request.killSignal ?? "SIGTERM";
37566
37568
  let idleTimer;
37567
37569
  let escalationTimer;
37570
+ let closed = false;
37571
+ const signalOwnedProcess = (signal) => {
37572
+ if (ownsProcessGroup && process5.platform === "win32" && child.pid !== undefined) {
37573
+ killEscalated = true;
37574
+ spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
37575
+ shell: false,
37576
+ stdio: "ignore",
37577
+ windowsHide: true
37578
+ }).once("error", () => child.kill(signal));
37579
+ return;
37580
+ }
37581
+ if (ownsProcessGroup && child.pid !== undefined) {
37582
+ try {
37583
+ process5.kill(-child.pid, signal);
37584
+ return;
37585
+ } catch {}
37586
+ }
37587
+ child.kill(signal);
37588
+ };
37568
37589
  const terminate = () => {
37569
- if (child.exitCode !== null || child.signalCode !== null) {
37590
+ if (closed || !ownsProcessGroup && (child.exitCode !== null || child.signalCode !== null)) {
37570
37591
  return;
37571
37592
  }
37572
- child.kill(killSignal);
37593
+ signalOwnedProcess(killSignal);
37573
37594
  if (killSignal !== "SIGKILL" && escalationTimer === undefined) {
37574
37595
  escalationTimer = setTimeout(() => {
37575
- if (child.exitCode === null && child.signalCode === null) {
37596
+ if (!closed) {
37576
37597
  killEscalated = true;
37577
- child.kill("SIGKILL");
37598
+ signalOwnedProcess("SIGKILL");
37578
37599
  }
37579
37600
  }, killGraceMs);
37580
37601
  }
@@ -37649,6 +37670,7 @@ async function runProcess(request) {
37649
37670
  };
37650
37671
  });
37651
37672
  child.once("close", (exitCode, signal) => {
37673
+ closed = true;
37652
37674
  if (timeout !== undefined) {
37653
37675
  clearTimeout(timeout);
37654
37676
  }
@@ -38063,6 +38085,137 @@ function parseLogcatThreadtimeLine(raw) {
38063
38085
  return { timestamp, pid, tid, priority, tag, message, raw: line };
38064
38086
  }
38065
38087
 
38088
+ // src/adb/remote-server-probe.ts
38089
+ import { createConnection } from "node:net";
38090
+ var REQUEST = "000chost:version";
38091
+ var MAX_RESPONSE_BYTES = 4096;
38092
+ function normalizedSocketHost(host) {
38093
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
38094
+ }
38095
+ function protocolFrame(buffer) {
38096
+ if (buffer.length < 4)
38097
+ return { kind: "incomplete" };
38098
+ const status = buffer.subarray(0, 4).toString("ascii");
38099
+ if (status !== "OKAY" && status !== "FAIL") {
38100
+ return { kind: "invalid", message: "The server returned an unknown ADB status." };
38101
+ }
38102
+ if (buffer.length < 8)
38103
+ return { kind: "incomplete" };
38104
+ const lengthText = buffer.subarray(4, 8).toString("ascii");
38105
+ if (!/^[0-9a-fA-F]{4}$/u.test(lengthText)) {
38106
+ return { kind: "invalid", message: "The server returned an invalid ADB payload length." };
38107
+ }
38108
+ const length = Number.parseInt(lengthText, 16);
38109
+ if (length > MAX_RESPONSE_BYTES) {
38110
+ return { kind: "invalid", message: "The server returned an oversized ADB payload." };
38111
+ }
38112
+ if (buffer.length < 8 + length)
38113
+ return { kind: "incomplete" };
38114
+ const payload = buffer.subarray(8, 8 + length).toString("utf8");
38115
+ return status === "OKAY" ? { kind: "okay", payload } : { kind: "rejected", message: payload || "The server rejected host:version." };
38116
+ }
38117
+ function adbProtocolVersion(value) {
38118
+ const match = value?.match(/^1\.0\.(\d+)$/u);
38119
+ if (match?.[1] === undefined)
38120
+ return;
38121
+ const parsed = Number(match[1]);
38122
+ return Number.isSafeInteger(parsed) && parsed >= 0 && parsed <= 65535 ? parsed : undefined;
38123
+ }
38124
+ var probeRemoteAdbServer = async (options) => {
38125
+ if (options.host.trim() === "")
38126
+ throw new RangeError("remote ADB host cannot be empty");
38127
+ if (!Number.isSafeInteger(options.port) || options.port < 1 || options.port > 65535) {
38128
+ throw new RangeError("remote ADB port must be from 1 to 65535");
38129
+ }
38130
+ if (!Number.isSafeInteger(options.expectedProtocolVersion) || options.expectedProtocolVersion < 0 || options.expectedProtocolVersion > 65535) {
38131
+ throw new RangeError("expected ADB protocol version must be from 0 to 65535");
38132
+ }
38133
+ if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 60000) {
38134
+ throw new RangeError("remote ADB probe timeout must be from 1 to 60000");
38135
+ }
38136
+ if (options.signal?.aborted === true) {
38137
+ return { ok: false, kind: "cancelled", message: "Remote ADB preflight was cancelled." };
38138
+ }
38139
+ return await new Promise((resolve) => {
38140
+ const socket = createConnection({
38141
+ host: normalizedSocketHost(options.host),
38142
+ port: options.port
38143
+ });
38144
+ let buffer = Buffer.alloc(0);
38145
+ let settled = false;
38146
+ const finish = (result) => {
38147
+ if (settled)
38148
+ return;
38149
+ settled = true;
38150
+ clearTimeout(timer);
38151
+ options.signal?.removeEventListener("abort", abort);
38152
+ socket.destroy();
38153
+ resolve(result);
38154
+ };
38155
+ const abort = () => finish({ ok: false, kind: "cancelled", message: "Remote ADB preflight was cancelled." });
38156
+ const timer = setTimeout(() => finish({
38157
+ ok: false,
38158
+ kind: "timeout",
38159
+ message: "The configured ADB server did not answer the safety preflight in time."
38160
+ }), options.timeoutMs);
38161
+ options.signal?.addEventListener("abort", abort, { once: true });
38162
+ socket.once("connect", () => socket.write(REQUEST, "ascii"));
38163
+ socket.on("data", (chunk) => {
38164
+ buffer = Buffer.concat([buffer, chunk]);
38165
+ if (buffer.length > MAX_RESPONSE_BYTES + 8) {
38166
+ finish({
38167
+ ok: false,
38168
+ kind: "invalid-response",
38169
+ message: "The configured endpoint returned an oversized ADB response."
38170
+ });
38171
+ return;
38172
+ }
38173
+ const frame = protocolFrame(buffer);
38174
+ if (frame.kind === "incomplete")
38175
+ return;
38176
+ if (frame.kind === "invalid") {
38177
+ finish({ ok: false, kind: "invalid-response", message: frame.message });
38178
+ return;
38179
+ }
38180
+ if (frame.kind === "rejected") {
38181
+ finish({ ok: false, kind: "rejected", message: frame.message });
38182
+ return;
38183
+ }
38184
+ if (!/^[0-9a-fA-F]{4}$/u.test(frame.payload)) {
38185
+ finish({
38186
+ ok: false,
38187
+ kind: "invalid-response",
38188
+ message: "The configured endpoint returned an invalid ADB protocol version."
38189
+ });
38190
+ return;
38191
+ }
38192
+ const actualProtocolVersion = Number.parseInt(frame.payload, 16);
38193
+ if (actualProtocolVersion !== options.expectedProtocolVersion) {
38194
+ finish({
38195
+ ok: false,
38196
+ kind: "protocol-mismatch",
38197
+ message: "The remote ADB server protocol does not match the local ADB client.",
38198
+ expectedProtocolVersion: options.expectedProtocolVersion,
38199
+ actualProtocolVersion
38200
+ });
38201
+ return;
38202
+ }
38203
+ finish({ ok: true, protocolVersion: actualProtocolVersion });
38204
+ });
38205
+ socket.once("end", () => finish({
38206
+ ok: false,
38207
+ kind: "invalid-response",
38208
+ message: "The configured endpoint closed before completing the ADB preflight."
38209
+ }));
38210
+ socket.once("error", (caught) => finish({
38211
+ ok: false,
38212
+ kind: "unreachable",
38213
+ message: "The configured ADB server could not be reached.",
38214
+ ...caught.code === undefined ? {} : { networkCode: caught.code }
38215
+ }));
38216
+ });
38217
+ };
38218
+
38066
38219
  // src/adb/client.ts
38067
38220
  function targetArguments(target) {
38068
38221
  if (typeof target === "string") {
@@ -38095,13 +38248,21 @@ class AdbClient {
38095
38248
  #options;
38096
38249
  #runner;
38097
38250
  #idFactory;
38251
+ #remoteServerProbe;
38252
+ #expectedProtocolVersion;
38098
38253
  constructor(options) {
38099
38254
  this.#options = options;
38100
38255
  this.#runner = options.runner ?? runProcess;
38101
38256
  this.#idFactory = options.idFactory ?? randomUUID2;
38257
+ this.#remoteServerProbe = options.remoteServerProbe ?? (options.runner === undefined ? probeRemoteAdbServer : false);
38102
38258
  }
38103
38259
  async version(signal) {
38104
- return await this.#observe("version", "Checking ADB version", ["version"], parseAdbVersion, signal, { useServerArguments: false });
38260
+ const observation = await this.#observe("version", "Checking ADB version", ["version"], parseAdbVersion, signal, { useServerArguments: false });
38261
+ const protocolVersion = adbProtocolVersion(observation.value.protocolVersion);
38262
+ if (protocolVersion !== undefined) {
38263
+ this.#expectedProtocolVersion = Promise.resolve(protocolVersion);
38264
+ }
38265
+ return observation;
38105
38266
  }
38106
38267
  async hostFeatures(signal) {
38107
38268
  return await this.#observe("host-features", "Checking ADB host features", ["host-features"], parseFeatureList, signal);
@@ -38159,6 +38320,47 @@ class AdbClient {
38159
38320
  ...this.#options.port === undefined ? [] : ["-P", String(this.#options.port)]
38160
38321
  ];
38161
38322
  }
38323
+ async#localProtocolVersion(signal) {
38324
+ if (this.#expectedProtocolVersion !== undefined)
38325
+ return await this.#expectedProtocolVersion;
38326
+ this.#expectedProtocolVersion = (async () => {
38327
+ const result = await this.#runner({
38328
+ executable: this.#options.executable,
38329
+ args: ["version"],
38330
+ timeoutMs: Math.min(this.#options.timeoutMs ?? 3000, 3000),
38331
+ ...signal === undefined ? {} : { signal }
38332
+ });
38333
+ if (result.spawnError !== undefined || result.streamError !== undefined || result.exitCode !== 0 || result.timedOut || result.aborted) {
38334
+ return;
38335
+ }
38336
+ return adbProtocolVersion(parseAdbVersion(result.stdout).protocolVersion);
38337
+ })();
38338
+ return await this.#expectedProtocolVersion;
38339
+ }
38340
+ async#remoteSafetyPreflight(signal) {
38341
+ if (this.#options.host === undefined && this.#options.port === undefined || this.#remoteServerProbe === false) {
38342
+ return;
38343
+ }
38344
+ const expectedProtocolVersion = await this.#localProtocolVersion(signal);
38345
+ if (signal?.aborted === true) {
38346
+ return { ok: false, kind: "cancelled", message: "Remote ADB preflight was cancelled." };
38347
+ }
38348
+ if (expectedProtocolVersion === undefined) {
38349
+ return {
38350
+ ok: false,
38351
+ kind: "client-version-unavailable",
38352
+ message: "The local ADB protocol version could not be verified safely."
38353
+ };
38354
+ }
38355
+ const result = await this.#remoteServerProbe({
38356
+ host: this.#options.host ?? "localhost",
38357
+ port: this.#options.port ?? 5037,
38358
+ expectedProtocolVersion,
38359
+ timeoutMs: Math.min(this.#options.timeoutMs ?? 3000, 3000),
38360
+ ...signal === undefined ? {} : { signal }
38361
+ });
38362
+ return result.ok ? undefined : result;
38363
+ }
38162
38364
  async#observe(operation, message, args, parse10, signal, options = {}) {
38163
38365
  const operationId = this.#idFactory();
38164
38366
  const correlation = { ...this.#options.correlation, operationId };
@@ -38179,6 +38381,41 @@ class AdbClient {
38179
38381
  ...background ? { presentation: "background", retention: "transient" } : {}
38180
38382
  }
38181
38383
  });
38384
+ const clientFailure = options.useServerArguments === false ? undefined : await this.#remoteSafetyPreflight(signal);
38385
+ if (clientFailure !== undefined) {
38386
+ const now = new Date().toISOString();
38387
+ const result2 = {
38388
+ executable: this.#options.executable,
38389
+ args: finalArgs,
38390
+ startedAt: now,
38391
+ finishedAt: now,
38392
+ durationMs: 0,
38393
+ exitCode: clientFailure.kind === "cancelled" ? null : 1,
38394
+ signal: null,
38395
+ stdout: "",
38396
+ stderr: clientFailure.message,
38397
+ stdoutTruncated: false,
38398
+ stderrTruncated: false,
38399
+ timedOut: clientFailure.kind === "timeout",
38400
+ aborted: clientFailure.kind === "cancelled",
38401
+ stoppedAfterIdle: false,
38402
+ killEscalated: false
38403
+ };
38404
+ this.#options.bus.emit({
38405
+ type: "operation.failed",
38406
+ source: `adb.${operation}`,
38407
+ severity: clientFailure.kind === "cancelled" && background ? "debug" : "error",
38408
+ message: clientFailure.kind === "cancelled" && background ? `${message} cancelled` : `${message} failed`,
38409
+ correlation,
38410
+ data: {
38411
+ ...processMetadata(result2),
38412
+ preflight: "remote-server",
38413
+ failure: clientFailure.kind,
38414
+ ...background ? { presentation: "background" } : {}
38415
+ }
38416
+ });
38417
+ return { operationId, process: result2, value: parse10(""), clientFailure };
38418
+ }
38182
38419
  const request = {
38183
38420
  executable: this.#options.executable,
38184
38421
  args: finalArgs,
@@ -38554,6 +38791,9 @@ var ProblemCode = {
38554
38791
  AdbNotFound: "ADB_NOT_FOUND",
38555
38792
  AdbOptionalProbeFailed: "ADB_OPTIONAL_PROBE_FAILED",
38556
38793
  AdbServerUnavailable: "ADB_SERVER_UNAVAILABLE",
38794
+ AdbRemoteServerInvalidResponse: "ADB_REMOTE_SERVER_INVALID_RESPONSE",
38795
+ AdbRemoteServerProtocolMismatch: "ADB_REMOTE_SERVER_PROTOCOL_MISMATCH",
38796
+ AdbRemoteServerUnreachable: "ADB_REMOTE_SERVER_UNREACHABLE",
38557
38797
  AdbVersionMismatch: "ADB_VERSION_MISMATCH",
38558
38798
  AdbTimeout: "ADB_TIMEOUT",
38559
38799
  InteractiveSelectionUnavailable: "INTERACTIVE_SELECTION_UNAVAILABLE",
@@ -38839,7 +39079,7 @@ function problemsForServerStatus(status, clientVersion, correlation) {
38839
39079
  }
38840
39080
  return problems;
38841
39081
  }
38842
- function adbProcessProblem(operation, result, correlation) {
39082
+ function adbProcessProblem(operation, result, correlation, clientFailure) {
38843
39083
  if (result.spawnError?.code === "ENOENT") {
38844
39084
  return adbNotFoundProblem(correlation, result.executable);
38845
39085
  }
@@ -38856,6 +39096,88 @@ function adbProcessProblem(operation, result, correlation) {
38856
39096
  correlation
38857
39097
  };
38858
39098
  }
39099
+ if (clientFailure?.kind === "protocol-mismatch") {
39100
+ return {
39101
+ code: ProblemCode.AdbRemoteServerProtocolMismatch,
39102
+ category: "adb.remote-server",
39103
+ severity: "error",
39104
+ summary: "The remote ADB server uses a different protocol version.",
39105
+ detail: "ADB Ready stopped before launching the ADB client because upstream ADB can terminate a mismatched shared server automatically.",
39106
+ retryable: true,
39107
+ evidence: [
39108
+ {
39109
+ source: "adb.remote-preflight",
39110
+ field: "expectedProtocolVersion",
39111
+ value: clientFailure.expectedProtocolVersion ?? null
39112
+ },
39113
+ {
39114
+ source: "adb.remote-preflight",
39115
+ field: "actualProtocolVersion",
39116
+ value: clientFailure.actualProtocolVersion ?? null
39117
+ }
39118
+ ],
39119
+ actions: [
39120
+ {
39121
+ id: "align_remote_platform_tools",
39122
+ title: "Align Platform-Tools on the client and remote ADB host",
39123
+ kind: "user",
39124
+ risk: "shared-global",
39125
+ automatic: false
39126
+ }
39127
+ ],
39128
+ correlation
39129
+ };
39130
+ }
39131
+ if (clientFailure?.kind === "unreachable") {
39132
+ return {
39133
+ code: ProblemCode.AdbRemoteServerUnreachable,
39134
+ category: "network.route",
39135
+ severity: "error",
39136
+ summary: "The configured remote ADB server is unreachable.",
39137
+ detail: "Verify the explicit host and port, then check the WSL, container, VM, VPN, firewall, or tunnel route without exposing the ADB socket publicly.",
39138
+ retryable: true,
39139
+ evidence: [
39140
+ ...clientFailure.networkCode === undefined ? [] : [
39141
+ {
39142
+ source: "adb.remote-preflight",
39143
+ field: "networkCode",
39144
+ value: clientFailure.networkCode
39145
+ }
39146
+ ]
39147
+ ],
39148
+ actions: [
39149
+ {
39150
+ id: "review_remote_adb_route",
39151
+ title: "Check the protected route to the configured ADB server",
39152
+ kind: "user",
39153
+ risk: "none",
39154
+ automatic: false
39155
+ }
39156
+ ],
39157
+ correlation
39158
+ };
39159
+ }
39160
+ if (clientFailure !== undefined && (clientFailure.kind === "invalid-response" || clientFailure.kind === "rejected" || clientFailure.kind === "client-version-unavailable")) {
39161
+ return {
39162
+ code: ProblemCode.AdbRemoteServerInvalidResponse,
39163
+ category: "adb.remote-server",
39164
+ severity: "error",
39165
+ summary: "The configured endpoint could not be verified as a compatible ADB server.",
39166
+ detail: "ADB Ready refused to send a normal ADB command until the endpoint and local client protocol can be verified safely.",
39167
+ retryable: true,
39168
+ evidence: [{ source: "adb.remote-preflight", field: "failure", value: clientFailure.kind }],
39169
+ actions: [
39170
+ {
39171
+ id: "verify_remote_adb_endpoint",
39172
+ title: "Verify the configured ADB host, port, and Platform-Tools installation",
39173
+ kind: "user",
39174
+ risk: "none",
39175
+ automatic: false
39176
+ }
39177
+ ],
39178
+ correlation
39179
+ };
39180
+ }
38859
39181
  if (result.timedOut) {
38860
39182
  return {
38861
39183
  code: ProblemCode.AdbTimeout,
@@ -38905,8 +39227,8 @@ ${result.stdout}`;
38905
39227
  correlation
38906
39228
  };
38907
39229
  }
38908
- function adbOptionalProbeProblem(operation, result, correlation) {
38909
- const failure3 = adbProcessProblem(operation, result, correlation);
39230
+ function adbOptionalProbeProblem(operation, result, correlation, clientFailure) {
39231
+ const failure3 = adbProcessProblem(operation, result, correlation, clientFailure);
38910
39232
  return {
38911
39233
  ...failure3,
38912
39234
  code: ProblemCode.AdbOptionalProbeFailed,
@@ -39391,6 +39713,7 @@ var NATIVE_EXTENSIONS = new Set([
39391
39713
  ".log",
39392
39714
  ".mp4",
39393
39715
  ".png",
39716
+ ".proto",
39394
39717
  ".txt",
39395
39718
  ".webm",
39396
39719
  ".xml"
@@ -39536,7 +39859,7 @@ async function collectNativeVerifierArtifacts(directory, limits = {}) {
39536
39859
  const file = await stat4(absolute).catch(() => {
39537
39860
  return;
39538
39861
  });
39539
- if (file === undefined || file.size > maxFileBytes || total + file.size > maxTotalBytes || artifacts.length >= maxFiles) {
39862
+ if (file === undefined || limits.modifiedSinceMs !== undefined && file.mtimeMs < limits.modifiedSinceMs || file.size > maxFileBytes || total + file.size > maxTotalBytes || artifacts.length >= maxFiles) {
39540
39863
  omitted += 1;
39541
39864
  continue;
39542
39865
  }
@@ -39812,7 +40135,7 @@ async function staleLease(directory, owner, now, options) {
39812
40135
  });
39813
40136
  return metadata !== undefined && now.getTime() - metadata.mtimeMs >= (options.ttlMs ?? DEFAULT_TTL_MS);
39814
40137
  }
39815
- async function acquireTargetLease(request, options = {}) {
40138
+ async function tryAcquireTargetLease(request, options = {}) {
39816
40139
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
39817
40140
  const heartbeatMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
39818
40141
  if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000) {
@@ -39932,6 +40255,51 @@ async function acquireTargetLease(request, options = {}) {
39932
40255
  message: "ADB Ready could not safely recover the stale target lease."
39933
40256
  };
39934
40257
  }
40258
+ function wait2(milliseconds, signal) {
40259
+ return new Promise((resolve) => {
40260
+ if (signal?.aborted === true) {
40261
+ resolve();
40262
+ return;
40263
+ }
40264
+ const finish = () => {
40265
+ signal?.removeEventListener("abort", abort);
40266
+ resolve();
40267
+ };
40268
+ const timer = setTimeout(finish, milliseconds);
40269
+ const abort = () => {
40270
+ clearTimeout(timer);
40271
+ finish();
40272
+ };
40273
+ signal?.addEventListener("abort", abort, { once: true });
40274
+ });
40275
+ }
40276
+ async function acquireTargetLease(request, options = {}) {
40277
+ const waitTimeoutMs = options.waitTimeoutMs ?? 0;
40278
+ const pollIntervalMs = options.waitPollIntervalMs ?? 250;
40279
+ if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 3600000) {
40280
+ throw new RangeError("target lease waitTimeoutMs must be from 0 to 3600000");
40281
+ }
40282
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 10 || pollIntervalMs > 1e4) {
40283
+ throw new RangeError("target lease waitPollIntervalMs must be from 10 to 10000");
40284
+ }
40285
+ const clock = options.clock ?? (() => new Date);
40286
+ const deadline = clock().getTime() + waitTimeoutMs;
40287
+ while (true) {
40288
+ if (options.signal?.aborted === true) {
40289
+ return {
40290
+ ok: false,
40291
+ code: "TARGET_LEASE_CANCELLED",
40292
+ message: "Target ownership wait was cancelled."
40293
+ };
40294
+ }
40295
+ const acquired = await tryAcquireTargetLease(request, options);
40296
+ if (acquired.ok || acquired.code !== "TARGET_BUSY" || clock().getTime() >= deadline) {
40297
+ return acquired;
40298
+ }
40299
+ const remaining = Math.max(0, deadline - clock().getTime());
40300
+ await (options.sleep ?? wait2)(Math.min(pollIntervalMs, remaining), options.signal);
40301
+ }
40302
+ }
39935
40303
 
39936
40304
  // src/evidence/ui-hierarchy-capture.ts
39937
40305
  var DEFAULT_LOCK_POLL_INTERVAL_MS = 50;
@@ -40630,7 +40998,7 @@ async function sendExpoControl(options) {
40630
40998
 
40631
40999
  // src/dev/expo-launch.ts
40632
41000
  var DEFAULT_TIMEOUT_MS2 = 2000;
40633
- var MAX_RESPONSE_BYTES = 16 * 1024;
41001
+ var MAX_RESPONSE_BYTES2 = 16 * 1024;
40634
41002
  var MAX_URL_LENGTH = 8 * 1024;
40635
41003
  function aborted4(signal) {
40636
41004
  return signal?.aborted === true;
@@ -40692,7 +41060,7 @@ async function boundedBody(response) {
40692
41060
  if (chunk.done)
40693
41061
  break;
40694
41062
  bytes += chunk.value.byteLength;
40695
- if (bytes > MAX_RESPONSE_BYTES)
41063
+ if (bytes > MAX_RESPONSE_BYTES2)
40696
41064
  throw new RangeError("Expo response is too large");
40697
41065
  body += decoder.decode(chunk.value, { stream: true });
40698
41066
  }
@@ -42386,6 +42754,7 @@ function createClient(executable, context, config4, dependencies) {
42386
42754
  ...config4.adbPort === undefined ? {} : { port: config4.adbPort },
42387
42755
  ...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
42388
42756
  ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
42757
+ ...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
42389
42758
  ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
42390
42759
  });
42391
42760
  }
@@ -42393,7 +42762,7 @@ function operationProblem2(operation, observation, commandId) {
42393
42762
  return adbProcessProblem(operation, observation.process, {
42394
42763
  commandId,
42395
42764
  operationId: observation.operationId
42396
- });
42765
+ }, observation.clientFailure);
42397
42766
  }
42398
42767
  function isConnectableDiscoveryService(service) {
42399
42768
  return service.serviceType === "legacy" || service.serviceType === "connect" && service.knownDevice !== false;
@@ -42584,7 +42953,7 @@ async function runDoctor(config4 = {}, dependencies = {}, signal) {
42584
42953
  const hostFeatures = await client.hostFeatures(signal);
42585
42954
  if (!processSucceeded(hostFeatures.process)) {
42586
42955
  const problem2 = operationProblem2("host-features", hostFeatures, context.commandId);
42587
- if (problem2.code === ProblemCode.OperationInterrupted) {
42956
+ if (problem2.code === ProblemCode.OperationInterrupted || hostFeatures.clientFailure !== undefined) {
42588
42957
  problems.push(problem2);
42589
42958
  return finish(context, null, problems);
42590
42959
  }
@@ -42601,7 +42970,7 @@ async function runDoctor(config4 = {}, dependencies = {}, signal) {
42601
42970
  }));
42602
42971
  } else {
42603
42972
  const problem2 = operationProblem2("server-status", status, context.commandId);
42604
- if (problem2.code === ProblemCode.OperationInterrupted) {
42973
+ if (problem2.code === ProblemCode.OperationInterrupted || status.clientFailure !== undefined) {
42605
42974
  problems.push(problem2);
42606
42975
  return finish(context, null, problems);
42607
42976
  }
@@ -43239,6 +43608,7 @@ async function runLogs(options, config4 = {}, dependencies = {}, signal) {
43239
43608
  ...config4.adbPort === undefined ? {} : { port: config4.adbPort },
43240
43609
  ...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
43241
43610
  ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
43611
+ ...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
43242
43612
  ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
43243
43613
  });
43244
43614
  let resolvedPid = options.pid;
@@ -44020,6 +44390,7 @@ async function runDev(options, config4 = {}, dependencies = {}, signal) {
44020
44390
  ...config4.adbPort === undefined ? {} : { port: config4.adbPort },
44021
44391
  ...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
44022
44392
  ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
44393
+ ...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
44023
44394
  idFactory
44024
44395
  });
44025
44396
  const devices = await discoveryClient.devices(signal);
@@ -44107,6 +44478,7 @@ async function runDev(options, config4 = {}, dependencies = {}, signal) {
44107
44478
  ...config4.adbPort === undefined ? {} : { port: config4.adbPort },
44108
44479
  ...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
44109
44480
  ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
44481
+ ...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
44110
44482
  idFactory
44111
44483
  };
44112
44484
  const client = new AdbClient(clientOptions);
@@ -45480,7 +45852,7 @@ function operationProblem(name, observation, commandId) {
45480
45852
  return adbProcessProblem(name, observation.process, {
45481
45853
  commandId,
45482
45854
  operationId: observation.operationId
45483
- });
45855
+ }, observation.clientFailure);
45484
45856
  }
45485
45857
  async function readyTarget(current, config4, dependencies, problems, signal) {
45486
45858
  const inventory = await runDevices(config4, { ...dependencies, bus: current.bus }, signal);
@@ -45526,6 +45898,7 @@ async function readyTarget(current, config4, dependencies, problems, signal) {
45526
45898
  ...config4.adbPort === undefined ? {} : { port: config4.adbPort },
45527
45899
  ...config4.timeoutMs === undefined ? {} : { timeoutMs: config4.timeoutMs },
45528
45900
  ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
45901
+ ...dependencies.remoteServerProbe === undefined ? {} : { remoteServerProbe: dependencies.remoteServerProbe },
45529
45902
  ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
45530
45903
  })
45531
45904
  };
@@ -45647,11 +46020,11 @@ async function launchApp(ready, applicationId, explicitActivity, commandId, prob
45647
46020
  if (succeeded(active.process) && active.value?.applicationId === applicationId)
45648
46021
  break;
45649
46022
  if (attempt < 4) {
45650
- const wait2 = dependencies.sleep ?? (async (milliseconds) => {
46023
+ const wait3 = dependencies.sleep ?? (async (milliseconds) => {
45651
46024
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
45652
46025
  return true;
45653
46026
  });
45654
- if (!await wait2(150, signal ?? new AbortController().signal))
46027
+ if (!await wait3(150, signal ?? new AbortController().signal))
45655
46028
  break;
45656
46029
  }
45657
46030
  }
@@ -46395,7 +46768,27 @@ var ROOT_KEYS2 = new Set([
46395
46768
  var PROFILE_KEYS = new Set(["extends", "adb", "timeoutMs", "output", "targets", "app", "dev"]);
46396
46769
  var ADB_KEYS = new Set(["path", "host", "port"]);
46397
46770
  var OUTPUT_KEYS = new Set(["color", "unicode", "animation", "interactive"]);
46398
- var TARGET_KEYS = new Set(["aliases"]);
46771
+ var TARGET_KEYS = new Set(["aliases", "pools"]);
46772
+ var TARGET_POOL_KEYS = new Set(["members", "maxConcurrency", "failFast", "leaseWaitMs"]);
46773
+ var TARGET_POOL_MEMBER_KEYS = new Set([
46774
+ "id",
46775
+ "kind",
46776
+ "required",
46777
+ "serial",
46778
+ "name",
46779
+ "host",
46780
+ "port",
46781
+ "model",
46782
+ "version",
46783
+ "locale",
46784
+ "orientation"
46785
+ ]);
46786
+ var TARGET_POOL_MEMBER_KEYS_BY_KIND = {
46787
+ adb: new Set(["id", "kind", "required", "serial"]),
46788
+ avd: new Set(["id", "kind", "required", "name"]),
46789
+ "remote-adb": new Set(["id", "kind", "required", "host", "port", "serial"]),
46790
+ firebase: new Set(["id", "kind", "required", "model", "version", "locale", "orientation"])
46791
+ };
46399
46792
  var APP_KEYS = new Set(["android"]);
46400
46793
  var ANDROID_APP_KEYS = new Set(["package"]);
46401
46794
  var DEV_KEYS = new Set([
@@ -46635,6 +47028,127 @@ function validateDocument(document2, source, location) {
46635
47028
  values.targetAliases = aliases;
46636
47029
  }
46637
47030
  }
47031
+ if (document2.targets.pools !== undefined) {
47032
+ if (!isObject4(document2.targets.pools)) {
47033
+ errors6.push(error4(source, location, "targets.pools", "targets.pools must be an object."));
47034
+ } else {
47035
+ const pools = {};
47036
+ for (const [poolName, candidate] of Object.entries(document2.targets.pools)) {
47037
+ const poolPath = `targets.pools.${poolName}`;
47038
+ if (!PROFILE_NAME.test(poolName)) {
47039
+ errors6.push(error4(source, location, poolPath, "Target pool name must be 1-64 letters, numbers, dots, underscores, or hyphens."));
47040
+ continue;
47041
+ }
47042
+ if (!isObject4(candidate)) {
47043
+ errors6.push(error4(source, location, poolPath, "Target pool must be an object."));
47044
+ continue;
47045
+ }
47046
+ validateUnknownKeys(candidate, TARGET_POOL_KEYS, `${poolPath}.`, source, location, errors6);
47047
+ const members4 = candidate.members;
47048
+ const maxConcurrency = candidate.maxConcurrency;
47049
+ const failFast = candidate.failFast ?? false;
47050
+ const leaseWaitMs = candidate.leaseWaitMs ?? 30000;
47051
+ let valid = true;
47052
+ if (!Array.isArray(members4) || members4.length < 1 || members4.length > 64) {
47053
+ errors6.push(error4(source, location, `${poolPath}.members`, "Target pool members must be an array with 1-64 entries."));
47054
+ valid = false;
47055
+ }
47056
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 32) {
47057
+ errors6.push(error4(source, location, `${poolPath}.maxConcurrency`, "Target pool maxConcurrency must be an integer from 1 to 32."));
47058
+ valid = false;
47059
+ }
47060
+ if (typeof failFast !== "boolean") {
47061
+ errors6.push(error4(source, location, `${poolPath}.failFast`, "Target pool failFast must be a boolean."));
47062
+ valid = false;
47063
+ }
47064
+ if (!Number.isSafeInteger(leaseWaitMs) || leaseWaitMs < 0 || leaseWaitMs > 3600000) {
47065
+ errors6.push(error4(source, location, `${poolPath}.leaseWaitMs`, "Target pool leaseWaitMs must be an integer from 0 to 3600000."));
47066
+ valid = false;
47067
+ }
47068
+ const parsedMembers = [];
47069
+ const memberIds = new Set;
47070
+ const resourceIds = new Set;
47071
+ if (Array.isArray(members4)) {
47072
+ members4.forEach((member, index) => {
47073
+ const memberPath = `${poolPath}.members.${String(index)}`;
47074
+ if (!isObject4(member)) {
47075
+ errors6.push(error4(source, location, memberPath, "Target pool member must be an object."));
47076
+ valid = false;
47077
+ return;
47078
+ }
47079
+ validateUnknownKeys(member, TARGET_POOL_MEMBER_KEYS, `${memberPath}.`, source, location, errors6);
47080
+ const id = member.id;
47081
+ const kind = member.kind;
47082
+ const required4 = member.required ?? true;
47083
+ if (kind === "adb" || kind === "avd" || kind === "remote-adb" || kind === "firebase") {
47084
+ validateUnknownKeys(member, TARGET_POOL_MEMBER_KEYS_BY_KIND[kind], `${memberPath}.`, source, location, errors6);
47085
+ }
47086
+ if (typeof id !== "string" || !PROFILE_NAME.test(id) || memberIds.has(id)) {
47087
+ errors6.push(error4(source, location, `${memberPath}.id`, "Target pool member id must be unique and use 1-64 letters, numbers, dots, underscores, or hyphens."));
47088
+ valid = false;
47089
+ return;
47090
+ }
47091
+ memberIds.add(id);
47092
+ if (typeof required4 !== "boolean") {
47093
+ errors6.push(error4(source, location, `${memberPath}.required`, "Target pool member required must be a boolean."));
47094
+ valid = false;
47095
+ return;
47096
+ }
47097
+ const optionalRequired = required4 ? {} : { required: false };
47098
+ const nonEmpty = (value) => typeof value === "string" && value.trim() === value && value.length > 0;
47099
+ let resourceId;
47100
+ if (kind === "adb" && nonEmpty(member.serial)) {
47101
+ parsedMembers.push({ id, kind, serial: member.serial, ...optionalRequired });
47102
+ resourceId = `adb:${member.serial}`;
47103
+ } else if (kind === "avd" && nonEmpty(member.name)) {
47104
+ parsedMembers.push({ id, kind, name: member.name, ...optionalRequired });
47105
+ resourceId = `avd:${member.name}`;
47106
+ } else if (kind === "remote-adb" && nonEmpty(member.host) && nonEmpty(member.serial) && (member.port === undefined || Number.isSafeInteger(member.port) && member.port >= 1 && member.port <= 65535)) {
47107
+ parsedMembers.push({
47108
+ id,
47109
+ kind,
47110
+ host: member.host,
47111
+ port: member.port ?? 5037,
47112
+ serial: member.serial,
47113
+ ...optionalRequired
47114
+ });
47115
+ resourceId = `remote-adb:${member.host}:${String(member.port ?? 5037)}:${member.serial}`;
47116
+ } else if (kind === "firebase" && nonEmpty(member.model) && nonEmpty(member.version) && (member.locale === undefined || nonEmpty(member.locale)) && (member.orientation === undefined || member.orientation === "landscape" || member.orientation === "portrait")) {
47117
+ parsedMembers.push({
47118
+ id,
47119
+ kind,
47120
+ model: member.model,
47121
+ version: member.version,
47122
+ locale: member.locale ?? "en",
47123
+ orientation: member.orientation ?? "portrait",
47124
+ ...optionalRequired
47125
+ });
47126
+ resourceId = `firebase:${member.model}:${member.version}:${String(member.locale ?? "en")}:${String(member.orientation ?? "portrait")}`;
47127
+ } else {
47128
+ errors6.push(error4(source, location, memberPath, "Target pool member must be adb(serial), avd(name), remote-adb(host, optional port, serial), or firebase(model, version, optional locale/orientation)."));
47129
+ valid = false;
47130
+ }
47131
+ if (resourceId !== undefined) {
47132
+ if (resourceIds.has(resourceId)) {
47133
+ errors6.push(error4(source, location, memberPath, "Target pool members must identify unique resources or provider dimensions."));
47134
+ valid = false;
47135
+ }
47136
+ resourceIds.add(resourceId);
47137
+ }
47138
+ });
47139
+ }
47140
+ if (valid) {
47141
+ pools[poolName] = {
47142
+ members: parsedMembers,
47143
+ maxConcurrency,
47144
+ failFast,
47145
+ leaseWaitMs
47146
+ };
47147
+ }
47148
+ }
47149
+ values.targetPools = pools;
47150
+ }
47151
+ }
46638
47152
  }
46639
47153
  }
46640
47154
  if (document2.app !== undefined) {
@@ -53209,6 +53723,1143 @@ ${githubSummary(result)}
53209
53723
  }
53210
53724
  }
53211
53725
 
53726
+ // src/automation/firebase-test-lab.ts
53727
+ import { randomUUID as randomUUID13 } from "node:crypto";
53728
+ import { chmod, mkdir as mkdir8, rename as rename8, rm as rm9, stat as stat15, writeFile as writeFile8 } from "node:fs/promises";
53729
+ import path23 from "node:path";
53730
+ var AUTH_PATTERN = /active account|auth login|authentication|credentials|permission denied|unauthenticated|unauthorized/iu;
53731
+ var MATRIX_PATTERN = /Test \[([^\]\r\n]+)\] has been created in the Google Cloud\./u;
53732
+ var CONSOLE_URL_PATTERN = /https:\/\/console\.firebase\.google\.com\/[A-Za-z0-9%_./?=&-]+/u;
53733
+ var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
53734
+ var PROVIDER_FILE_LIMIT = 100;
53735
+ var PROVIDER_FILE_BYTES = 20 * 1024 * 1024;
53736
+ var PROVIDER_TOTAL_BYTES = 50 * 1024 * 1024;
53737
+ var PROVIDER_EXTENSIONS = new Set([
53738
+ ".ec",
53739
+ ".html",
53740
+ ".jpeg",
53741
+ ".jpg",
53742
+ ".json",
53743
+ ".log",
53744
+ ".mp4",
53745
+ ".png",
53746
+ ".proto",
53747
+ ".txt",
53748
+ ".webm",
53749
+ ".xml"
53750
+ ]);
53751
+ function problem2(code, category, summary2, detail, commandId, retryable = true) {
53752
+ return {
53753
+ code,
53754
+ category,
53755
+ severity: "error",
53756
+ summary: summary2,
53757
+ detail,
53758
+ retryable,
53759
+ evidence: [],
53760
+ actions: [],
53761
+ correlation: { commandId }
53762
+ };
53763
+ }
53764
+ function finish3(started, finished, commandId, data, problems, exitCode2) {
53765
+ return {
53766
+ exitCode: exitCode2,
53767
+ result: {
53768
+ schemaVersion: SCHEMA_VERSION,
53769
+ command: "test firebase",
53770
+ commandId,
53771
+ ok: exitCode2 === 0 /* Success */,
53772
+ startedAt: started.toISOString(),
53773
+ finishedAt: finished.toISOString(),
53774
+ durationMs: Math.max(0, finished.getTime() - started.getTime()),
53775
+ data,
53776
+ problems
53777
+ }
53778
+ };
53779
+ }
53780
+ function parseJsonArray(value) {
53781
+ try {
53782
+ const parsed = JSON.parse(value);
53783
+ return Array.isArray(parsed) ? parsed : undefined;
53784
+ } catch {
53785
+ return;
53786
+ }
53787
+ }
53788
+ function success(result) {
53789
+ return result.spawnError === undefined && result.streamError === undefined && result.exitCode === 0 && !result.timedOut && !result.aborted;
53790
+ }
53791
+ function capacity(value) {
53792
+ const normalized = value?.replace(/^DEVICE_CAPACITY_/u, "").toLowerCase();
53793
+ return normalized === "high" || normalized === "medium" || normalized === "low" || normalized === "none" ? normalized : "unknown";
53794
+ }
53795
+ function hasTag(tags, expected) {
53796
+ return tags.some((tag) => tag.toLowerCase().includes(expected));
53797
+ }
53798
+ function artifactArgument(cwd, value) {
53799
+ return value.startsWith("gs://") ? value : path23.resolve(cwd, value);
53800
+ }
53801
+ function parseFirebaseDevice(value) {
53802
+ const fields = new Map;
53803
+ for (const part of value.split(",")) {
53804
+ const separator = part.indexOf("=");
53805
+ if (separator < 1)
53806
+ return;
53807
+ const key = part.slice(0, separator).trim();
53808
+ const fieldValue = part.slice(separator + 1).trim();
53809
+ if (!new Set(["locale", "model", "orientation", "version"]).has(key) || fieldValue === "") {
53810
+ return;
53811
+ }
53812
+ if (fields.has(key))
53813
+ return;
53814
+ fields.set(key, fieldValue);
53815
+ }
53816
+ const model = fields.get("model");
53817
+ const version4 = fields.get("version");
53818
+ const locale = fields.get("locale") ?? "en";
53819
+ const orientation = fields.get("orientation") ?? "portrait";
53820
+ if (model === undefined || version4 === undefined || !SAFE_ID.test(model) || !SAFE_ID.test(version4) || !/^[A-Za-z]{2,3}(?:[_-][A-Za-z0-9]{2,8})?$/u.test(locale) || orientation !== "portrait" && orientation !== "landscape") {
53821
+ return;
53822
+ }
53823
+ return { model, version: version4, locale, orientation };
53824
+ }
53825
+ function parseFirebaseCatalog(modelsOutput, versionsOutput) {
53826
+ const models = parseJsonArray(modelsOutput);
53827
+ const versions4 = parseJsonArray(versionsOutput);
53828
+ if (models === undefined || versions4 === undefined)
53829
+ return;
53830
+ const versionTags = new Map(versions4.filter((version4) => typeof version4.id === "string").map((version4) => [version4.id, version4.tags ?? []]));
53831
+ const devices = [];
53832
+ for (const model of models) {
53833
+ if (typeof model.id !== "string" || !SAFE_ID.test(model.id))
53834
+ continue;
53835
+ for (const version4 of model.supportedVersionIds ?? []) {
53836
+ if (!SAFE_ID.test(version4))
53837
+ continue;
53838
+ const info = model.perVersionInfo?.find((candidate) => candidate.versionId === version4);
53839
+ devices.push({
53840
+ model: model.id,
53841
+ version: version4,
53842
+ locale: "en",
53843
+ orientation: "portrait",
53844
+ name: model.name ?? model.id,
53845
+ form: model.form ?? "UNKNOWN",
53846
+ ...model.formFactor === undefined ? {} : { formFactor: model.formFactor },
53847
+ capacity: capacity(info?.deviceCapacity),
53848
+ tags: [
53849
+ ...new Set([
53850
+ ...model.tags ?? [],
53851
+ ...(model.accessDeniedReasons?.length ?? 0) === 0 ? [] : ["access_denied"],
53852
+ ...versionTags.get(version4) ?? [],
53853
+ ...info?.tags ?? []
53854
+ ])
53855
+ ].map((tag) => tag.toLowerCase()).sort()
53856
+ });
53857
+ }
53858
+ }
53859
+ return devices.sort((left, right) => `${left.model}/${left.version}`.localeCompare(`${right.model}/${right.version}`));
53860
+ }
53861
+ function validateDevices(requested, catalog, options) {
53862
+ for (const device of requested) {
53863
+ const current = catalog.find((candidate) => candidate.model === device.model && candidate.version === device.version);
53864
+ if (current === undefined)
53865
+ return `${device.model}/${device.version} is unavailable or incompatible.`;
53866
+ if (hasTag(current.tags, "access_denied"))
53867
+ return `${device.model}/${device.version} is not accessible to this project.`;
53868
+ if (hasTag(current.tags, "deprecated") && options.allowDeprecated !== true)
53869
+ return `${device.model}/${device.version} is deprecated.`;
53870
+ if (hasTag(current.tags, "reduced_stability") && options.allowReducedStability !== true)
53871
+ return `${device.model}/${device.version} currently has reduced stability.`;
53872
+ if (current.capacity === "none")
53873
+ return `${device.model}/${device.version} currently has no capacity.`;
53874
+ if (current.capacity === "low" && options.allowLowCapacity !== true)
53875
+ return `${device.model}/${device.version} currently has low capacity.`;
53876
+ }
53877
+ return;
53878
+ }
53879
+ function gcloudBase(options) {
53880
+ return ["--quiet", ...options.project === undefined ? [] : [`--project=${options.project}`]];
53881
+ }
53882
+ function deviceArgument(device) {
53883
+ return `model=${device.model},version=${device.version},locale=${device.locale},orientation=${device.orientation}`;
53884
+ }
53885
+ function runArguments(options, resultsDir) {
53886
+ return [
53887
+ "firebase",
53888
+ "test",
53889
+ "android",
53890
+ "run",
53891
+ `--type=${options.action}`,
53892
+ `--app=${artifactArgument(options.cwd, options.app ?? "")}`,
53893
+ ...options.action === "instrumentation" ? [`--test=${artifactArgument(options.cwd, options.test ?? "")}`] : [],
53894
+ ...(options.devices ?? []).flatMap((device) => ["--device", deviceArgument(device)]),
53895
+ ...options.testTimeout === undefined ? [] : [`--timeout=${options.testTimeout}`],
53896
+ ...options.resultsBucket === undefined ? [] : [`--results-bucket=${options.resultsBucket}`],
53897
+ ...options.resultsBucket === undefined ? [] : [`--results-dir=${resultsDir}`],
53898
+ "--format=json",
53899
+ ...gcloudBase(options)
53900
+ ];
53901
+ }
53902
+ function extractProviderIdentity(output) {
53903
+ const matrixId = output.match(MATRIX_PATTERN)?.[1];
53904
+ const consoleUrl = output.match(CONSOLE_URL_PATTERN)?.[0];
53905
+ return {
53906
+ ...matrixId === undefined ? {} : { matrixId },
53907
+ ...consoleUrl === undefined ? {} : { consoleUrl }
53908
+ };
53909
+ }
53910
+ function parseOutcomes(output) {
53911
+ const values = parseJsonArray(output) ?? [];
53912
+ return values.flatMap((value) => {
53913
+ if (typeof value.outcome !== "string")
53914
+ return [];
53915
+ return [
53916
+ {
53917
+ outcome: value.outcome,
53918
+ axis: typeof value.axis_value === "string" ? value.axis_value : "unknown",
53919
+ details: typeof value.test_details === "string" ? value.test_details : ""
53920
+ }
53921
+ ];
53922
+ });
53923
+ }
53924
+ function classify(result, outcomes) {
53925
+ if (result.timedOut)
53926
+ return {
53927
+ outcome: "observation-timed-out",
53928
+ exitCode: 40 /* ChildProcess */,
53929
+ code: "FTL_OBSERVATION_TIMED_OUT"
53930
+ };
53931
+ if (result.aborted)
53932
+ return {
53933
+ outcome: "observation-stopped",
53934
+ exitCode: 130 /* Interrupted */,
53935
+ code: "FTL_OBSERVATION_STOPPED"
53936
+ };
53937
+ if (AUTH_PATTERN.test(`${result.stderr}
53938
+ ${result.stdout}`))
53939
+ return { outcome: "auth-failed", exitCode: 10 /* Environment */, code: "FTL_AUTH_FAILED" };
53940
+ if (result.exitCode === 0) {
53941
+ return {
53942
+ outcome: outcomes.some(({ outcome: outcome2 }) => outcome2.toLowerCase() === "flaky") ? "flaky" : "passed",
53943
+ exitCode: 0 /* Success */,
53944
+ code: ""
53945
+ };
53946
+ }
53947
+ if (result.exitCode === 10)
53948
+ return {
53949
+ outcome: "assertion-failed",
53950
+ exitCode: 40 /* ChildProcess */,
53951
+ code: "FTL_ASSERTION_FAILED"
53952
+ };
53953
+ if (result.exitCode === 15)
53954
+ return { outcome: "inconclusive", exitCode: 40 /* ChildProcess */, code: "FTL_INCONCLUSIVE" };
53955
+ if (result.exitCode === 18)
53956
+ return { outcome: "unsupported", exitCode: 2 /* InvalidInput */, code: "FTL_UNSUPPORTED" };
53957
+ if (result.exitCode === 19)
53958
+ return { outcome: "cancelled", exitCode: 130 /* Interrupted */, code: "FTL_CANCELLED" };
53959
+ return {
53960
+ outcome: "infrastructure-failed",
53961
+ exitCode: 40 /* ChildProcess */,
53962
+ code: "FTL_INFRASTRUCTURE_FAILED"
53963
+ };
53964
+ }
53965
+ function parseStorageObjects(output) {
53966
+ try {
53967
+ const parsed = JSON.parse(output);
53968
+ if (Array.isArray(parsed))
53969
+ return parsed;
53970
+ if (typeof parsed === "object" && parsed !== null)
53971
+ return [parsed];
53972
+ } catch {
53973
+ const records = [];
53974
+ for (const line of output.split(/\r?\n/u).filter((value) => value.trim() !== "")) {
53975
+ try {
53976
+ const parsed = JSON.parse(line);
53977
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
53978
+ return;
53979
+ records.push(parsed);
53980
+ } catch {
53981
+ return;
53982
+ }
53983
+ }
53984
+ return records.length === 0 ? undefined : records;
53985
+ }
53986
+ return;
53987
+ }
53988
+ function safeProviderName(index, url4) {
53989
+ const raw = url4.slice(url4.lastIndexOf("/") + 1) || "artifact";
53990
+ const safe = raw.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 120) || "artifact";
53991
+ return `${String(index + 1).padStart(3, "0")}-${safe}`;
53992
+ }
53993
+ async function collectProviderEvidence(options, runner, destination, resultsDir, remoteRunning) {
53994
+ if (options.resultsBucket === undefined) {
53995
+ return { providerFiles: 0, omittedFiles: 0, collection: "not-configured" };
53996
+ }
53997
+ const remotePrefix = `${options.resultsBucket}/${resultsDir.replace(/^\/+|\/+$/gu, "")}`;
53998
+ if (remoteRunning) {
53999
+ return {
54000
+ providerFiles: 0,
54001
+ omittedFiles: 0,
54002
+ collection: "remote-running",
54003
+ remotePrefix
54004
+ };
54005
+ }
54006
+ const listed = await runner({
54007
+ executable: options.gcloudPath ?? "gcloud",
54008
+ args: ["storage", "ls", "--recursive", "--json", remotePrefix, ...gcloudBase(options)],
54009
+ cwd: options.cwd,
54010
+ timeoutMs: 120000,
54011
+ maxBufferBytes: 4 * 1024 * 1024
54012
+ });
54013
+ if (!success(listed)) {
54014
+ return {
54015
+ providerFiles: 0,
54016
+ omittedFiles: 0,
54017
+ collection: "unavailable",
54018
+ remotePrefix,
54019
+ warning: redactText(listed.stderr || listed.stdout || "gcloud storage listing failed.").value
54020
+ };
54021
+ }
54022
+ const objects = parseStorageObjects(listed.stdout);
54023
+ if (objects === undefined) {
54024
+ return {
54025
+ providerFiles: 0,
54026
+ omittedFiles: 0,
54027
+ collection: "unavailable",
54028
+ remotePrefix,
54029
+ warning: "gcloud storage returned an unreadable artifact listing."
54030
+ };
54031
+ }
54032
+ const prefix = `${remotePrefix}/`;
54033
+ const eligible = objects.flatMap((object5) => {
54034
+ if (object5.type !== "cloud_object" || typeof object5.url !== "string" || !object5.url.startsWith(prefix) || object5.url.includes("?") || !PROVIDER_EXTENSIONS.has(path23.extname(object5.url).toLowerCase())) {
54035
+ return [];
54036
+ }
54037
+ const bytes = Number(object5.metadata?.size);
54038
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > PROVIDER_FILE_BYTES)
54039
+ return [];
54040
+ return [{ url: object5.url, bytes }];
54041
+ });
54042
+ const selected = [];
54043
+ let totalBytes = 0;
54044
+ for (const object5 of eligible) {
54045
+ if (selected.length >= PROVIDER_FILE_LIMIT || totalBytes + object5.bytes > PROVIDER_TOTAL_BYTES) {
54046
+ continue;
54047
+ }
54048
+ selected.push(object5);
54049
+ totalBytes += object5.bytes;
54050
+ }
54051
+ const providerDirectory = path23.join(destination, "provider");
54052
+ await mkdir8(providerDirectory, { recursive: true, mode: 448 });
54053
+ const manifest = [];
54054
+ for (const [index, object5] of selected.entries()) {
54055
+ const file = safeProviderName(index, object5.url);
54056
+ const copied = await runner({
54057
+ executable: options.gcloudPath ?? "gcloud",
54058
+ args: [
54059
+ "storage",
54060
+ "cp",
54061
+ object5.url,
54062
+ path23.join(providerDirectory, file),
54063
+ ...gcloudBase(options)
54064
+ ],
54065
+ cwd: options.cwd,
54066
+ timeoutMs: 120000,
54067
+ maxBufferBytes: 256 * 1024
54068
+ });
54069
+ if (!success(copied)) {
54070
+ return {
54071
+ providerFiles: manifest.length,
54072
+ omittedFiles: objects.length - manifest.length,
54073
+ collection: "unavailable",
54074
+ remotePrefix,
54075
+ warning: redactText(copied.stderr || copied.stdout || "gcloud storage copy failed.").value
54076
+ };
54077
+ }
54078
+ const localPath = path23.join(providerDirectory, file);
54079
+ const local = await stat15(localPath).catch(() => {
54080
+ return;
54081
+ });
54082
+ if (local === undefined || !local.isFile() || local.size !== object5.bytes) {
54083
+ return {
54084
+ providerFiles: manifest.length,
54085
+ omittedFiles: objects.length - manifest.length,
54086
+ collection: "unavailable",
54087
+ remotePrefix,
54088
+ warning: `Downloaded provider artifact failed size verification: ${file}`
54089
+ };
54090
+ }
54091
+ await chmod(localPath, 384);
54092
+ manifest.push({ source: object5.url, file, bytes: object5.bytes });
54093
+ }
54094
+ await writeFile8(path23.join(destination, "provider-files.json"), `${JSON.stringify(manifest, null, 2)}
54095
+ `, { mode: 384 });
54096
+ return {
54097
+ providerFiles: manifest.length,
54098
+ omittedFiles: Math.max(0, objects.length - manifest.length),
54099
+ collection: "complete",
54100
+ remotePrefix
54101
+ };
54102
+ }
54103
+ async function retainEvidence(options, commandId, processResult, data, runner, resultsDir) {
54104
+ const relative = path23.join(".adb-ready", "artifacts", `firebase-${commandId}`);
54105
+ const destination = path23.resolve(options.cwd, relative);
54106
+ const temporary = `${destination}.tmp`;
54107
+ await rm9(temporary, { recursive: true, force: true });
54108
+ await mkdir8(temporary, { recursive: true, mode: 448 });
54109
+ try {
54110
+ const provider = await collectProviderEvidence(options, runner, temporary, resultsDir, processResult.timedOut || processResult.aborted);
54111
+ const evidence = {
54112
+ path: relative.split(path23.sep).join("/"),
54113
+ providerFiles: provider.providerFiles,
54114
+ omittedFiles: provider.omittedFiles,
54115
+ collection: provider.collection,
54116
+ ...provider.remotePrefix === undefined ? {} : { remotePrefix: provider.remotePrefix }
54117
+ };
54118
+ data.evidence = evidence;
54119
+ await writeFile8(path23.join(temporary, "stdout.txt"), redactText(processResult.stdout).value, {
54120
+ mode: 384
54121
+ });
54122
+ await writeFile8(path23.join(temporary, "stderr.txt"), redactText(processResult.stderr).value, {
54123
+ mode: 384
54124
+ });
54125
+ await writeFile8(path23.join(temporary, "result.json"), `${JSON.stringify(data, null, 2)}
54126
+ `, {
54127
+ mode: 384
54128
+ });
54129
+ await rm9(destination, { recursive: true, force: true });
54130
+ await rename8(temporary, destination);
54131
+ return { evidence, ...provider.warning === undefined ? {} : { warning: provider.warning } };
54132
+ } catch (error5) {
54133
+ await rm9(temporary, { recursive: true, force: true }).catch(() => {
54134
+ return;
54135
+ });
54136
+ throw error5;
54137
+ }
54138
+ }
54139
+ async function readCatalog(options, runner) {
54140
+ const executable = options.gcloudPath ?? "gcloud";
54141
+ const base = gcloudBase(options);
54142
+ const models = await runner({
54143
+ executable,
54144
+ args: ["firebase", "test", "android", "models", "list", "--format=json", ...base],
54145
+ cwd: options.cwd,
54146
+ timeoutMs: 120000
54147
+ });
54148
+ if (!success(models))
54149
+ return { failure: models };
54150
+ const versions4 = await runner({
54151
+ executable,
54152
+ args: ["firebase", "test", "android", "versions", "list", "--format=json", ...base],
54153
+ cwd: options.cwd,
54154
+ timeoutMs: 120000
54155
+ });
54156
+ if (!success(versions4))
54157
+ return { failure: versions4 };
54158
+ const catalog = parseFirebaseCatalog(models.stdout, versions4.stdout);
54159
+ return catalog === undefined ? {} : { catalog };
54160
+ }
54161
+ async function cancelMatrix(options, runner, request) {
54162
+ const token = await runner({
54163
+ executable: options.gcloudPath ?? "gcloud",
54164
+ args: ["auth", "print-access-token", "--quiet"],
54165
+ cwd: options.cwd,
54166
+ timeoutMs: 30000,
54167
+ maxBufferBytes: 64 * 1024
54168
+ });
54169
+ if (!success(token) || token.stdout.trim() === "") {
54170
+ return { ok: false, detail: "Google Cloud authentication is unavailable.", auth: true };
54171
+ }
54172
+ const response = await request(`https://testing.googleapis.com/v1/projects/${encodeURIComponent(options.project ?? "")}/testMatrices/${encodeURIComponent(options.matrixId ?? "")}:cancel`, {
54173
+ method: "POST",
54174
+ headers: { authorization: `Bearer ${token.stdout.trim()}` },
54175
+ signal: AbortSignal.timeout(30000)
54176
+ }).catch(() => {
54177
+ return;
54178
+ });
54179
+ if (response === undefined)
54180
+ return {
54181
+ ok: false,
54182
+ detail: "Firebase Test Lab cancellation could not be reached.",
54183
+ auth: false
54184
+ };
54185
+ if (!response.ok) {
54186
+ const detail = redactText(await response.text()).value;
54187
+ return {
54188
+ ok: false,
54189
+ detail: detail || `Firebase Test Lab returned HTTP ${String(response.status)}.`,
54190
+ auth: response.status === 401 || response.status === 403
54191
+ };
54192
+ }
54193
+ return { ok: true };
54194
+ }
54195
+ async function runFirebaseTestLab(options, dependencies = {}, signal) {
54196
+ const clock = dependencies.clock ?? (() => new Date);
54197
+ const started = clock();
54198
+ const commandId = (dependencies.idFactory ?? randomUUID13)();
54199
+ const runner = dependencies.runner ?? runProcess;
54200
+ const executable = options.gcloudPath ?? "gcloud";
54201
+ if (options.action === "cancel") {
54202
+ if (options.project === undefined || options.matrixId === undefined) {
54203
+ return finish3(started, clock(), commandId, null, [
54204
+ problem2("FTL_CANCEL_INPUT_INVALID", "input.firebase", "Cancellation requires a project and matrix ID.", "Pass --project PROJECT and one exact matrix ID.", commandId, false)
54205
+ ], 2 /* InvalidInput */);
54206
+ }
54207
+ if (options.dryRun === true) {
54208
+ const plan2 = {
54209
+ schemaVersion: SCHEMA_VERSION,
54210
+ dryRun: true,
54211
+ steps: [
54212
+ {
54213
+ id: "firebase-cancel",
54214
+ title: `Cancel Firebase Test Lab matrix ${options.matrixId}`,
54215
+ risk: "destructive",
54216
+ executable: "Firebase Testing API",
54217
+ args: [options.project, options.matrixId]
54218
+ }
54219
+ ]
54220
+ };
54221
+ return finish3(started, clock(), commandId, {
54222
+ status: "planned",
54223
+ action: "cancel",
54224
+ project: options.project,
54225
+ matrixId: options.matrixId,
54226
+ plan: plan2
54227
+ }, [], 0 /* Success */);
54228
+ }
54229
+ const cancelled = await cancelMatrix(options, runner, dependencies.fetch ?? fetch);
54230
+ if (!cancelled.ok) {
54231
+ return finish3(started, clock(), commandId, null, [
54232
+ problem2(cancelled.auth ? "FTL_AUTH_FAILED" : "FTL_CANCEL_FAILED", cancelled.auth ? "environment.auth" : "provider.firebase", "Firebase Test Lab matrix cancellation failed.", cancelled.detail, commandId)
54233
+ ], cancelled.auth ? 10 /* Environment */ : 40 /* ChildProcess */);
54234
+ }
54235
+ return finish3(started, clock(), commandId, {
54236
+ status: "cancelled",
54237
+ action: "cancel",
54238
+ project: options.project,
54239
+ matrixId: options.matrixId,
54240
+ outcome: "cancelled"
54241
+ }, [], 0 /* Success */);
54242
+ }
54243
+ if (options.action === "devices") {
54244
+ const catalogResult2 = await readCatalog(options, runner);
54245
+ if (catalogResult2.failure !== undefined || catalogResult2.catalog === undefined) {
54246
+ const failure4 = catalogResult2.failure;
54247
+ const auth = AUTH_PATTERN.test(`${failure4?.stderr ?? ""}
54248
+ ${failure4?.stdout ?? ""}`);
54249
+ return finish3(started, clock(), commandId, null, [
54250
+ problem2(auth ? "FTL_AUTH_FAILED" : "FTL_CATALOG_FAILED", auth ? "environment.auth" : "provider.firebase.catalog", "Firebase Test Lab device catalog is unavailable.", auth ? "Authenticate gcloud for the selected project." : "gcloud did not return a valid machine-readable device catalog.", commandId)
54251
+ ], auth ? 10 /* Environment */ : 40 /* ChildProcess */);
54252
+ }
54253
+ return finish3(started, clock(), commandId, {
54254
+ status: "catalogued",
54255
+ action: "devices",
54256
+ ...options.project === undefined ? {} : { project: options.project },
54257
+ devices: catalogResult2.catalog
54258
+ }, [], 0 /* Success */);
54259
+ }
54260
+ if (options.app === undefined || options.action === "instrumentation" && options.test === undefined || options.devices === undefined || options.devices.length === 0) {
54261
+ return finish3(started, clock(), commandId, null, [
54262
+ problem2("FTL_RUN_INPUT_INVALID", "input.firebase", "Firebase Test Lab requires explicit artifacts and at least one device.", "Pass --app, --test for instrumentation, and one or more --test-device values.", commandId, false)
54263
+ ], 2 /* InvalidInput */);
54264
+ }
54265
+ for (const candidate of [options.app, ...options.test === undefined ? [] : [options.test]]) {
54266
+ if (candidate.startsWith("gs://"))
54267
+ continue;
54268
+ const exists = await stat15(path23.resolve(options.cwd, candidate)).then((entry) => entry.isFile()).catch(() => false);
54269
+ if (!exists)
54270
+ return finish3(started, clock(), commandId, null, [
54271
+ problem2("FTL_ARTIFACT_NOT_FOUND", "input.artifact", "A Firebase Test Lab artifact was not found.", candidate, commandId, false)
54272
+ ], 2 /* InvalidInput */);
54273
+ }
54274
+ const catalogResult = await readCatalog(options, runner);
54275
+ if (catalogResult.failure !== undefined || catalogResult.catalog === undefined) {
54276
+ const failure4 = catalogResult.failure;
54277
+ const auth = AUTH_PATTERN.test(`${failure4?.stderr ?? ""}
54278
+ ${failure4?.stdout ?? ""}`);
54279
+ return finish3(started, clock(), commandId, null, [
54280
+ problem2(auth ? "FTL_AUTH_FAILED" : "FTL_CATALOG_FAILED", auth ? "environment.auth" : "provider.firebase.catalog", "Firebase Test Lab preflight could not read the live device catalog.", auth ? "Authenticate gcloud for the selected project." : "Retry after checking gcloud and Firebase Test Lab availability.", commandId)
54281
+ ], auth ? 10 /* Environment */ : 40 /* ChildProcess */);
54282
+ }
54283
+ const rejected = validateDevices(options.devices, catalogResult.catalog, options);
54284
+ if (rejected !== undefined)
54285
+ return finish3(started, clock(), commandId, null, [
54286
+ problem2("FTL_DEVICE_POLICY_REJECTED", "input.firebase.device", "The requested Firebase Test Lab device was rejected by policy.", rejected, commandId, false)
54287
+ ], 2 /* InvalidInput */);
54288
+ const resultsDir = options.resultsDir ?? `adb-ready/${commandId}`;
54289
+ const args = runArguments(options, resultsDir);
54290
+ const plan = {
54291
+ schemaVersion: SCHEMA_VERSION,
54292
+ dryRun: options.dryRun === true,
54293
+ steps: [
54294
+ {
54295
+ id: "firebase-test",
54296
+ title: `Run one bounded Firebase Test Lab ${options.action} matrix`,
54297
+ risk: "open-world",
54298
+ executable,
54299
+ args
54300
+ }
54301
+ ]
54302
+ };
54303
+ if (options.dryRun === true)
54304
+ return finish3(started, clock(), commandId, {
54305
+ status: "planned",
54306
+ action: options.action,
54307
+ ...options.project === undefined ? {} : { project: options.project },
54308
+ selectedDevices: options.devices,
54309
+ command: { executable, args },
54310
+ plan
54311
+ }, [], 0 /* Success */);
54312
+ const executed = await runner({
54313
+ executable,
54314
+ args,
54315
+ cwd: options.cwd,
54316
+ ...signal === undefined ? {} : { signal },
54317
+ timeoutMs: options.observationTimeoutMs ?? 30 * 60000,
54318
+ killSignal: "SIGKILL",
54319
+ killProcessGroup: true,
54320
+ maxBufferBytes: 4 * 1024 * 1024
54321
+ });
54322
+ const results = parseOutcomes(executed.stdout);
54323
+ const classified = classify(executed, results);
54324
+ const identity = extractProviderIdentity(`${executed.stderr}
54325
+ ${executed.stdout}`);
54326
+ const data = {
54327
+ status: "completed",
54328
+ action: options.action,
54329
+ ...options.project === undefined ? {} : { project: options.project },
54330
+ selectedDevices: options.devices,
54331
+ command: { executable, args },
54332
+ outcome: classified.outcome,
54333
+ ...identity,
54334
+ results,
54335
+ ...(executed.timedOut || executed.aborted) && identity.matrixId !== undefined ? { remoteContinues: true } : {}
54336
+ };
54337
+ const retained = await retainEvidence(options, commandId, executed, data, runner, resultsDir);
54338
+ const problems = classified.exitCode === 0 /* Success */ ? [] : [
54339
+ problem2(classified.code, `provider.firebase.${classified.outcome}`, `Firebase Test Lab ${classified.outcome.replaceAll("-", " ")}.`, executed.timedOut || executed.aborted ? "Local observation stopped without cancelling the remote matrix. Use test firebase cancel explicitly if cancellation is intended." : redactText(executed.stderr || executed.stdout || "Firebase Test Lab returned no diagnostic output.").value, commandId, classified.outcome !== "assertion-failed")
54340
+ ];
54341
+ if (retained.warning !== undefined) {
54342
+ problems.push({
54343
+ ...problem2("FTL_EVIDENCE_COLLECTION_FAILED", "evidence.firebase", "Firebase Test Lab provider evidence could not be collected completely.", retained.warning, commandId),
54344
+ severity: "warning"
54345
+ });
54346
+ }
54347
+ return finish3(started, clock(), commandId, data, problems, classified.exitCode);
54348
+ }
54349
+
54350
+ // src/automation/gradle-managed-devices.ts
54351
+ import { randomUUID as randomUUID14 } from "node:crypto";
54352
+ import { mkdir as mkdir9, readdir as readdir5, rename as rename9, rm as rm10, stat as stat16, writeFile as writeFile9 } from "node:fs/promises";
54353
+ import path24 from "node:path";
54354
+ var MANAGED_RESULT_SUFFIX = path24.join("build", "outputs", "androidTest-results", "managedDevice");
54355
+ var MANAGED_REPORT_SUFFIX = path24.join("build", "reports", "androidTests", "managedDevice");
54356
+ var MANAGED_ADDITIONAL_OUTPUT_SUFFIX = path24.join("build", "outputs", "managed_device_android_test_additional_output");
54357
+ var SKIPPED_DIRECTORIES = new Set([".git", ".gradle", ".idea", ".adb-ready", "node_modules"]);
54358
+ function problem3(code, category, summary2, detail, commandId, evidence = []) {
54359
+ return {
54360
+ code,
54361
+ category,
54362
+ severity: "error",
54363
+ summary: summary2,
54364
+ detail,
54365
+ retryable: true,
54366
+ evidence,
54367
+ actions: [],
54368
+ correlation: { commandId }
54369
+ };
54370
+ }
54371
+ function finish4(started, finished, commandId, data, problems, exitCode2) {
54372
+ return {
54373
+ exitCode: exitCode2,
54374
+ result: {
54375
+ schemaVersion: SCHEMA_VERSION,
54376
+ command: "test gradle",
54377
+ commandId,
54378
+ ok: exitCode2 === 0 /* Success */,
54379
+ startedAt: started.toISOString(),
54380
+ finishedAt: finished.toISOString(),
54381
+ durationMs: Math.max(0, finished.getTime() - started.getTime()),
54382
+ data,
54383
+ problems
54384
+ }
54385
+ };
54386
+ }
54387
+ async function regularFile3(candidate) {
54388
+ return await stat16(candidate).then((entry) => entry.isFile()).catch(() => false);
54389
+ }
54390
+ async function resolveGradle2(cwd, explicit) {
54391
+ if (explicit !== undefined) {
54392
+ const executable = path24.isAbsolute(explicit) ? explicit : path24.resolve(cwd, explicit);
54393
+ return { executable, display: explicit };
54394
+ }
54395
+ const wrapper = process.platform === "win32" ? "gradlew.bat" : "gradlew";
54396
+ for (const relative of [wrapper, path24.join("android", wrapper)]) {
54397
+ const executable = path24.join(cwd, relative);
54398
+ if (await regularFile3(executable))
54399
+ return { executable, display: relative.split(path24.sep).join("/") };
54400
+ }
54401
+ return;
54402
+ }
54403
+ function validManagedTask(task) {
54404
+ const leaf = task.split(":").filter(Boolean).at(-1) ?? "";
54405
+ return /^[A-Za-z][A-Za-z0-9_]*AndroidTest$/u.test(leaf) && !/^connected[A-Z]/u.test(leaf) && !/^device[A-Z].*AndroidTest$/u.test(leaf);
54406
+ }
54407
+ function parseGradleManagedTasks(output) {
54408
+ const tasks = new Set;
54409
+ for (const line of output.split(/\r?\n/u)) {
54410
+ const match = line.match(/^\s*(:?[A-Za-z0-9_.:-]+AndroidTest)\s+-\s+(.+)$/u);
54411
+ if (match === null)
54412
+ continue;
54413
+ const task = match[1];
54414
+ const description = (match[2] ?? "").toLowerCase();
54415
+ const leaf = task?.split(":").filter(Boolean).at(-1) ?? "";
54416
+ if (task !== undefined && validManagedTask(task) && (/managed device|all devices (?:defined|in)/u.test(description) || /Group[A-Z].*AndroidTest$/u.test(leaf))) {
54417
+ tasks.add(task);
54418
+ }
54419
+ }
54420
+ return [...tasks].sort((left, right) => left.localeCompare(right));
54421
+ }
54422
+ function gradleArgs(options, task) {
54423
+ return [
54424
+ task,
54425
+ "--console=plain",
54426
+ "--no-daemon",
54427
+ ...options.shards === undefined ? [] : [`-Pandroid.experimental.androidTest.numManagedDeviceShards=${String(options.shards)}`],
54428
+ ...options.softwareRendering === true ? ["-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect"] : []
54429
+ ];
54430
+ }
54431
+ async function findManagedRoots(root) {
54432
+ const found = [];
54433
+ const visit = async (directory, depth) => {
54434
+ if (depth > 8 || found.length >= 100)
54435
+ return;
54436
+ const resultRoot = path24.join(directory, MANAGED_RESULT_SUFFIX);
54437
+ const reportRoot = path24.join(directory, MANAGED_REPORT_SUFFIX);
54438
+ const additionalOutputRoot = path24.join(directory, MANAGED_ADDITIONAL_OUTPUT_SUFFIX);
54439
+ for (const candidate of [resultRoot, reportRoot, additionalOutputRoot]) {
54440
+ const directoryExists = await stat16(candidate).then((entry) => entry.isDirectory()).catch(() => false);
54441
+ if (directoryExists)
54442
+ found.push(candidate);
54443
+ }
54444
+ const entries = await readdir5(directory, { withFileTypes: true }).catch(() => []);
54445
+ for (const entry of entries) {
54446
+ if (!entry.isDirectory() || entry.isSymbolicLink() || SKIPPED_DIRECTORIES.has(entry.name))
54447
+ continue;
54448
+ if (entry.name === "build")
54449
+ continue;
54450
+ await visit(path24.join(directory, entry.name), depth + 1);
54451
+ }
54452
+ };
54453
+ await visit(root, 0);
54454
+ return [...new Set(found)].sort((left, right) => left.localeCompare(right));
54455
+ }
54456
+ function junitSummary(documents) {
54457
+ const summary2 = { total: 0, failed: 0, errors: 0, skipped: 0 };
54458
+ for (const document2 of documents) {
54459
+ for (const match of document2.matchAll(/<testsuite\b([^>]*)>/giu)) {
54460
+ const attributes2 = match[1] ?? "";
54461
+ const read = (name) => {
54462
+ const value = attributes2.match(new RegExp(`\\b${name}=["'](\\d+)["']`, "iu"))?.[1];
54463
+ return value === undefined ? 0 : Number(value);
54464
+ };
54465
+ summary2.total += read("tests");
54466
+ summary2.failed += read("failures");
54467
+ summary2.errors += read("errors");
54468
+ summary2.skipped += read("skipped");
54469
+ }
54470
+ }
54471
+ return summary2;
54472
+ }
54473
+ async function retainEvidence2(options) {
54474
+ const safeId = options.commandId.replace(/[^A-Za-z0-9._-]+/gu, "-").slice(0, 80);
54475
+ const relative = path24.posix.join(".adb-ready", "artifacts", `gradle-managed-${safeId}`);
54476
+ const destination = path24.join(options.cwd, ...relative.split("/"));
54477
+ const temporary = `${destination}.tmp-${randomUUID14()}`;
54478
+ const collect = async (freshOnly) => {
54479
+ const files = [];
54480
+ let omitted = 0;
54481
+ let retainedBytes = 0;
54482
+ for (const [rootIndex, root] of options.roots.entries()) {
54483
+ const native = await collectNativeVerifierArtifacts(root, {
54484
+ maxFiles: Math.max(0, 100 - files.length),
54485
+ maxFileBytes: 20 * 1024 * 1024,
54486
+ maxTotalBytes: Math.max(0, 50 * 1024 * 1024 - retainedBytes),
54487
+ ...freshOnly ? { modifiedSinceMs: Date.parse(options.process.startedAt) - 2000 } : {}
54488
+ });
54489
+ omitted += native.omitted;
54490
+ for (const artifact of native.artifacts) {
54491
+ files.push({
54492
+ name: path24.posix.join(`root-${String(rootIndex + 1)}`, artifact.relativePath),
54493
+ content: artifact.content
54494
+ });
54495
+ retainedBytes += artifact.content.byteLength;
54496
+ }
54497
+ }
54498
+ return { files, omitted };
54499
+ };
54500
+ let collected = await collect(true);
54501
+ let provenance = collected.files.length === 0 ? "none" : "fresh";
54502
+ if (collected.files.length === 0 && options.outcome === "passed") {
54503
+ collected = await collect(false);
54504
+ if (collected.files.length > 0)
54505
+ provenance = "gradle-cache";
54506
+ }
54507
+ const nativeFiles = collected.files;
54508
+ const omittedFiles = collected.omitted;
54509
+ const reports = nativeFiles.map(({ name }) => path24.posix.join(relative, "native", name));
54510
+ const junitDocuments = nativeFiles.filter(({ name }) => name.toLowerCase().endsWith(".xml")).map(({ content }) => new TextDecoder().decode(content));
54511
+ const tests = junitSummary(junitDocuments);
54512
+ const outcome2 = provenance === "fresh" && tests.failed + tests.errors > 0 ? "assertion-failed" : options.outcome;
54513
+ await mkdir9(path24.dirname(destination), { recursive: true, mode: 448 });
54514
+ await mkdir9(path24.join(temporary, "native"), { recursive: true, mode: 448 });
54515
+ try {
54516
+ for (const file of nativeFiles) {
54517
+ const target = path24.join(temporary, "native", ...file.name.split("/"));
54518
+ await mkdir9(path24.dirname(target), { recursive: true, mode: 448 });
54519
+ const text = /\.(?:html|json|log|txt|xml)$/iu.test(file.name);
54520
+ await writeFile9(target, text ? redactText(new TextDecoder().decode(file.content)).value : file.content, { mode: 384 });
54521
+ }
54522
+ await writeFile9(path24.join(temporary, "stdout.txt"), redactText(options.process.stdout).value, {
54523
+ mode: 384
54524
+ });
54525
+ await writeFile9(path24.join(temporary, "stderr.txt"), redactText(options.process.stderr).value, {
54526
+ mode: 384
54527
+ });
54528
+ await writeFile9(path24.join(temporary, "result.json"), `${JSON.stringify({ schemaVersion: 1, task: options.task, outcome: outcome2, provenance, tests, reports }, null, 2)}
54529
+ `, { mode: 384 });
54530
+ await rename9(temporary, destination);
54531
+ } catch (error5) {
54532
+ await rm10(temporary, { recursive: true, force: true }).catch(() => {
54533
+ return;
54534
+ });
54535
+ throw error5;
54536
+ }
54537
+ return {
54538
+ path: relative,
54539
+ nativeFiles: nativeFiles.length,
54540
+ omittedFiles,
54541
+ reports,
54542
+ tests,
54543
+ outcome: outcome2,
54544
+ provenance
54545
+ };
54546
+ }
54547
+ function successfulProcess(result) {
54548
+ return result.spawnError === undefined && result.streamError === undefined && result.exitCode === 0 && !result.timedOut && !result.aborted;
54549
+ }
54550
+ function identifiesManagedDeviceTask(output, task) {
54551
+ const leaf = task.split(":").filter(Boolean).at(-1) ?? "";
54552
+ return /ManagedDevice|managed[ -]device/iu.test(output) || /Group[A-Z].*AndroidTest$/u.test(leaf) && /all devices defined/iu.test(output);
54553
+ }
54554
+ async function runGradleManagedTest(options, dependencies = {}, signal) {
54555
+ const clock = dependencies.clock ?? (() => new Date);
54556
+ const commandId = (dependencies.idFactory ?? randomUUID14)();
54557
+ const started = clock();
54558
+ const runner = dependencies.runner ?? runProcess;
54559
+ const gradle = await resolveGradle2(options.cwd, options.gradlePath);
54560
+ if (gradle === undefined) {
54561
+ return finish4(started, clock(), commandId, null, [
54562
+ problem3("GRADLE_WRAPPER_NOT_FOUND", "environment.gradle", "No Gradle Wrapper was found for this project.", "Run from the Android project root, keep the Wrapper checked in, or pass --gradle PATH.", commandId)
54563
+ ], 10 /* Environment */);
54564
+ }
54565
+ if (options.task === undefined) {
54566
+ const discovered = await runner({
54567
+ executable: gradle.executable,
54568
+ args: ["tasks", "--all", "--console=plain", "--no-daemon"],
54569
+ cwd: options.cwd,
54570
+ ...signal === undefined ? {} : { signal },
54571
+ timeoutMs: options.timeoutMs ?? 120000,
54572
+ killProcessGroup: true
54573
+ });
54574
+ if (!successfulProcess(discovered)) {
54575
+ const interrupted = discovered.aborted;
54576
+ return finish4(started, clock(), commandId, null, [
54577
+ problem3(interrupted ? "GRADLE_DISCOVERY_CANCELLED" : "GRADLE_DISCOVERY_FAILED", "automation.gradle.discovery", interrupted ? "Gradle managed-device discovery was cancelled." : "Gradle managed-device tasks could not be discovered.", discovered.timedOut ? "Gradle task discovery exceeded its timeout." : redactText(discovered.stderr || discovered.stdout || "Gradle did not start.").value, commandId)
54578
+ ], interrupted ? 130 /* Interrupted */ : 40 /* ChildProcess */);
54579
+ }
54580
+ return finish4(started, clock(), commandId, {
54581
+ status: "discovered",
54582
+ wrapper: gradle.display,
54583
+ tasks: parseGradleManagedTasks(discovered.stdout)
54584
+ }, [], 0 /* Success */);
54585
+ }
54586
+ if (!validManagedTask(options.task)) {
54587
+ return finish4(started, clock(), commandId, null, [
54588
+ problem3("GRADLE_MANAGED_TASK_INVALID", "input.gradle.task", "The requested Gradle task is not a managed-device test task.", "Use a declared task ending in AndroidTest; connected* tasks remain target-bound and belong under adb-ready run.", commandId, [{ source: "cli", field: "task", value: options.task }])
54589
+ ], 2 /* InvalidInput */);
54590
+ }
54591
+ const args = gradleArgs(options, options.task);
54592
+ const command = { executable: gradle.display, args };
54593
+ const plan = {
54594
+ schemaVersion: SCHEMA_VERSION,
54595
+ dryRun: options.dryRun === true,
54596
+ steps: [
54597
+ {
54598
+ id: "gradle-managed-test",
54599
+ title: `Let Gradle provision, test, and tear down ${options.task}`,
54600
+ risk: "local-additive",
54601
+ executable: gradle.display,
54602
+ args
54603
+ },
54604
+ {
54605
+ id: "retain-gradle-evidence",
54606
+ title: "Retain bounded native reports and normalized result",
54607
+ risk: "local-additive"
54608
+ }
54609
+ ]
54610
+ };
54611
+ if (options.dryRun) {
54612
+ return finish4(started, clock(), commandId, { status: "planned", wrapper: gradle.display, task: options.task, command, plan }, [], 0 /* Success */);
54613
+ }
54614
+ const preflight = await runner({
54615
+ executable: gradle.executable,
54616
+ args: ["help", "--task", options.task, "--console=plain", "--no-daemon"],
54617
+ cwd: options.cwd,
54618
+ ...signal === undefined ? {} : { signal },
54619
+ timeoutMs: Math.min(options.timeoutMs ?? 15 * 60000, 120000),
54620
+ killProcessGroup: true
54621
+ });
54622
+ if (!successfulProcess(preflight)) {
54623
+ const cancelled = preflight.aborted;
54624
+ return finish4(started, clock(), commandId, null, [
54625
+ problem3(cancelled ? "GRADLE_TASK_PREFLIGHT_CANCELLED" : "GRADLE_MANAGED_TASK_NOT_FOUND", "automation.gradle.task", cancelled ? "Gradle task validation was cancelled." : "The requested Gradle managed-device task is not declared by this build.", cancelled ? "No Gradle test task was started." : "Run adb-ready test gradle to discover declared managed-device tasks.", commandId, [{ source: "gradle", field: "task", value: options.task }])
54626
+ ], cancelled ? 130 /* Interrupted */ : 2 /* InvalidInput */);
54627
+ }
54628
+ if (!identifiesManagedDeviceTask(`${preflight.stdout}
54629
+ ${preflight.stderr}`, options.task)) {
54630
+ return finish4(started, clock(), commandId, null, [
54631
+ problem3("GRADLE_TASK_NOT_MANAGED_DEVICE", "input.gradle.task", "Gradle resolved the task, but did not identify it as a managed-device task.", "Run adb-ready test gradle and select a task declared by Android Gradle Plugin managedDevices.", commandId, [{ source: "gradle", field: "task", value: options.task }])
54632
+ ], 2 /* InvalidInput */);
54633
+ }
54634
+ const executed = await runner({
54635
+ executable: gradle.executable,
54636
+ args,
54637
+ cwd: options.cwd,
54638
+ ...signal === undefined ? {} : { signal },
54639
+ timeoutMs: options.timeoutMs ?? 15 * 60000,
54640
+ killProcessGroup: true
54641
+ });
54642
+ const roots = await findManagedRoots(options.cwd);
54643
+ let provisionalOutcome = successfulProcess(executed) ? "passed" : executed.aborted ? "cancelled" : executed.timedOut ? "timed-out" : "infrastructure-failed";
54644
+ const retained = await retainEvidence2({
54645
+ cwd: options.cwd,
54646
+ commandId,
54647
+ process: executed,
54648
+ task: options.task,
54649
+ outcome: provisionalOutcome,
54650
+ roots
54651
+ });
54652
+ provisionalOutcome = retained.outcome;
54653
+ const exitCode2 = provisionalOutcome === "passed" ? 0 /* Success */ : provisionalOutcome === "cancelled" ? 130 /* Interrupted */ : 40 /* ChildProcess */;
54654
+ const problems = provisionalOutcome === "passed" ? [] : [
54655
+ problem3(provisionalOutcome === "assertion-failed" ? "GRADLE_TEST_ASSERTION_FAILED" : provisionalOutcome === "timed-out" ? "GRADLE_MANAGED_TEST_TIMED_OUT" : provisionalOutcome === "cancelled" ? "GRADLE_MANAGED_TEST_CANCELLED" : "GRADLE_MANAGED_INFRASTRUCTURE_FAILED", provisionalOutcome === "assertion-failed" ? "automation.test.assertion" : "automation.gradle.infrastructure", provisionalOutcome === "assertion-failed" ? "One or more Gradle managed-device assertions failed." : provisionalOutcome === "timed-out" ? "The Gradle managed-device task exceeded its timeout." : provisionalOutcome === "cancelled" ? "The Gradle managed-device task was cancelled." : "Gradle could not complete the managed-device infrastructure lifecycle.", `Review the retained native reports and Gradle output in ${retained.path}.`, commandId)
54656
+ ];
54657
+ return finish4(started, clock(), commandId, {
54658
+ status: "completed",
54659
+ wrapper: gradle.display,
54660
+ task: options.task,
54661
+ command,
54662
+ outcome: provisionalOutcome,
54663
+ tests: retained.tests,
54664
+ reports: retained.reports,
54665
+ evidence: {
54666
+ path: retained.path,
54667
+ nativeFiles: retained.nativeFiles,
54668
+ omittedFiles: retained.omittedFiles,
54669
+ provenance: retained.provenance
54670
+ }
54671
+ }, problems, exitCode2);
54672
+ }
54673
+
54674
+ // src/automation/target-pool.ts
54675
+ import { randomUUID as randomUUID15 } from "node:crypto";
54676
+ function skipped(member, status) {
54677
+ return {
54678
+ id: member.id,
54679
+ kind: member.kind,
54680
+ required: member.required !== false,
54681
+ status,
54682
+ exitCode: 130 /* Interrupted */
54683
+ };
54684
+ }
54685
+ async function runTargetPool(options, runMember) {
54686
+ const started = (options.clock ?? (() => new Date))();
54687
+ const commandId = options.idFactory?.() ?? randomUUID15();
54688
+ const maxConcurrency = options.maxConcurrency ?? options.pool.maxConcurrency;
54689
+ const failFast = options.failFast ?? options.pool.failFast;
54690
+ const controller = new AbortController;
54691
+ const cancel = () => controller.abort(options.signal?.reason);
54692
+ options.signal?.addEventListener("abort", cancel, { once: true });
54693
+ if (options.signal?.aborted === true)
54694
+ cancel();
54695
+ const members4 = options.pool.members;
54696
+ const results = new Array(members4.length);
54697
+ let nextIndex = 0;
54698
+ let stopScheduling = controller.signal.aborted;
54699
+ const worker = async () => {
54700
+ while (!stopScheduling) {
54701
+ const index = nextIndex;
54702
+ nextIndex += 1;
54703
+ const member = members4[index];
54704
+ if (member === undefined)
54705
+ return;
54706
+ if (controller.signal.aborted) {
54707
+ results[index] = skipped(member, "cancelled");
54708
+ options.onMemberFinish?.(results[index]);
54709
+ return;
54710
+ }
54711
+ try {
54712
+ options.onMemberStart?.(member);
54713
+ const executed = await runMember(member, controller.signal);
54714
+ const status = executed.exitCode === 0 /* Success */ ? "passed" : controller.signal.aborted ? "cancelled" : "failed";
54715
+ results[index] = {
54716
+ id: member.id,
54717
+ kind: member.kind,
54718
+ required: member.required !== false,
54719
+ status,
54720
+ exitCode: executed.exitCode,
54721
+ result: executed.result
54722
+ };
54723
+ options.onMemberFinish?.(results[index]);
54724
+ if (failFast && member.required !== false && status !== "passed") {
54725
+ stopScheduling = true;
54726
+ if (options.cancelRunningOnFailFast !== false) {
54727
+ controller.abort(new Error(`Required pool member ${member.id} failed.`));
54728
+ }
54729
+ }
54730
+ } catch (caught) {
54731
+ const interrupted = controller.signal.aborted;
54732
+ results[index] = {
54733
+ id: member.id,
54734
+ kind: member.kind,
54735
+ required: member.required !== false,
54736
+ status: interrupted ? "cancelled" : "failed",
54737
+ exitCode: interrupted ? 130 /* Interrupted */ : 70 /* Internal */
54738
+ };
54739
+ options.onMemberFinish?.(results[index]);
54740
+ if (failFast && member.required !== false) {
54741
+ stopScheduling = true;
54742
+ if (options.cancelRunningOnFailFast !== false)
54743
+ controller.abort(caught);
54744
+ }
54745
+ }
54746
+ }
54747
+ };
54748
+ await Promise.all(Array.from({ length: Math.min(maxConcurrency, members4.length) }, async () => await worker()));
54749
+ options.signal?.removeEventListener("abort", cancel);
54750
+ for (let index = 0;index < members4.length; index += 1) {
54751
+ if (results[index] === undefined) {
54752
+ const member = members4[index];
54753
+ if (member !== undefined) {
54754
+ const result = skipped(member, controller.signal.aborted ? "cancelled" : "skipped");
54755
+ results[index] = result;
54756
+ options.onMemberFinish?.(result);
54757
+ }
54758
+ }
54759
+ }
54760
+ const completed = results.filter((result) => result !== undefined);
54761
+ const summary2 = {
54762
+ passed: completed.filter(({ status }) => status === "passed").length,
54763
+ failed: completed.filter(({ status }) => status === "failed").length,
54764
+ cancelled: completed.filter(({ status }) => status === "cancelled").length,
54765
+ skipped: completed.filter(({ status }) => status === "skipped").length
54766
+ };
54767
+ const requiredFailure = completed.find(({ required: required4, status }) => required4 && status !== "passed");
54768
+ const exitCode2 = requiredFailure === undefined ? 0 /* Success */ : requiredFailure.status === "cancelled" || requiredFailure.status === "skipped" ? 130 /* Interrupted */ : requiredFailure.exitCode;
54769
+ const finished = (options.clock ?? (() => new Date))();
54770
+ const problems = requiredFailure === undefined ? [] : [
54771
+ {
54772
+ code: "TARGET_POOL_REQUIRED_MEMBER_FAILED",
54773
+ category: "automation.pool",
54774
+ severity: "error",
54775
+ summary: `Required target pool member ${requiredFailure.id} did not pass.`,
54776
+ detail: `The aggregate preserves every member result and cannot succeed while a required member is ${requiredFailure.status}.`,
54777
+ retryable: true,
54778
+ evidence: [
54779
+ { source: "target.pool", field: "member", value: requiredFailure.id },
54780
+ { source: "target.pool", field: "status", value: requiredFailure.status }
54781
+ ],
54782
+ actions: [],
54783
+ correlation: { commandId }
54784
+ }
54785
+ ];
54786
+ return {
54787
+ exitCode: exitCode2,
54788
+ result: {
54789
+ schemaVersion: SCHEMA_VERSION,
54790
+ command: options.command ?? "run pool",
54791
+ commandId,
54792
+ ok: exitCode2 === 0 /* Success */,
54793
+ startedAt: started.toISOString(),
54794
+ finishedAt: finished.toISOString(),
54795
+ durationMs: Math.max(0, finished.getTime() - started.getTime()),
54796
+ data: {
54797
+ pool: options.name,
54798
+ maxConcurrency,
54799
+ failFast,
54800
+ coordination: options.coordination ?? "host-local-filesystem",
54801
+ members: completed,
54802
+ summary: summary2
54803
+ },
54804
+ problems
54805
+ }
54806
+ };
54807
+ }
54808
+ var VALUE_OPTIONS = new Set([
54809
+ "--pool",
54810
+ "--max-concurrency",
54811
+ "--lease-wait",
54812
+ "--device",
54813
+ "--serial",
54814
+ "-s",
54815
+ "--transport-id",
54816
+ "--avd",
54817
+ "--adb-host",
54818
+ "--adb-port",
54819
+ "--format"
54820
+ ]);
54821
+ var FLAG_OPTIONS = new Set([
54822
+ "--fail-fast",
54823
+ "--no-fail-fast",
54824
+ "--json",
54825
+ "--select",
54826
+ "--last",
54827
+ "--non-interactive",
54828
+ "--color",
54829
+ "--no-color",
54830
+ "--animation",
54831
+ "--no-animation"
54832
+ ]);
54833
+ function targetPoolMemberArguments(argv, member) {
54834
+ const separator = argv.indexOf("--");
54835
+ const options = separator === -1 ? argv : argv.slice(0, separator);
54836
+ const passthrough = separator === -1 ? [] : argv.slice(separator);
54837
+ const retained = [];
54838
+ for (let index = 0;index < options.length; index += 1) {
54839
+ const argument = options[index];
54840
+ if (argument === undefined)
54841
+ continue;
54842
+ const option = argument.split("=", 1)[0] ?? argument;
54843
+ if (VALUE_OPTIONS.has(option)) {
54844
+ if (!argument.includes("="))
54845
+ index += 1;
54846
+ continue;
54847
+ }
54848
+ if (FLAG_OPTIONS.has(option))
54849
+ continue;
54850
+ retained.push(argument);
54851
+ }
54852
+ retained.push("--format", "json", "--non-interactive", "--no-color", "--no-animation");
54853
+ if (member.kind === "adb")
54854
+ retained.push("--device", member.serial);
54855
+ else if (member.kind === "avd")
54856
+ retained.push("--avd", member.name);
54857
+ else {
54858
+ retained.push("--adb-host", member.host, "--adb-port", String(member.port), "--device", member.serial);
54859
+ }
54860
+ return [...retained, ...passthrough];
54861
+ }
54862
+
53212
54863
  // src/automation/target-profile.ts
53213
54864
  var PROBE_TIMEOUT_MS3 = 1e4;
53214
54865
  function validValue(value) {
@@ -53342,10 +54993,10 @@ async function inspectAndroidTargetProfile(options, runner = runProcess) {
53342
54993
  }
53343
54994
 
53344
54995
  // src/state/target-state.ts
53345
- import { randomUUID as randomUUID13 } from "node:crypto";
53346
- import { mkdir as mkdir8, readFile as readFile14, rename as rename8, rm as rm9, writeFile as writeFile8 } from "node:fs/promises";
54996
+ import { randomUUID as randomUUID16 } from "node:crypto";
54997
+ import { mkdir as mkdir10, readFile as readFile14, rename as rename10, rm as rm11, writeFile as writeFile10 } from "node:fs/promises";
53347
54998
  import { homedir as homedir8 } from "node:os";
53348
- import path23 from "node:path";
54999
+ import path25 from "node:path";
53349
55000
  import process14 from "node:process";
53350
55001
  var EMPTY_STATE = { version: 1, targets: {} };
53351
55002
  var MAX_REMEMBERED_SERVERS = 20;
@@ -53370,7 +55021,7 @@ function parseState(value) {
53370
55021
  return { version: 1, targets };
53371
55022
  }
53372
55023
  function defaultTargetStatePath(platform, env, homeDirectory) {
53373
- const platformPath = platform === "win32" ? path23.win32 : path23.posix;
55024
+ const platformPath = platform === "win32" ? path25.win32 : path25.posix;
53374
55025
  if (env.XDG_STATE_HOME !== undefined && env.XDG_STATE_HOME.trim() !== "") {
53375
55026
  return platformPath.join(env.XDG_STATE_HOME, "adb-ready", "state.json");
53376
55027
  }
@@ -53449,20 +55100,20 @@ async function writeRememberedTarget(target, options = {}) {
53449
55100
  }
53450
55101
  }).sort(([, left], [, right]) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, MAX_REMEMBERED_SERVERS);
53451
55102
  const document2 = { version: 1, targets: Object.fromEntries(entries) };
53452
- const directory = path23.dirname(statePath);
53453
- const temporary = path23.join(directory, `.state-${String(process14.pid)}-${randomUUID13()}.tmp`);
55103
+ const directory = path25.dirname(statePath);
55104
+ const temporary = path25.join(directory, `.state-${String(process14.pid)}-${randomUUID16()}.tmp`);
53454
55105
  try {
53455
- await mkdir8(directory, { recursive: true, mode: 448 });
53456
- await writeFile8(temporary, `${JSON.stringify(document2, null, 2)}
55106
+ await mkdir10(directory, { recursive: true, mode: 448 });
55107
+ await writeFile10(temporary, `${JSON.stringify(document2, null, 2)}
53457
55108
  `, {
53458
55109
  encoding: "utf8",
53459
55110
  flag: "wx",
53460
55111
  mode: 384
53461
55112
  });
53462
- await rename8(temporary, statePath);
55113
+ await rename10(temporary, statePath);
53463
55114
  return { ok: true, path: statePath };
53464
55115
  } catch {
53465
- await rm9(temporary, { force: true }).catch(() => {
55116
+ await rm11(temporary, { force: true }).catch(() => {
53466
55117
  return;
53467
55118
  });
53468
55119
  return {
@@ -54358,6 +56009,16 @@ async function showHomeScreen(options) {
54358
56009
  label: "Preview project plan",
54359
56010
  description: "Resolve the development workflow without allocating a target."
54360
56011
  },
56012
+ {
56013
+ value: "test-gradle",
56014
+ label: "Gradle managed-device tests",
56015
+ description: "Discover build-owned virtual-device and group test tasks."
56016
+ },
56017
+ {
56018
+ value: "test-firebase",
56019
+ label: "Firebase Test Lab",
56020
+ description: "Browse cloud devices before running a remote test matrix."
56021
+ },
54361
56022
  {
54362
56023
  value: "run-help",
54363
56024
  label: "Run a bounded verification",
@@ -54924,6 +56585,18 @@ function isDevData(value) {
54924
56585
  function isAutomationRunData(value) {
54925
56586
  return isRecord2(value) && typeof value.outcome === "string" && isRecord2(value.evidence) && (value.session === null || isDevData(value.session));
54926
56587
  }
56588
+ function isGradleManagedTestData(value) {
56589
+ return isRecord2(value) && (value.status === "completed" || value.status === "discovered" || value.status === "planned") && typeof value.wrapper === "string";
56590
+ }
56591
+ function isFirebaseTestLabData(value) {
56592
+ return isRecord2(value) && (value.status === "cancelled" || value.status === "catalogued" || value.status === "completed" || value.status === "planned") && (value.action === "cancel" || value.action === "devices" || value.action === "instrumentation" || value.action === "robo");
56593
+ }
56594
+ function isTargetPoolData(value) {
56595
+ if (!isRecord2(value) || typeof value.pool !== "string" || !Array.isArray(value.members)) {
56596
+ return false;
56597
+ }
56598
+ return typeof value.maxConcurrency === "number" && typeof value.failFast === "boolean" && isRecord2(value.summary) && value.members.every((member) => isRecord2(member) && typeof member.id === "string" && typeof member.kind === "string" && typeof member.required === "boolean" && typeof member.status === "string" && typeof member.exitCode === "number");
56599
+ }
54927
56600
  function isVersionData(value) {
54928
56601
  return isRecord2(value) && typeof value.version === "string";
54929
56602
  }
@@ -54995,19 +56668,19 @@ function wirelessServiceLabel(service) {
54995
56668
  const pairingState = service.serviceType !== "connect" || service.knownDevice === undefined ? "" : service.knownDevice ? " · paired" : " · pair first";
54996
56669
  return `${clean(identity)} · ${clean(service.endpoint.serial)} · ${clean(service.serviceType)}${pairingState}`;
54997
56670
  }
54998
- function problemLines(problem2, capabilities, verbose) {
56671
+ function problemLines(problem4, capabilities, verbose) {
54999
56672
  const glyphs = symbols(capabilities);
55000
- const marker = problem2.severity === "error" && problem2.code !== ProblemCode.OperationInterrupted ? style.failure(glyphs.failure, capabilities) : style.warning(glyphs.warning, capabilities);
56673
+ const marker = problem4.severity === "error" && problem4.code !== ProblemCode.OperationInterrupted ? style.failure(glyphs.failure, capabilities) : style.warning(glyphs.warning, capabilities);
55001
56674
  const lines = [
55002
- `${marker} ${style.strong(clean(problem2.summary), capabilities)}`,
55003
- ` ${clean(problem2.detail)}`
56675
+ `${marker} ${style.strong(clean(problem4.summary), capabilities)}`,
56676
+ ` ${clean(problem4.detail)}`
55004
56677
  ];
55005
- const action = problem2.actions[0];
56678
+ const action = problem4.actions[0];
55006
56679
  if (action !== undefined) {
55007
56680
  lines.push(` ${style.accent("Next:", capabilities)} ${clean(action.title)}`);
55008
56681
  }
55009
56682
  if (verbose) {
55010
- for (const evidence of problem2.evidence) {
56683
+ for (const evidence of problem4.evidence) {
55011
56684
  const field = evidence.field === undefined ? evidence.source : `${evidence.source}.${evidence.field}`;
55012
56685
  lines.push(` ${style.dim(`${clean(field)}: ${clean(JSON.stringify(evidence.value))}`, capabilities)}`);
55013
56686
  }
@@ -55069,6 +56742,60 @@ function renderHuman(result, options) {
55069
56742
  if (result.command === "run" && isAutomationRunData(result.data)) {
55070
56743
  lines.push(...automationHumanLines(result.data, capabilities));
55071
56744
  }
56745
+ if (result.command === "test gradle" && isGradleManagedTestData(result.data)) {
56746
+ const data = result.data;
56747
+ if (data.status === "discovered") {
56748
+ const tasks = data.tasks ?? [];
56749
+ lines.push(`${style.success(glyphs.success, capabilities)} Gradle ${clean(data.wrapper)}`, "", style.strong(`Managed-device tasks (${String(tasks.length)})`, capabilities), ...tasks.length === 0 ? [`${style.dim(glyphs.end, capabilities)} None declared`] : tasks.map((task, index) => {
56750
+ const branch = index === tasks.length - 1 ? glyphs.end : glyphs.branch;
56751
+ return `${style.dim(branch, capabilities)} ${clean(task)}`;
56752
+ }));
56753
+ } else {
56754
+ const marker = data.outcome === undefined || data.outcome === "passed" ? style.success(glyphs.success, capabilities) : style.failure(glyphs.failure, capabilities);
56755
+ lines.push(`${style.success(glyphs.success, capabilities)} Gradle ${clean(data.wrapper)}`, `${marker} Task ${clean(data.task ?? "unresolved")}${data.outcome === undefined ? " · planned" : ` · ${clean(data.outcome)}`}`);
56756
+ if (data.tests !== undefined) {
56757
+ lines.push(`${data.tests.failed + data.tests.errors === 0 ? style.success(glyphs.success, capabilities) : style.failure(glyphs.failure, capabilities)} Tests ${String(data.tests.total)} total · ${String(data.tests.failed)} failed · ${String(data.tests.errors)} errors · ${String(data.tests.skipped)} skipped`);
56758
+ }
56759
+ if (data.evidence !== undefined) {
56760
+ lines.push(`${style.accent(glyphs.active, capabilities)} Evidence ${clean(data.evidence.path)} · ${String(data.evidence.nativeFiles)} native files · ${clean(data.evidence.provenance)}`);
56761
+ }
56762
+ }
56763
+ }
56764
+ if (result.command === "test firebase" && isFirebaseTestLabData(result.data)) {
56765
+ const data = result.data;
56766
+ const marker = data.outcome === "flaky" ? style.warning(glyphs.warning, capabilities) : data.outcome === undefined || data.outcome === "passed" || data.status === "catalogued" ? style.success(glyphs.success, capabilities) : style.failure(glyphs.failure, capabilities);
56767
+ lines.push(`${marker} Firebase ${clean(data.action)} · ${clean(data.status)}${data.outcome === undefined ? "" : ` · ${clean(data.outcome)}`}`);
56768
+ if (data.status === "catalogued") {
56769
+ const devices = data.devices ?? [];
56770
+ lines.push(`${style.accent(glyphs.active, capabilities)} Catalog ${String(devices.length)} compatible model/version choices`, ...devices.slice(0, 20).map((device, index) => {
56771
+ const branch = index === Math.min(devices.length, 20) - 1 ? glyphs.end : glyphs.branch;
56772
+ return `${style.dim(branch, capabilities)} ${clean(device.model)}/${clean(device.version)} · ${clean(device.form.toLowerCase())} · ${clean(device.capacity)} capacity${device.tags.length === 0 ? "" : ` · ${device.tags.map(clean).join(", ")}`}`;
56773
+ }), ...devices.length > 20 ? [
56774
+ style.dim(` ${String(devices.length - 20)} more · use --json for the full catalog`, capabilities)
56775
+ ] : []);
56776
+ }
56777
+ if (data.matrixId !== undefined) {
56778
+ lines.push(`${style.accent(glyphs.active, capabilities)} Matrix ${clean(data.matrixId)}`);
56779
+ }
56780
+ if (data.consoleUrl !== undefined) {
56781
+ lines.push(`${style.accent(glyphs.active, capabilities)} Results ${clean(data.consoleUrl)}`);
56782
+ }
56783
+ if (data.remoteContinues === true) {
56784
+ lines.push(`${style.warning(glyphs.warning, capabilities)} Remote still running · explicit cancel required`);
56785
+ }
56786
+ if (data.evidence !== undefined) {
56787
+ lines.push(`${style.accent(glyphs.active, capabilities)} Evidence ${clean(data.evidence.path)} · ${String(data.evidence.providerFiles)} provider files · ${clean(data.evidence.collection)}`);
56788
+ }
56789
+ }
56790
+ if ((result.command === "run pool" || result.command === "test firebase pool") && isTargetPoolData(result.data)) {
56791
+ const data = result.data;
56792
+ lines.push(`${style.accent(glyphs.active, capabilities)} Pool ${clean(data.pool)} · concurrency ${String(data.maxConcurrency)} · ${data.failFast ? "fail fast" : "run all"}`, `${style.success(glyphs.success, capabilities)} Summary ${String(data.summary.passed)} passed · ${String(data.summary.failed)} failed · ${String(data.summary.cancelled)} cancelled · ${String(data.summary.skipped)} skipped`, `${style.dim(glyphs.branch, capabilities)} Coordination ${clean(data.coordination)}`, "", style.strong(`Members (${String(data.members.length)})`, capabilities));
56793
+ data.members.forEach((member, index) => {
56794
+ const branch = index === data.members.length - 1 ? glyphs.end : glyphs.branch;
56795
+ const marker = member.status === "passed" ? style.success(glyphs.success, capabilities) : member.status === "failed" ? style.failure(glyphs.failure, capabilities) : member.status === "cancelled" ? style.warning(glyphs.warning, capabilities) : style.dim(glyphs.skipped, capabilities);
56796
+ lines.push(`${style.dim(branch, capabilities)} ${marker} ${clean(member.id)} · ${clean(member.kind)} · ${clean(member.status)}${member.required ? "" : " · optional"}`);
56797
+ });
56798
+ }
55072
56799
  if (result.command === "logs" && isLogsData(result.data)) {
55073
56800
  const data = result.data;
55074
56801
  lines.push(`${style.success(glyphs.success, capabilities)} Target ${clean(data.selected.target.name)} · ${clean(data.selected.transport.serial)}`, `${style.success(glyphs.success, capabilities)} Filter ${data.filters.map(clean).join(" ")}${data.packageName === undefined ? "" : ` · ${clean(data.packageName)}`}${data.uid === undefined ? "" : ` · UID ${String(data.uid)}`}${data.pid === undefined ? "" : ` · PID ${String(data.pid)}`}`, `${style.success(glyphs.success, capabilities)} Buffers ${data.buffers.length === 0 ? "device default" : data.buffers.map(clean).join(", ")}`, `${style.success(glyphs.success, capabilities)} Records ${String(data.records.length)} retained${data.dropped === 0 ? "" : ` · ${String(data.dropped)} dropped`}`);
@@ -55224,9 +56951,9 @@ function renderHuman(result, options) {
55224
56951
  if (data.problems.length === 0) {
55225
56952
  lines.push(`${style.dim(glyphs.end, capabilities)} No recorded problems`);
55226
56953
  } else {
55227
- data.problems.forEach((problem2, index) => {
56954
+ data.problems.forEach((problem4, index) => {
55228
56955
  const branch = index === data.problems.length - 1 ? glyphs.end : glyphs.branch;
55229
- lines.push(`${style.dim(branch, capabilities)} ${style.failure(clean(problem2.code), capabilities)} · ${clean(problem2.summary)}`, ` ${clean(problem2.detail)}`);
56956
+ lines.push(`${style.dim(branch, capabilities)} ${style.failure(clean(problem4.code), capabilities)} · ${clean(problem4.summary)}`, ` ${clean(problem4.detail)}`);
55230
56957
  });
55231
56958
  }
55232
56959
  }
@@ -55313,8 +57040,8 @@ function renderHuman(result, options) {
55313
57040
  }
55314
57041
  if (result.problems.length > 0) {
55315
57042
  lines.push("", style.strong("Diagnostics", capabilities));
55316
- for (const problem2 of result.problems) {
55317
- lines.push(...problemLines(problem2, capabilities, options.verbose ?? false));
57043
+ for (const problem4 of result.problems) {
57044
+ lines.push(...problemLines(problem4, capabilities, options.verbose ?? false));
55318
57045
  }
55319
57046
  }
55320
57047
  const interrupted = result.problems.some(({ code }) => code === ProblemCode.OperationInterrupted) && !result.problems.some(({ code, severity }) => severity === "error" && code !== ProblemCode.OperationInterrupted);
@@ -55343,6 +57070,26 @@ function renderPlain(result, sink) {
55343
57070
  if (isVersionData(result.data))
55344
57071
  sink.write(`version=${clean(result.data.version)}
55345
57072
  `);
57073
+ if (isTargetPoolData(result.data)) {
57074
+ sink.write(`pool=${clean(result.data.pool)}
57075
+ `);
57076
+ sink.write(`max_concurrency=${String(result.data.maxConcurrency)}
57077
+ `);
57078
+ sink.write(`fail_fast=${String(result.data.failFast)}
57079
+ `);
57080
+ sink.write(`passed=${String(result.data.summary.passed)}
57081
+ `);
57082
+ sink.write(`failed=${String(result.data.summary.failed)}
57083
+ `);
57084
+ sink.write(`cancelled=${String(result.data.summary.cancelled)}
57085
+ `);
57086
+ sink.write(`skipped=${String(result.data.summary.skipped)}
57087
+ `);
57088
+ result.data.members.forEach((member, index) => {
57089
+ sink.write(`member_${String(index)}=${clean(member.id)}:${clean(member.kind)}:${clean(member.status)}:${String(member.exitCode)}
57090
+ `);
57091
+ });
57092
+ }
55346
57093
  if (isConnectData(result.data)) {
55347
57094
  sink.write(`endpoint=${clean(result.data.endpoint)}
55348
57095
  `);
@@ -55429,6 +57176,81 @@ function renderPlain(result, sink) {
55429
57176
  }
55430
57177
  if (isAutomationRunData(result.data))
55431
57178
  renderAutomationPlain(result.data, sink);
57179
+ if (isGradleManagedTestData(result.data)) {
57180
+ sink.write(`status=${clean(result.data.status)}
57181
+ `);
57182
+ sink.write(`gradle=${clean(result.data.wrapper)}
57183
+ `);
57184
+ if (result.data.task !== undefined)
57185
+ sink.write(`task=${clean(result.data.task)}
57186
+ `);
57187
+ if (result.data.tasks !== undefined) {
57188
+ sink.write(`task_count=${String(result.data.tasks.length)}
57189
+ `);
57190
+ result.data.tasks.forEach((task, index) => {
57191
+ sink.write(`task_${String(index)}=${clean(task)}
57192
+ `);
57193
+ });
57194
+ }
57195
+ if (result.data.outcome !== undefined)
57196
+ sink.write(`outcome=${clean(result.data.outcome)}
57197
+ `);
57198
+ if (result.data.tests !== undefined) {
57199
+ sink.write(`tests=${String(result.data.tests.total)}
57200
+ `);
57201
+ sink.write(`test_failures=${String(result.data.tests.failed)}
57202
+ `);
57203
+ sink.write(`test_errors=${String(result.data.tests.errors)}
57204
+ `);
57205
+ sink.write(`test_skipped=${String(result.data.tests.skipped)}
57206
+ `);
57207
+ }
57208
+ if (result.data.evidence !== undefined) {
57209
+ sink.write(`evidence=${clean(result.data.evidence.path)}
57210
+ `);
57211
+ sink.write(`native_file_count=${String(result.data.evidence.nativeFiles)}
57212
+ `);
57213
+ sink.write(`evidence_provenance=${clean(result.data.evidence.provenance)}
57214
+ `);
57215
+ }
57216
+ }
57217
+ if (isFirebaseTestLabData(result.data)) {
57218
+ sink.write(`status=${clean(result.data.status)}
57219
+ `);
57220
+ sink.write(`action=${clean(result.data.action)}
57221
+ `);
57222
+ if (result.data.project !== undefined)
57223
+ sink.write(`project=${clean(result.data.project)}
57224
+ `);
57225
+ if (result.data.outcome !== undefined)
57226
+ sink.write(`outcome=${clean(result.data.outcome)}
57227
+ `);
57228
+ if (result.data.matrixId !== undefined)
57229
+ sink.write(`matrix_id=${clean(result.data.matrixId)}
57230
+ `);
57231
+ if (result.data.consoleUrl !== undefined)
57232
+ sink.write(`console_url=${clean(result.data.consoleUrl)}
57233
+ `);
57234
+ if (result.data.devices !== undefined)
57235
+ sink.write(`device_count=${String(result.data.devices.length)}
57236
+ `);
57237
+ if (result.data.evidence !== undefined) {
57238
+ sink.write(`evidence=${clean(result.data.evidence.path)}
57239
+ `);
57240
+ sink.write(`provider_file_count=${String(result.data.evidence.providerFiles)}
57241
+ `);
57242
+ sink.write(`provider_file_omitted=${String(result.data.evidence.omittedFiles)}
57243
+ `);
57244
+ sink.write(`evidence_collection=${clean(result.data.evidence.collection)}
57245
+ `);
57246
+ if (result.data.evidence.remotePrefix !== undefined)
57247
+ sink.write(`provider_result_prefix=${clean(result.data.evidence.remotePrefix)}
57248
+ `);
57249
+ }
57250
+ if (result.data.remoteContinues !== undefined)
57251
+ sink.write(`remote_continues=${String(result.data.remoteContinues)}
57252
+ `);
57253
+ }
55432
57254
  if (isLogsData(result.data)) {
55433
57255
  sink.write(`serial=${clean(result.data.selected.transport.serial)}
55434
57256
  `);
@@ -55621,8 +57443,8 @@ function renderPlain(result, sink) {
55621
57443
  `);
55622
57444
  sink.write(`problem_count=${String(result.data.problems.length)}
55623
57445
  `);
55624
- result.data.problems.forEach((problem2) => {
55625
- sink.write(`problem=${clean(problem2.code)}:${clean(problem2.summary)}
57446
+ result.data.problems.forEach((problem4) => {
57447
+ sink.write(`problem=${clean(problem4.code)}:${clean(problem4.summary)}
55626
57448
  `);
55627
57449
  });
55628
57450
  }
@@ -55740,8 +57562,8 @@ function renderPlain(result, sink) {
55740
57562
  `);
55741
57563
  });
55742
57564
  }
55743
- for (const problem2 of result.problems) {
55744
- sink.write(`problem=${clean(problem2.code)}:${clean(problem2.summary)}
57565
+ for (const problem4 of result.problems) {
57566
+ sink.write(`problem=${clean(problem4.code)}:${clean(problem4.summary)}
55745
57567
  `);
55746
57568
  }
55747
57569
  }
@@ -55910,12 +57732,259 @@ function resolveTerminalCapabilities(preferences) {
55910
57732
  };
55911
57733
  }
55912
57734
 
57735
+ // src/cli/completion.ts
57736
+ var COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell", "nushell"];
57737
+ var COMMANDS = [
57738
+ { name: "agent", description: "Configure a project-local AI agent bridge", values: ["setup"] },
57739
+ {
57740
+ name: "app",
57741
+ description: "Resolve, inspect, install, or control one app",
57742
+ values: ["resolve", "info", "install", "launch", "stop", "restart", "clear-data", "uninstall"]
57743
+ },
57744
+ { name: "apps", description: "List packages on one Android target", values: ["list"] },
57745
+ {
57746
+ name: "capture",
57747
+ description: "Capture verified device evidence",
57748
+ values: ["screenshot", "screen-record"]
57749
+ },
57750
+ { name: "completion", description: "Generate shell completion", values: COMPLETION_SHELLS },
57751
+ {
57752
+ name: "config",
57753
+ description: "Validate or explain configuration",
57754
+ values: ["validate", "explain"]
57755
+ },
57756
+ { name: "connect", description: "Connect a wireless Android target" },
57757
+ { name: "context", description: "Export bounded AI-ready context" },
57758
+ { name: "dev", description: "Start a target-bound development session" },
57759
+ { name: "devices", description: "List visible Android targets" },
57760
+ { name: "doctor", description: "Inspect the local ADB environment" },
57761
+ { name: "help", description: "Show command help" },
57762
+ { name: "init", description: "Create detected project configuration" },
57763
+ {
57764
+ name: "inspect",
57765
+ description: "Build bounded device evidence",
57766
+ values: ["app", "failures", "ui"]
57767
+ },
57768
+ { name: "logs", description: "Stream focused target logs" },
57769
+ { name: "mcp", description: "Serve typed tools over local stdio" },
57770
+ { name: "open", description: "Open a deep link or web URL" },
57771
+ { name: "pair", description: "Pair through Wireless debugging" },
57772
+ { name: "ports", description: "Manage verified TCP mappings", values: ["forward", "reverse"] },
57773
+ { name: "problems", description: "Show saved session problems" },
57774
+ { name: "run", description: "Run one bounded verification job" },
57775
+ { name: "sessions", description: "Inspect saved sessions", values: ["list", "show", "events"] },
57776
+ { name: "test", description: "Run managed Android tests", values: ["gradle", "firebase"] },
57777
+ {
57778
+ name: "ui",
57779
+ description: "Perform a bounded verified UI action",
57780
+ values: [
57781
+ "audit",
57782
+ "find",
57783
+ "get",
57784
+ "assert",
57785
+ "compare",
57786
+ "tap",
57787
+ "long-press",
57788
+ "scroll",
57789
+ "swipe",
57790
+ "fill",
57791
+ "clear",
57792
+ "type",
57793
+ "press",
57794
+ "keyboard",
57795
+ "permission",
57796
+ "wait"
57797
+ ]
57798
+ },
57799
+ { name: "version", description: "Show version" }
57800
+ ];
57801
+ var GLOBAL_OPTIONS = [
57802
+ "--help",
57803
+ "-h",
57804
+ "--version",
57805
+ "-V",
57806
+ "--format",
57807
+ "--json",
57808
+ "--quiet",
57809
+ "--verbose",
57810
+ "--color",
57811
+ "--no-color",
57812
+ "--unicode",
57813
+ "--no-unicode",
57814
+ "--animation",
57815
+ "--no-animation",
57816
+ "--non-interactive",
57817
+ "--timeout",
57818
+ "--adb",
57819
+ "--adb-host",
57820
+ "--adb-port",
57821
+ "--config",
57822
+ "--profile",
57823
+ "--select",
57824
+ "--device",
57825
+ "--serial",
57826
+ "-s",
57827
+ "--transport-id",
57828
+ "--last",
57829
+ "--dry-run"
57830
+ ];
57831
+ var commandNames = () => COMMANDS.map(({ name }) => name).join(" ");
57832
+ var optionNames = () => GLOBAL_OPTIONS.join(" ");
57833
+ function bash() {
57834
+ const cases = COMMANDS.filter(({ values: values2 }) => values2 !== undefined).map(({ name, values: values2 }) => ` ${name}) candidates="${values2?.join(" ")}" ;;`).join(`
57835
+ `);
57836
+ return `# Generated by adb-ready. Source this file from ~/.bashrc.
57837
+ _adb_ready_complete() {
57838
+ local current command candidates
57839
+ COMPREPLY=()
57840
+ current="\${COMP_WORDS[COMP_CWORD]}"
57841
+ command="\${COMP_WORDS[1]}"
57842
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
57843
+ candidates="${commandNames()} ${optionNames()}"
57844
+ elif [[ \${COMP_CWORD} -eq 2 ]]; then
57845
+ case "\${command}" in
57846
+ ${cases}
57847
+ help) candidates="${commandNames()}" ;;
57848
+ *) candidates="${optionNames()}" ;;
57849
+ esac
57850
+ else
57851
+ candidates="${optionNames()}"
57852
+ fi
57853
+ COMPREPLY=( $(compgen -W "\${candidates}" -- "\${current}") )
57854
+ }
57855
+ complete -F _adb_ready_complete adb-ready adbr
57856
+ `;
57857
+ }
57858
+ function zsh() {
57859
+ const commands = COMMANDS.map(({ name, description }) => ` '${name}:${description.replaceAll("'", "'\\''")}'`).join(`
57860
+ `);
57861
+ const cases = COMMANDS.filter(({ values: values2 }) => values2 !== undefined).map(({ name, values: values2 }) => ` ${name}) _values 'action' ${values2?.join(" ")} ;;`).join(`
57862
+ `);
57863
+ return `#compdef adb-ready adbr
57864
+ # Generated by adb-ready. Add its directory to fpath, then run compinit.
57865
+ _adb_ready() {
57866
+ local -a commands
57867
+ commands=(
57868
+ ${commands}
57869
+ )
57870
+ _arguments -C \\
57871
+ '(-h --help)'{-h,--help}'[show help]' \\
57872
+ '(-V --version)'{-V,--version}'[show version]' \\
57873
+ '--format[select output format]:format:(human plain json ndjson)' \\
57874
+ '--json[emit JSON]' \\
57875
+ '--non-interactive[never prompt]' \\
57876
+ '1:command:->command' \\
57877
+ '*::argument:->argument'
57878
+ case $state in
57879
+ command) _describe 'command' commands ;;
57880
+ argument)
57881
+ case $words[2] in
57882
+ ${cases}
57883
+ help) _describe 'command' commands ;;
57884
+ esac
57885
+ ;;
57886
+ esac
57887
+ }
57888
+ _adb_ready "$@"
57889
+ `;
57890
+ }
57891
+ function fish() {
57892
+ const lines = [
57893
+ "# Generated by adb-ready. Save as ~/.config/fish/completions/adb-ready.fish."
57894
+ ];
57895
+ for (const binary of ["adb-ready", "adbr"]) {
57896
+ for (const { name, description } of COMMANDS) {
57897
+ const safeDescription = description.replaceAll("'", "\\'");
57898
+ lines.push(`complete -c ${binary} -f -n '__fish_use_subcommand' -a '${name}' -d '${safeDescription}'`);
57899
+ }
57900
+ for (const { name, values: values2 } of COMMANDS) {
57901
+ if (values2 === undefined)
57902
+ continue;
57903
+ lines.push(`complete -c ${binary} -f -n '__fish_seen_subcommand_from ${name}' -a '${values2.join(" ")}'`);
57904
+ }
57905
+ lines.push(`complete -c ${binary} -s h -l help -d 'Show help'`, `complete -c ${binary} -s V -l version -d 'Show version'`, `complete -c ${binary} -l json -d 'Emit JSON'`, `complete -c ${binary} -l non-interactive -d 'Never prompt'`, `complete -c ${binary} -l dry-run -d 'Plan without mutation'`);
57906
+ }
57907
+ return `${lines.join(`
57908
+ `)}
57909
+ `;
57910
+ }
57911
+ function powershell() {
57912
+ const map = COMMANDS.filter(({ values: values2 }) => values2 !== undefined).map(({ name, values: values2 }) => ` '${name}' = @('${values2?.join("','")}')`).join(`
57913
+ `);
57914
+ return `# Generated by adb-ready. Add this script to your PowerShell profile.
57915
+ $AdbReadyCommands = @('${commandNames().replaceAll(" ", "','")}')
57916
+ $AdbReadyOptions = @('${optionNames().replaceAll(" ", "','")}')
57917
+ $AdbReadyActions = @{
57918
+ ${map}
57919
+ }
57920
+ Register-ArgumentCompleter -Native -CommandName adb-ready,adbr -ScriptBlock {
57921
+ param($wordToComplete, $commandAst, $cursorPosition)
57922
+ $words = $commandAst.CommandElements | ForEach-Object { $_.Extent.Text }
57923
+ $candidates = if ($words.Count -le 1) {
57924
+ $AdbReadyCommands + $AdbReadyOptions
57925
+ } elseif ($words.Count -eq 2 -and $AdbReadyActions.ContainsKey($words[1])) {
57926
+ $AdbReadyActions[$words[1]] + $AdbReadyOptions
57927
+ } elseif ($words[1] -eq 'help') {
57928
+ $AdbReadyCommands
57929
+ } else {
57930
+ $AdbReadyOptions
57931
+ }
57932
+ $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
57933
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
57934
+ }
57935
+ }
57936
+ `;
57937
+ }
57938
+ function nushellExtern(binary) {
57939
+ return `export extern "${binary}" [
57940
+ command?: string@"nu-complete adb-ready commands"
57941
+ ...args: string
57942
+ --help(-h)
57943
+ --version(-V)
57944
+ --json
57945
+ --quiet
57946
+ --verbose
57947
+ --non-interactive
57948
+ --dry-run
57949
+ ]`;
57950
+ }
57951
+ function nushell() {
57952
+ const records = COMMANDS.map(({ name, description }) => ` { value: "${name}", description: "${description}" }`).join(`
57953
+ `);
57954
+ return `# Generated by adb-ready. Source this file from config.nu.
57955
+ def "nu-complete adb-ready commands" [] {
57956
+ [
57957
+ ${records}
57958
+ ]
57959
+ }
57960
+
57961
+ ${nushellExtern("adb-ready")}
57962
+
57963
+ ${nushellExtern("adbr")}
57964
+ `;
57965
+ }
57966
+ function generateCompletion(shell) {
57967
+ switch (shell) {
57968
+ case "bash":
57969
+ return bash();
57970
+ case "zsh":
57971
+ return zsh();
57972
+ case "fish":
57973
+ return fish();
57974
+ case "powershell":
57975
+ return powershell();
57976
+ case "nushell":
57977
+ return nushell();
57978
+ }
57979
+ }
57980
+
55913
57981
  // src/cli/arguments.ts
55914
- var COMMANDS = new Set([
57982
+ var COMMANDS2 = new Set([
55915
57983
  "agent",
55916
57984
  "app",
55917
57985
  "apps",
55918
57986
  "capture",
57987
+ "completion",
55919
57988
  "config",
55920
57989
  "connect",
55921
57990
  "context",
@@ -55933,6 +58002,7 @@ var COMMANDS = new Set([
55933
58002
  "problems",
55934
58003
  "run",
55935
58004
  "sessions",
58005
+ "test",
55936
58006
  "ui",
55937
58007
  "version"
55938
58008
  ]);
@@ -56008,6 +58078,12 @@ var BOOLEAN_OPTIONS = new Set([
56008
58078
  "--all",
56009
58079
  "--all-projects",
56010
58080
  "--interactive-only",
58081
+ "--software-rendering",
58082
+ "--allow-deprecated",
58083
+ "--allow-reduced-stability",
58084
+ "--allow-low-capacity",
58085
+ "--fail-fast",
58086
+ "--no-fail-fast",
56011
58087
  "--submit",
56012
58088
  "--secret-stdin"
56013
58089
  ]);
@@ -56058,6 +58134,7 @@ function splitLongOption(argument) {
56058
58134
  function parseArguments(argv) {
56059
58135
  let command;
56060
58136
  let helpTarget;
58137
+ let completionShell;
56061
58138
  let format = "human";
56062
58139
  let quiet = false;
56063
58140
  let verbose = false;
@@ -56079,6 +58156,10 @@ function parseArguments(argv) {
56079
58156
  let pairingCodeStdin = false;
56080
58157
  let remembered = false;
56081
58158
  let dryRun = false;
58159
+ let poolName;
58160
+ let maxConcurrency;
58161
+ let failFast;
58162
+ let leaseWaitMs;
56082
58163
  let allProjects = false;
56083
58164
  let portDirection;
56084
58165
  let portAction;
@@ -56093,6 +58174,24 @@ function parseArguments(argv) {
56093
58174
  let customCommand;
56094
58175
  let runCommand;
56095
58176
  let runTimeoutMs;
58177
+ let testKind;
58178
+ let gradleTask;
58179
+ let gradlePath;
58180
+ let gradleShards;
58181
+ let gradleSoftwareRendering = false;
58182
+ let firebaseAction;
58183
+ let firebaseMatrixId;
58184
+ const firebaseDevices = [];
58185
+ let gcloudPath;
58186
+ let cloudProject;
58187
+ let firebaseApp;
58188
+ let firebaseTest;
58189
+ let firebaseResultsBucket;
58190
+ let firebaseResultsDir;
58191
+ let firebaseTestTimeout;
58192
+ let firebaseAllowDeprecated = false;
58193
+ let firebaseAllowReducedStability = false;
58194
+ let firebaseAllowLowCapacity = false;
56096
58195
  let avdName;
56097
58196
  let deployArtifact = false;
56098
58197
  const runArtifactPaths = [];
@@ -56173,17 +58272,17 @@ function parseArguments(argv) {
56173
58272
  }
56174
58273
  if (!argument.startsWith("-")) {
56175
58274
  if (command === undefined) {
56176
- if (!COMMANDS.has(argument)) {
56177
- const hint = suggestion(argument, COMMANDS);
58275
+ if (!COMMANDS2.has(argument)) {
58276
+ const hint = suggestion(argument, COMMANDS2);
56178
58277
  return failure4("CLI_USAGE", `Unknown command: ${argument}${hint === "" ? "" : `. ${hint}`}`);
56179
58278
  }
56180
58279
  const candidate = argument;
56181
58280
  command = candidate;
56182
58281
  continue;
56183
58282
  }
56184
- if (command === "help" && COMMANDS.has(argument)) {
58283
+ if (command === "help" && COMMANDS2.has(argument)) {
56185
58284
  const candidate = argument;
56186
- if (candidate === "agent" || candidate === "app" || candidate === "apps" || candidate === "capture" || candidate === "connect" || candidate === "config" || candidate === "context" || candidate === "dev" || candidate === "devices" || candidate === "doctor" || candidate === "logs" || candidate === "mcp" || candidate === "open" || candidate === "init" || candidate === "inspect" || candidate === "pair" || candidate === "ports" || candidate === "problems" || candidate === "run" || candidate === "sessions" || candidate === "ui" || candidate === "version") {
58285
+ if (candidate === "agent" || candidate === "app" || candidate === "apps" || candidate === "capture" || candidate === "completion" || candidate === "connect" || candidate === "config" || candidate === "context" || candidate === "dev" || candidate === "devices" || candidate === "doctor" || candidate === "logs" || candidate === "mcp" || candidate === "open" || candidate === "init" || candidate === "inspect" || candidate === "pair" || candidate === "ports" || candidate === "problems" || candidate === "run" || candidate === "sessions" || candidate === "test" || candidate === "ui" || candidate === "version") {
56187
58286
  helpTarget = candidate;
56188
58287
  continue;
56189
58288
  }
@@ -56192,10 +58291,35 @@ function parseArguments(argv) {
56192
58291
  endpoint = argument;
56193
58292
  continue;
56194
58293
  }
58294
+ if (command === "completion" && completionShell === undefined) {
58295
+ if (!COMPLETION_SHELLS.includes(argument)) {
58296
+ return failure4("CLI_INVALID_VALUE", `Invalid completion shell: ${argument}. Expected bash, zsh, fish, powershell, or nushell.`);
58297
+ }
58298
+ completionShell = argument;
58299
+ continue;
58300
+ }
56195
58301
  if (command === "agent" && argument === "setup" && !agentSetupSeen) {
56196
58302
  agentSetupSeen = true;
56197
58303
  continue;
56198
58304
  }
58305
+ if (command === "test") {
58306
+ if (testKind === undefined && (argument === "firebase" || argument === "gradle")) {
58307
+ testKind = argument;
58308
+ continue;
58309
+ }
58310
+ if (testKind === "gradle" && gradleTask === undefined) {
58311
+ gradleTask = argument;
58312
+ continue;
58313
+ }
58314
+ if (testKind === "firebase" && firebaseAction === undefined && (argument === "cancel" || argument === "devices" || argument === "instrumentation" || argument === "robo")) {
58315
+ firebaseAction = argument;
58316
+ continue;
58317
+ }
58318
+ if (testKind === "firebase" && firebaseAction === "cancel" && firebaseMatrixId === undefined) {
58319
+ firebaseMatrixId = argument;
58320
+ continue;
58321
+ }
58322
+ }
56199
58323
  if (command === "agent" && agentSetupSeen && agentClient === undefined) {
56200
58324
  if (!AGENT_CLIENTS.has(argument)) {
56201
58325
  return failure4("CLI_INVALID_VALUE", `Invalid agent client: ${argument}. Expected codex, claude-code, cursor, vscode, windsurf, or generic.`);
@@ -56373,6 +58497,35 @@ function parseArguments(argv) {
56373
58497
  remembered = true;
56374
58498
  } else if (option === "--dry-run") {
56375
58499
  dryRun = true;
58500
+ } else if (option === "--pool") {
58501
+ const value = readValue();
58502
+ if (typeof value !== "string")
58503
+ return value;
58504
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/u.test(value)) {
58505
+ return failure4("CLI_INVALID_VALUE", `Invalid target pool name: ${value}.`, option);
58506
+ }
58507
+ poolName = value;
58508
+ } else if (option === "--max-concurrency") {
58509
+ const value = readValue();
58510
+ if (typeof value !== "string")
58511
+ return value;
58512
+ const parsed = Number(value);
58513
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 32) {
58514
+ return failure4("CLI_INVALID_VALUE", "--max-concurrency must be an integer from 1 to 32.", option);
58515
+ }
58516
+ maxConcurrency = parsed;
58517
+ } else if (option === "--fail-fast") {
58518
+ failFast = true;
58519
+ } else if (option === "--no-fail-fast") {
58520
+ failFast = false;
58521
+ } else if (option === "--lease-wait") {
58522
+ const value = readValue();
58523
+ if (typeof value !== "string")
58524
+ return value;
58525
+ leaseWaitMs = parseDuration(value);
58526
+ if (leaseWaitMs === undefined || leaseWaitMs > 3600000) {
58527
+ return failure4("CLI_INVALID_VALUE", "--lease-wait must be a positive duration up to 1h.", option);
58528
+ }
56376
58529
  } else if (option === "--deploy") {
56377
58530
  deployArtifact = true;
56378
58531
  } else if (option === "--all-projects") {
@@ -56692,6 +58845,96 @@ function parseArguments(argv) {
56692
58845
  if (runTimeoutMs === undefined) {
56693
58846
  return failure4("CLI_INVALID_VALUE", `Invalid run timeout: ${value}. Use a positive duration such as 30s or 10m.`, option);
56694
58847
  }
58848
+ } else if (option === "--gradle") {
58849
+ const value = readValue();
58850
+ if (typeof value !== "string")
58851
+ return value;
58852
+ if (value.trim() !== value || value.length === 0 || !printableValue(value)) {
58853
+ return failure4("CLI_INVALID_VALUE", "--gradle must be a non-empty printable path.", option);
58854
+ }
58855
+ gradlePath = value;
58856
+ } else if (option === "--shards") {
58857
+ const value = readValue();
58858
+ if (typeof value !== "string")
58859
+ return value;
58860
+ const parsed = Number(value);
58861
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 256) {
58862
+ return failure4("CLI_INVALID_VALUE", "--shards must be an integer from 1 to 256.", option);
58863
+ }
58864
+ gradleShards = parsed;
58865
+ } else if (option === "--software-rendering") {
58866
+ gradleSoftwareRendering = true;
58867
+ } else if (option === "--allow-deprecated") {
58868
+ firebaseAllowDeprecated = true;
58869
+ } else if (option === "--allow-reduced-stability") {
58870
+ firebaseAllowReducedStability = true;
58871
+ } else if (option === "--allow-low-capacity") {
58872
+ firebaseAllowLowCapacity = true;
58873
+ } else if (option === "--gcloud") {
58874
+ const value = readValue();
58875
+ if (typeof value !== "string")
58876
+ return value;
58877
+ if (value.trim() !== value || value.length === 0 || !printableValue(value)) {
58878
+ return failure4("CLI_INVALID_VALUE", "--gcloud must be a non-empty printable path.", option);
58879
+ }
58880
+ gcloudPath = value;
58881
+ } else if (option === "--project") {
58882
+ const value = readValue();
58883
+ if (typeof value !== "string")
58884
+ return value;
58885
+ if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/u.test(value)) {
58886
+ return failure4("CLI_INVALID_VALUE", "--project must be an explicit Google Cloud project ID.", option);
58887
+ }
58888
+ cloudProject = value;
58889
+ } else if (option === "--app") {
58890
+ const value = readValue();
58891
+ if (typeof value !== "string")
58892
+ return value;
58893
+ if (value.trim() !== value || value.length === 0 || !printableValue(value) || value.startsWith("gs://") && !/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]\/.+/u.test(value) || value.includes("?")) {
58894
+ return failure4("CLI_INVALID_VALUE", "--app must be a non-empty printable artifact path.", option);
58895
+ }
58896
+ firebaseApp = value;
58897
+ } else if (option === "--test-apk") {
58898
+ const value = readValue();
58899
+ if (typeof value !== "string")
58900
+ return value;
58901
+ if (value.trim() !== value || value.length === 0 || !printableValue(value) || value.startsWith("gs://") && !/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]\/.+/u.test(value) || value.includes("?")) {
58902
+ return failure4("CLI_INVALID_VALUE", "--test-apk must be a non-empty printable path.", option);
58903
+ }
58904
+ firebaseTest = value;
58905
+ } else if (option === "--test-device") {
58906
+ const value = readValue();
58907
+ if (typeof value !== "string")
58908
+ return value;
58909
+ const parsed = parseFirebaseDevice(value);
58910
+ if (parsed === undefined) {
58911
+ return failure4("CLI_INVALID_VALUE", "--test-device requires model=ID,version=ID with optional locale and orientation.", option);
58912
+ }
58913
+ firebaseDevices.push(parsed);
58914
+ } else if (option === "--results-bucket") {
58915
+ const value = readValue();
58916
+ if (typeof value !== "string")
58917
+ return value;
58918
+ if (!/^gs:\/\/[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]$/u.test(value)) {
58919
+ return failure4("CLI_INVALID_VALUE", "--results-bucket must be a gs:// bucket URI.", option);
58920
+ }
58921
+ firebaseResultsBucket = value;
58922
+ } else if (option === "--results-dir") {
58923
+ const value = readValue();
58924
+ if (typeof value !== "string")
58925
+ return value;
58926
+ if (value.length > 512 || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/u.test(value) || value.trim() !== value || value.startsWith("/") || value.endsWith("/") || value.includes("//") || value.split("/").some((segment) => segment === "." || segment === "..") || !printableValue(value)) {
58927
+ return failure4("CLI_INVALID_VALUE", "--results-dir must be a safe unique bucket-relative path.", option);
58928
+ }
58929
+ firebaseResultsDir = value;
58930
+ } else if (option === "--test-timeout") {
58931
+ const value = readValue();
58932
+ if (typeof value !== "string")
58933
+ return value;
58934
+ if (parseDuration(value) === undefined) {
58935
+ return failure4("CLI_INVALID_VALUE", "--test-timeout must be a positive duration such as 5m.", option);
58936
+ }
58937
+ firebaseTestTimeout = value;
56695
58938
  } else if (option === "--avd") {
56696
58939
  const value = readValue();
56697
58940
  if (typeof value !== "string")
@@ -56797,6 +59040,9 @@ function parseArguments(argv) {
56797
59040
  "--adb-port",
56798
59041
  "--config",
56799
59042
  "--profile",
59043
+ "--pool",
59044
+ "--max-concurrency",
59045
+ "--lease-wait",
56800
59046
  "--mcp-profile",
56801
59047
  "--device",
56802
59048
  "--serial",
@@ -56827,6 +59073,16 @@ function parseArguments(argv) {
56827
59073
  "--max-records",
56828
59074
  "--budget",
56829
59075
  "--run-timeout",
59076
+ "--gradle",
59077
+ "--shards",
59078
+ "--gcloud",
59079
+ "--project",
59080
+ "--app",
59081
+ "--test-apk",
59082
+ "--test-device",
59083
+ "--results-bucket",
59084
+ "--results-dir",
59085
+ "--test-timeout",
56830
59086
  "--avd",
56831
59087
  "--artifact",
56832
59088
  "--variant",
@@ -56891,8 +59147,8 @@ function parseArguments(argv) {
56891
59147
  if (remembered && (select || device !== undefined || transportId !== undefined)) {
56892
59148
  return failure4("CLI_USAGE", "--last cannot be combined with another target selector.");
56893
59149
  }
56894
- if (dryRun && command !== "connect" && command !== "dev" && command !== "run" && command !== "app" && command !== "open" && command !== "pair" && command !== "ports" && command !== "init" && command !== "agent" && command !== "ui") {
56895
- return failure4("CLI_USAGE", "--dry-run can only be used with app, connect, dev, run, init, open, pair, or ports.", "--dry-run");
59150
+ if (dryRun && command !== "connect" && command !== "dev" && command !== "run" && command !== "app" && command !== "open" && command !== "pair" && command !== "ports" && command !== "init" && command !== "agent" && command !== "test" && command !== "ui") {
59151
+ return failure4("CLI_USAGE", "--dry-run can only be used with app, connect, dev, run, init, open, pair, ports, test, or agent setup.", "--dry-run");
56896
59152
  }
56897
59153
  if (command === "ports") {
56898
59154
  if (portDirection === undefined) {
@@ -56926,6 +59182,12 @@ function parseArguments(argv) {
56926
59182
  if (command === "capture" && captureKind === undefined) {
56927
59183
  return failure4("CLI_USAGE", "capture requires screenshot or screen-record.");
56928
59184
  }
59185
+ if (command === "completion" && completionShell === undefined) {
59186
+ return failure4("CLI_USAGE", "completion requires bash, zsh, fish, powershell, or nushell.");
59187
+ }
59188
+ if (command === "completion" && argv.some((argument) => argument !== "completion" && argument !== completionShell)) {
59189
+ return failure4("CLI_USAGE", "completion accepts only one shell name; redirect its exact script output to a file.");
59190
+ }
56929
59191
  if (command === "inspect" && inspectKind === undefined) {
56930
59192
  return failure4("CLI_USAGE", "inspect requires app, failures, or ui.");
56931
59193
  }
@@ -57122,8 +59384,56 @@ function parseArguments(argv) {
57122
59384
  if (command === "run" && runCommand === undefined) {
57123
59385
  return failure4("CLI_USAGE", "run requires a bounded command after --.");
57124
59386
  }
59387
+ const poolCommand = command === "run" || command === "test" && testKind === "firebase" && (firebaseAction === "instrumentation" || firebaseAction === "robo");
59388
+ if (poolName !== undefined && !poolCommand) {
59389
+ return failure4("CLI_USAGE", "--pool can only be used with run or a Firebase instrumentation/Robo workflow.", "--pool");
59390
+ }
59391
+ if (poolName !== undefined && (select || remembered || device !== undefined || transportId !== undefined || avdName !== undefined)) {
59392
+ return failure4("CLI_USAGE", "--pool cannot be combined with another target selector.", "--pool");
59393
+ }
59394
+ if (poolName !== undefined && firebaseDevices.length > 0) {
59395
+ return failure4("CLI_USAGE", "--pool cannot be combined with --test-device.", "--pool");
59396
+ }
59397
+ if ((maxConcurrency !== undefined || failFast !== undefined || leaseWaitMs !== undefined) && poolName === undefined) {
59398
+ return failure4("CLI_USAGE", "--max-concurrency, --fail-fast, --no-fail-fast, and --lease-wait require --pool.");
59399
+ }
59400
+ if (leaseWaitMs !== undefined && command !== "run") {
59401
+ return failure4("CLI_USAGE", "--lease-wait only applies to local run pools.", "--lease-wait");
59402
+ }
57125
59403
  if (runTimeoutMs !== undefined && command !== "run") {
57126
- return failure4("CLI_USAGE", "--run-timeout can only be used with run.", "--run-timeout");
59404
+ if (command !== "test") {
59405
+ return failure4("CLI_USAGE", "--run-timeout can only be used with run or a supported test workflow.", "--run-timeout");
59406
+ }
59407
+ }
59408
+ if (command === "test" && testKind === undefined) {
59409
+ return failure4("CLI_USAGE", "test requires a workflow: adb-ready test gradle [TASK] or adb-ready test firebase ACTION.");
59410
+ }
59411
+ if (testKind !== "gradle" && (gradlePath !== undefined || gradleShards !== undefined || gradleSoftwareRendering)) {
59412
+ return failure4("CLI_USAGE", "--gradle, --shards, and --software-rendering can only be used with test gradle.");
59413
+ }
59414
+ const firebaseOptions = firebaseAction !== undefined || firebaseMatrixId !== undefined || firebaseDevices.length > 0 || gcloudPath !== undefined || cloudProject !== undefined || firebaseApp !== undefined || firebaseTest !== undefined || firebaseResultsBucket !== undefined || firebaseResultsDir !== undefined || firebaseTestTimeout !== undefined || firebaseAllowDeprecated || firebaseAllowReducedStability || firebaseAllowLowCapacity;
59415
+ if (firebaseOptions && (command !== "test" || testKind !== "firebase")) {
59416
+ return failure4("CLI_USAGE", "Firebase options can only be used with adb-ready test firebase.");
59417
+ }
59418
+ if (testKind === "firebase") {
59419
+ if (firebaseAction === undefined) {
59420
+ return failure4("CLI_USAGE", "test firebase requires devices, instrumentation, robo, or cancel.");
59421
+ }
59422
+ if (firebaseResultsDir !== undefined && firebaseResultsBucket === undefined) {
59423
+ return failure4("CLI_USAGE", "--results-dir requires --results-bucket.");
59424
+ }
59425
+ if (firebaseAction === "instrumentation" && firebaseTest === undefined) {
59426
+ return failure4("CLI_USAGE", "Firebase instrumentation requires --test-apk PATH.");
59427
+ }
59428
+ if ((firebaseAction === "instrumentation" || firebaseAction === "robo") && (firebaseApp === undefined || firebaseDevices.length === 0 && poolName === undefined)) {
59429
+ return failure4("CLI_USAGE", `Firebase ${firebaseAction} requires --app PATH and at least one --test-device.`);
59430
+ }
59431
+ if (firebaseAction === "cancel" && (firebaseMatrixId === undefined || cloudProject === undefined)) {
59432
+ return failure4("CLI_USAGE", "Firebase cancellation requires MATRIX_ID and --project PROJECT.");
59433
+ }
59434
+ if ((firebaseAction === "cancel" || firebaseAction === "devices") && (firebaseApp !== undefined || firebaseTest !== undefined || firebaseDevices.length > 0 || firebaseResultsBucket !== undefined || firebaseResultsDir !== undefined || firebaseTestTimeout !== undefined)) {
59435
+ return failure4("CLI_USAGE", `Firebase ${firebaseAction} does not accept test artifacts, dimensions, or result storage options.`);
59436
+ }
57127
59437
  }
57128
59438
  const autonomousRunOptions = avdName !== undefined || deployArtifact || runArtifactPaths.length > 0 || runVariant !== undefined || javaPath !== undefined || bundletoolPath !== undefined;
57129
59439
  if (autonomousRunOptions && command !== "run") {
@@ -57183,6 +59493,7 @@ function parseArguments(argv) {
57183
59493
  options: {
57184
59494
  command,
57185
59495
  ...helpTarget === undefined ? {} : { helpTarget },
59496
+ ...completionShell === undefined ? {} : { completionShell },
57186
59497
  format,
57187
59498
  quiet,
57188
59499
  verbose,
@@ -57204,6 +59515,10 @@ function parseArguments(argv) {
57204
59515
  pairingCodeStdin,
57205
59516
  remembered,
57206
59517
  dryRun,
59518
+ ...poolName === undefined ? {} : { poolName },
59519
+ ...maxConcurrency === undefined ? {} : { maxConcurrency },
59520
+ ...failFast === undefined ? {} : { failFast },
59521
+ ...leaseWaitMs === undefined ? {} : { leaseWaitMs },
57207
59522
  allProjects,
57208
59523
  ...portDirection === undefined ? {} : { portDirection },
57209
59524
  ...portAction === undefined ? {} : { portAction },
@@ -57218,6 +59533,24 @@ function parseArguments(argv) {
57218
59533
  ...customCommand === undefined ? {} : { customCommand },
57219
59534
  ...runCommand === undefined ? {} : { runCommand },
57220
59535
  ...runTimeoutMs === undefined ? {} : { runTimeoutMs },
59536
+ ...testKind === undefined ? {} : { testKind },
59537
+ ...gradleTask === undefined ? {} : { gradleTask },
59538
+ ...gradlePath === undefined ? {} : { gradlePath },
59539
+ ...gradleShards === undefined ? {} : { gradleShards },
59540
+ ...gradleSoftwareRendering ? { gradleSoftwareRendering: true } : {},
59541
+ ...firebaseAction === undefined ? {} : { firebaseAction },
59542
+ ...firebaseMatrixId === undefined ? {} : { firebaseMatrixId },
59543
+ ...firebaseDevices.length === 0 ? {} : { firebaseDevices },
59544
+ ...gcloudPath === undefined ? {} : { gcloudPath },
59545
+ ...cloudProject === undefined ? {} : { cloudProject },
59546
+ ...firebaseApp === undefined ? {} : { firebaseApp },
59547
+ ...firebaseTest === undefined ? {} : { firebaseTest },
59548
+ ...firebaseResultsBucket === undefined ? {} : { firebaseResultsBucket },
59549
+ ...firebaseResultsDir === undefined ? {} : { firebaseResultsDir },
59550
+ ...firebaseTestTimeout === undefined ? {} : { firebaseTestTimeout },
59551
+ ...firebaseAllowDeprecated ? { firebaseAllowDeprecated: true } : {},
59552
+ ...firebaseAllowReducedStability ? { firebaseAllowReducedStability: true } : {},
59553
+ ...firebaseAllowLowCapacity ? { firebaseAllowLowCapacity: true } : {},
57221
59554
  ...avdName === undefined ? {} : { avdName },
57222
59555
  ...deployArtifact ? { deployArtifact: true } : {},
57223
59556
  ...runArtifactPaths.length === 0 ? {} : { runArtifactPaths },
@@ -57289,6 +59622,7 @@ Commands:
57289
59622
  app ACTION [APP_ID] Resolve, inspect, install, or control one app
57290
59623
  apps list List packages on one Android target
57291
59624
  capture ACTION Save a verified screenshot or bounded screen recording
59625
+ completion SHELL Generate completion for bash, zsh, fish, PowerShell, or Nushell
57292
59626
  config ACTION Validate or explain resolved configuration
57293
59627
  connect [HOST:PORT] Connect and verify a wireless Android target
57294
59628
  context [SESSION] Export bounded AI-ready diagnostic context
@@ -57304,6 +59638,8 @@ Commands:
57304
59638
  pair [HOST:PORT] Pair using Android's six-digit pairing code
57305
59639
  ports DIRECTION ACTION Manage verified TCP forward/reverse mappings
57306
59640
  sessions [ACTION] Inspect saved development sessions
59641
+ test gradle [TASK] Discover or run a Gradle managed-device test
59642
+ test firebase ACTION Run or manage a Firebase Test Lab matrix
57307
59643
  ui ACTION Perform a bounded, verified Android UI action
57308
59644
  problems [SESSION] Show problems from a saved session
57309
59645
  help [COMMAND] Show help
@@ -57393,6 +59729,12 @@ Writes verified binary evidence inside the current project. Existing files are
57393
59729
  never replaced unless --force is explicit. Recordings are bounded to 180s.
57394
59730
  Screenshot crops use source pixels. Dimension limits preserve aspect ratio and
57395
59731
  never upscale the image. The CLI keeps full resolution unless limits are set.
59732
+ `,
59733
+ completion: `Usage: adb-ready completion bash|zsh|fish|powershell|nushell
59734
+
59735
+ Prints a static completion script for both adb-ready and adbr. Generation is
59736
+ offline, deterministic, and never loads project configuration or contacts ADB.
59737
+ See docs/completions.md for one-time installation instructions.
57396
59738
  `,
57397
59739
  config: `Usage:
57398
59740
  adb-ready config validate [options]
@@ -57446,6 +59788,10 @@ waits for every readiness assertion, runs one bounded verification command,
57446
59788
  then cleans owned resources. The verification exit code is preserved.
57447
59789
 
57448
59790
  Run options:
59791
+ --pool NAME Fan out through one explicit project target pool
59792
+ --max-concurrency N Override the pool's bounded concurrency (1-32)
59793
+ --[no-]fail-fast Override whether a required failure stops queued work
59794
+ --lease-wait DURATION Bound host-local target queueing (maximum: 1h)
57449
59795
  --avd NAME Reuse or start this exact existing AVD
57450
59796
  --deploy Discover and deploy one compatible Android artifact
57451
59797
  --artifact PATH Deploy this project-local APK, APK Set, or AAB; repeat for splits
@@ -57462,6 +59808,8 @@ Verification processes also receive ANDROID_SERIAL, ADB_READY_TARGET_SERIAL, and
57462
59808
  ADB_READY_VERIFIER_OUTPUT_DIR. Direct Maestro test and Android CLI layout/screen-capture commands
57463
59809
  are pinned to the selected target and their native reports are retained automatically.
57464
59810
  AVDs must already exist. ADB Ready never creates or upgrades an SDK or AVD implicitly.
59811
+ Pool members each retain an isolated session and result. Pool mode never changes the
59812
+ single-target invariant inside a run.
57465
59813
  `,
57466
59814
  doctor: `Usage: adb-ready doctor [options]
57467
59815
 
@@ -57611,6 +59959,50 @@ List filters:
57611
59959
  --since DURATION Keep sessions updated in the final window
57612
59960
  --preset NAME Keep sessions using one development preset
57613
59961
  --limit COUNT Return at most 1-100 sessions
59962
+ `,
59963
+ test: `Usage:
59964
+ adb-ready test gradle
59965
+ adb-ready test gradle TASK [options]
59966
+ adb-ready test firebase devices [options]
59967
+ adb-ready test firebase instrumentation [options]
59968
+ adb-ready test firebase robo [options]
59969
+ adb-ready test firebase cancel MATRIX_ID --project PROJECT [options]
59970
+
59971
+ Discovers declared Gradle Managed Device tasks or hands one exact task to the
59972
+ project's checked-in Gradle Wrapper. Gradle owns provisioning, test execution,
59973
+ sharding, and teardown; ADB Ready never leases or controls its transient target.
59974
+
59975
+ Gradle managed-device options:
59976
+ --gradle PATH Use an explicit Gradle Wrapper or executable
59977
+ --shards COUNT Request 1-256 uniform managed-device shards
59978
+ --software-rendering Request Gradle's SwiftShader server rendering mode
59979
+ --run-timeout DURATION Bound task execution (default: 15m)
59980
+ --dry-run Print the exact task plan without starting Gradle
59981
+
59982
+ Native JUnit XML and HTML reports are retained with bounded Gradle output under
59983
+ .adb-ready/artifacts/. Product assertion failures, infrastructure failures,
59984
+ timeouts, cancellation, and missing tasks remain distinct structured outcomes.
59985
+
59986
+ Firebase Test Lab options:
59987
+ --pool NAME Run one matrix per named Firebase dimension
59988
+ --max-concurrency N Bound simultaneously observed matrices (1-32)
59989
+ --[no-]fail-fast Stop submitting queued dimensions after a required failure
59990
+ --project PROJECT Use one explicit Google Cloud project
59991
+ --gcloud PATH Use an explicit gcloud executable
59992
+ --app PATH App APK or supported app bundle
59993
+ --test-apk PATH Instrumentation test APK
59994
+ --test-device DIMENSIONS Exact model=ID,version=ID[,locale=..,orientation=..]
59995
+ --test-timeout DURATION Provider-side timeout for each execution
59996
+ --run-timeout DURATION Bound local observation (default: 30m)
59997
+ --results-bucket GS_URI Store provider artifacts in an owned GCS bucket
59998
+ --results-dir PATH Unique bucket-relative result directory
59999
+ --allow-deprecated Explicitly accept a deprecated catalog dimension
60000
+ --allow-reduced-stability Explicitly accept a reduced-stability dimension
60001
+ --allow-low-capacity Explicitly accept a low-capacity dimension
60002
+
60003
+ Every run validates dimensions against the live catalog before upload. Stopping
60004
+ local observation leaves the remote matrix running; only the explicit cancel
60005
+ action cancels it.
57614
60006
  `,
57615
60007
  version: `Usage: adb-ready version
57616
60008
 
@@ -57619,6 +60011,92 @@ configuration or ADB. Human output and --version remain one bare version line;
57619
60011
  --format plain, --json, and --format ndjson use the global automation contract.
57620
60012
  `
57621
60013
  };
60014
+
60015
+ class CapturedOutput {
60016
+ isTTY = false;
60017
+ columns = 120;
60018
+ rows = 40;
60019
+ #chunks = [];
60020
+ write(value) {
60021
+ this.#chunks.push(value);
60022
+ }
60023
+ text() {
60024
+ return this.#chunks.join("");
60025
+ }
60026
+ }
60027
+ function parsedResultEnvelope(value) {
60028
+ try {
60029
+ const parsed = JSON.parse(value.trim());
60030
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
60031
+ return;
60032
+ const result = parsed;
60033
+ return result.schemaVersion === SCHEMA_VERSION && typeof result.command === "string" && typeof result.commandId === "string" && typeof result.ok === "boolean" && typeof result.startedAt === "string" && typeof result.finishedAt === "string" && typeof result.durationMs === "number" && Array.isArray(result.problems) ? result : undefined;
60034
+ } catch {
60035
+ return;
60036
+ }
60037
+ }
60038
+ function targetPoolProblem(code, summary2, detail2, commandId = "target-pool", retryable = false) {
60039
+ return {
60040
+ code,
60041
+ category: "automation.pool",
60042
+ severity: "error",
60043
+ summary: summary2,
60044
+ detail: detail2,
60045
+ retryable,
60046
+ evidence: [],
60047
+ actions: [],
60048
+ correlation: { commandId }
60049
+ };
60050
+ }
60051
+ function targetPoolProgress(options, io) {
60052
+ if (options.format !== "human" || options.quiet)
60053
+ return {};
60054
+ return {
60055
+ onMemberStart: (member) => io.error.write(`pool ${member.id} running
60056
+ `),
60057
+ onMemberFinish: (member) => io.error.write(`pool ${member.id} ${member.status} · exit ${String(member.exitCode)}
60058
+ `)
60059
+ };
60060
+ }
60061
+ async function runLocalTargetPool(argv, options, pool, io, dependencies, signal) {
60062
+ return await runTargetPool({
60063
+ name: options.poolName ?? "",
60064
+ pool,
60065
+ ...options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency },
60066
+ ...options.failFast === undefined ? {} : { failFast: options.failFast },
60067
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60068
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60069
+ ...signal === undefined ? {} : { signal },
60070
+ ...targetPoolProgress(options, io)
60071
+ }, async (member, memberSignal) => {
60072
+ if (member.kind === "firebase") {
60073
+ throw new Error("Firebase dimensions cannot execute through a local run pool.");
60074
+ }
60075
+ const output = new CapturedOutput;
60076
+ const error5 = new CapturedOutput;
60077
+ const memberDependencies = {
60078
+ ...dependencies,
60079
+ targetLease: dependencies.targetLease === false ? false : {
60080
+ ...dependencies.targetLease ?? {},
60081
+ waitTimeoutMs: options.leaseWaitMs ?? pool.leaseWaitMs,
60082
+ signal: memberSignal
60083
+ },
60084
+ writeRememberedTarget: async () => ({ ok: true, path: "target-pool" })
60085
+ };
60086
+ const exitCode2 = await runCli(targetPoolMemberArguments(argv, member), {
60087
+ input: io.input,
60088
+ output,
60089
+ error: error5,
60090
+ cwd: io.cwd,
60091
+ env: io.env
60092
+ }, memberDependencies, memberSignal);
60093
+ const result = parsedResultEnvelope(output.text());
60094
+ if (result === undefined) {
60095
+ throw new Error(`Pool member ${member.id} did not return one JSON result envelope.`);
60096
+ }
60097
+ return { exitCode: exitCode2, result };
60098
+ });
60099
+ }
57622
60100
  function requiresTargetLease(options) {
57623
60101
  if (options.dryRun)
57624
60102
  return false;
@@ -57694,7 +60172,7 @@ function capabilities(options, values2, io, target, fallbackFormat) {
57694
60172
  function failureResult(command, problems, dependencies) {
57695
60173
  const clock = dependencies.clock ?? (() => new Date);
57696
60174
  const now = clock();
57697
- const commandId = (dependencies.idFactory ?? randomUUID14)();
60175
+ const commandId = (dependencies.idFactory ?? randomUUID17)();
57698
60176
  return {
57699
60177
  schemaVersion: SCHEMA_VERSION,
57700
60178
  command,
@@ -57704,9 +60182,9 @@ function failureResult(command, problems, dependencies) {
57704
60182
  finishedAt: now.toISOString(),
57705
60183
  durationMs: 0,
57706
60184
  data: null,
57707
- problems: problems.map((problem2) => ({
57708
- ...problem2,
57709
- correlation: { ...problem2.correlation, commandId }
60185
+ problems: problems.map((problem4) => ({
60186
+ ...problem4,
60187
+ correlation: { ...problem4.correlation, commandId }
57710
60188
  }))
57711
60189
  };
57712
60190
  }
@@ -57880,6 +60358,8 @@ async function runInteractiveSession(io, dependencies, terminal2, signal) {
57880
60358
  pair: ["pair"],
57881
60359
  "run-help": ["help", "run"],
57882
60360
  sessions: ["sessions"],
60361
+ "test-firebase": ["test", "firebase", "devices"],
60362
+ "test-gradle": ["test", "gradle"],
57883
60363
  version: ["version"]
57884
60364
  };
57885
60365
  clearInteractiveScreen(io.error, terminal2);
@@ -57909,10 +60389,10 @@ async function selectDevice(execution2, io, terminal2, remembered, signal) {
57909
60389
  }
57910
60390
  const selectable = data.targets.filter((target) => target.transports.some(({ stable, state }) => stable && state === "device"));
57911
60391
  if (selectable.length === 0) {
57912
- const problem2 = inputProblem(ProblemCode.NoSelectableTarget, "No ready Android target can be selected.", "Connect or recover a target until ADB reports the device state.", execution2.result.commandId);
60392
+ const problem4 = inputProblem(ProblemCode.NoSelectableTarget, "No ready Android target can be selected.", "Connect or recover a target until ADB reports the device state.", execution2.result.commandId);
57913
60393
  return {
57914
60394
  exitCode: 20 /* Target */,
57915
- result: { ...execution2.result, ok: false, problems: [...execution2.result.problems, problem2] }
60395
+ result: { ...execution2.result, ok: false, problems: [...execution2.result.problems, problem4] }
57916
60396
  };
57917
60397
  }
57918
60398
  const selection2 = await selectOne({
@@ -58014,10 +60494,14 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58014
60494
  });
58015
60495
  return 0 /* Success */;
58016
60496
  }
60497
+ if (options.command === "completion") {
60498
+ io.output.write(generateCompletion(options.completionShell ?? "bash"));
60499
+ return 0 /* Success */;
60500
+ }
58017
60501
  if (options.command === "mcp") {
58018
60502
  const explicitRoot = io.env.ADB_READY_MCP_PROJECT_ROOT?.trim();
58019
60503
  await runMcpStdio({
58020
- cwd: explicitRoot === undefined || explicitRoot === "" ? io.cwd : path24.resolve(explicitRoot),
60504
+ cwd: explicitRoot === undefined || explicitRoot === "" ? io.cwd : path26.resolve(explicitRoot),
58021
60505
  env: io.env,
58022
60506
  dependencies,
58023
60507
  version: VERSION,
@@ -58060,6 +60544,119 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58060
60544
  });
58061
60545
  return execution3.exitCode;
58062
60546
  }
60547
+ if (options.command === "test") {
60548
+ if (options.testKind === "firebase" && options.firebaseAction !== undefined) {
60549
+ const firebaseAction = options.firebaseAction;
60550
+ const executeFirebase = async (devices, memberSignal, resultsDirSuffix) => await runFirebaseTestLab({
60551
+ cwd: io.cwd,
60552
+ action: firebaseAction,
60553
+ ...options.gcloudPath === undefined ? {} : { gcloudPath: options.gcloudPath },
60554
+ ...options.cloudProject === undefined ? {} : { project: options.cloudProject },
60555
+ ...options.firebaseApp === undefined ? {} : { app: options.firebaseApp },
60556
+ ...options.firebaseTest === undefined ? {} : { test: options.firebaseTest },
60557
+ ...devices === undefined ? {} : { devices },
60558
+ ...options.firebaseMatrixId === undefined ? {} : { matrixId: options.firebaseMatrixId },
60559
+ ...options.firebaseResultsBucket === undefined ? {} : { resultsBucket: options.firebaseResultsBucket },
60560
+ ...options.firebaseResultsDir === undefined ? {} : {
60561
+ resultsDir: resultsDirSuffix === undefined ? options.firebaseResultsDir : `${options.firebaseResultsDir}/${resultsDirSuffix}`
60562
+ },
60563
+ ...options.firebaseTestTimeout === undefined ? {} : { testTimeout: options.firebaseTestTimeout },
60564
+ ...options.runTimeoutMs === undefined ? {} : { observationTimeoutMs: options.runTimeoutMs },
60565
+ ...options.firebaseAllowDeprecated === true ? { allowDeprecated: true } : {},
60566
+ ...options.firebaseAllowReducedStability === true ? { allowReducedStability: true } : {},
60567
+ ...options.firebaseAllowLowCapacity === true ? { allowLowCapacity: true } : {},
60568
+ ...options.dryRun ? { dryRun: true } : {}
60569
+ }, {
60570
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60571
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60572
+ ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
60573
+ ...dependencies.firebaseFetch === undefined ? {} : { fetch: dependencies.firebaseFetch }
60574
+ }, memberSignal);
60575
+ let execution4;
60576
+ if (options.poolName === undefined) {
60577
+ execution4 = await executeFirebase(options.firebaseDevices, signal);
60578
+ } else {
60579
+ const loadedPool = await (dependencies.loadConfig ?? loadConfig)({
60580
+ cwd: io.cwd,
60581
+ env: io.env,
60582
+ ...options.configPath === undefined ? {} : { projectConfigPath: options.configPath },
60583
+ explicitProjectConfig: options.configPath !== undefined,
60584
+ ...options.profileName === undefined ? {} : { profileName: options.profileName },
60585
+ cli: cliConfig(options)
60586
+ });
60587
+ if (!loadedPool.ok) {
60588
+ const failed2 = failureResult("test firebase pool", configProblems(loadedPool.errors), dependencies);
60589
+ renderFailure(failed2, options.format, io);
60590
+ return 2 /* InvalidInput */;
60591
+ }
60592
+ const pool = loadedPool.config.values.targetPools?.[options.poolName];
60593
+ if (pool === undefined) {
60594
+ const failed2 = failureResult("test firebase pool", [
60595
+ targetPoolProblem("TARGET_POOL_NOT_FOUND", `Target pool ${options.poolName} was not found.`, "Define the named pool under targets.pools in the project configuration.")
60596
+ ], dependencies);
60597
+ renderFailure(failed2, options.format, io);
60598
+ return 2 /* InvalidInput */;
60599
+ }
60600
+ if (pool.members.some((member) => member.kind !== "firebase")) {
60601
+ const failed2 = failureResult("test firebase pool", [
60602
+ targetPoolProblem("TARGET_POOL_KIND_MISMATCH", `Target pool ${options.poolName} contains a non-Firebase member.`, "Firebase pool runs require every member to declare kind firebase.")
60603
+ ], dependencies);
60604
+ renderFailure(failed2, options.format, io);
60605
+ return 2 /* InvalidInput */;
60606
+ }
60607
+ execution4 = await runTargetPool({
60608
+ name: options.poolName,
60609
+ command: "test firebase pool",
60610
+ pool,
60611
+ coordination: "provider-managed",
60612
+ cancelRunningOnFailFast: false,
60613
+ ...options.maxConcurrency === undefined ? {} : { maxConcurrency: options.maxConcurrency },
60614
+ ...options.failFast === undefined ? {} : { failFast: options.failFast },
60615
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60616
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60617
+ ...signal === undefined ? {} : { signal },
60618
+ ...targetPoolProgress(options, io)
60619
+ }, async (member, memberSignal) => {
60620
+ const firebase = member;
60621
+ return await executeFirebase([
60622
+ {
60623
+ model: firebase.model,
60624
+ version: firebase.version,
60625
+ locale: firebase.locale,
60626
+ orientation: firebase.orientation
60627
+ }
60628
+ ], memberSignal, firebase.id);
60629
+ });
60630
+ }
60631
+ renderResult(execution4.result, {
60632
+ format: options.format,
60633
+ capabilities: capabilities(options, cliConfig(options), io, "output", options.format),
60634
+ sink: options.format === "human" ? io.error : io.output,
60635
+ verbose: options.verbose
60636
+ });
60637
+ return execution4.exitCode;
60638
+ }
60639
+ const execution3 = await runGradleManagedTest({
60640
+ cwd: io.cwd,
60641
+ ...options.gradleTask === undefined ? {} : { task: options.gradleTask },
60642
+ ...options.gradlePath === undefined ? {} : { gradlePath: options.gradlePath },
60643
+ ...options.gradleShards === undefined ? {} : { shards: options.gradleShards },
60644
+ ...options.gradleSoftwareRendering === true ? { softwareRendering: true } : {},
60645
+ ...options.runTimeoutMs === undefined ? {} : { timeoutMs: options.runTimeoutMs },
60646
+ ...options.dryRun ? { dryRun: true } : {}
60647
+ }, {
60648
+ ...dependencies.clock === undefined ? {} : { clock: dependencies.clock },
60649
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory },
60650
+ ...dependencies.runner === undefined ? {} : { runner: dependencies.runner }
60651
+ }, signal);
60652
+ renderResult(execution3.result, {
60653
+ format: options.format,
60654
+ capabilities: capabilities(options, cliConfig(options), io, "output", options.format),
60655
+ sink: options.format === "human" ? io.error : io.output,
60656
+ verbose: options.verbose
60657
+ });
60658
+ return execution3.exitCode;
60659
+ }
58063
60660
  if (options.command === "context" || options.command === "sessions" || options.command === "problems") {
58064
60661
  const storeOptions = dependencies.sessionStore === false ? { env: io.env, projectRoot: io.cwd, allProjects: options.allProjects } : {
58065
60662
  ...dependencies.sessionStore ?? { env: io.env },
@@ -58107,6 +60704,31 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58107
60704
  return execution3.exitCode;
58108
60705
  }
58109
60706
  const values2 = loaded.config.values;
60707
+ if (options.command === "run" && options.poolName !== undefined) {
60708
+ const pool = values2.targetPools?.[options.poolName];
60709
+ if (pool === undefined) {
60710
+ const failed2 = failureResult("run pool", [
60711
+ targetPoolProblem("TARGET_POOL_NOT_FOUND", `Target pool ${options.poolName} was not found.`, "Define the named pool under targets.pools in the project configuration.")
60712
+ ], dependencies);
60713
+ renderFailure(failed2, options.format, io);
60714
+ return 2 /* InvalidInput */;
60715
+ }
60716
+ if (pool.members.some((member) => member.kind === "firebase")) {
60717
+ const failed2 = failureResult("run pool", [
60718
+ targetPoolProblem("TARGET_POOL_KIND_MISMATCH", `Target pool ${options.poolName} contains a Firebase member.`, "Local run pools accept adb, avd, and remote-adb members only. Use test firebase with a Firebase-only pool.")
60719
+ ], dependencies);
60720
+ renderFailure(failed2, options.format, io);
60721
+ return 2 /* InvalidInput */;
60722
+ }
60723
+ const execution3 = await runLocalTargetPool(argv, options, pool, io, dependencies, signal);
60724
+ renderResult(execution3.result, {
60725
+ format: options.format,
60726
+ capabilities: capabilities(options, values2, io, "output", options.format),
60727
+ sink: options.format === "human" ? io.error : io.output,
60728
+ verbose: options.verbose
60729
+ });
60730
+ return execution3.exitCode;
60731
+ }
58110
60732
  const errorCapabilities = capabilities(options, values2, io, "error", options.format);
58111
60733
  const outputCapabilities = capabilities(options, values2, io, "output", options.format);
58112
60734
  let lastTarget;
@@ -58118,7 +60740,7 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58118
60740
  ...values2.adbPort === undefined ? {} : { adbPort: values2.adbPort }
58119
60741
  });
58120
60742
  if (!state.ok) {
58121
- const problem2 = {
60743
+ const problem4 = {
58122
60744
  code: state.code,
58123
60745
  category: options.remembered ? "environment.state" : "state.persistence",
58124
60746
  severity: options.remembered ? "error" : "warning",
@@ -58130,11 +60752,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58130
60752
  correlation: { commandId: "state" }
58131
60753
  };
58132
60754
  if (options.remembered) {
58133
- const failure5 = failureResult(options.command, [problem2], dependencies);
60755
+ const failure5 = failureResult(options.command, [problem4], dependencies);
58134
60756
  renderFailure(failure5, options.format, io);
58135
60757
  return 10 /* Environment */;
58136
60758
  }
58137
- stateWarning = problem2;
60759
+ stateWarning = problem4;
58138
60760
  }
58139
60761
  if (state.ok) {
58140
60762
  lastTarget = rememberedTarget(state.document, {
@@ -58162,10 +60784,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58162
60784
  };
58163
60785
  let androidCapabilities;
58164
60786
  let preparedAvd;
60787
+ let avdResourceLease;
58165
60788
  let autonomousEvidence;
58166
60789
  let autonomousAdb;
58167
60790
  const autonomousRun = options.command === "run" && (options.avdName !== undefined || options.deployArtifact === true);
58168
- const runPreparationCommandId = autonomousRun ? (dependencies.idFactory ?? randomUUID14)() : "run-preparation";
60791
+ const runPreparationCommandId = autonomousRun ? (dependencies.idFactory ?? randomUUID17)() : "run-preparation";
58169
60792
  const finishAutonomousFailure = async (problems, requestedExitCode) => {
58170
60793
  let exitCode2 = requestedExitCode;
58171
60794
  if (preparedAvd !== undefined) {
@@ -58178,6 +60801,8 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58178
60801
  }
58179
60802
  preparedAvd = undefined;
58180
60803
  }
60804
+ await avdResourceLease?.release();
60805
+ avdResourceLease = undefined;
58181
60806
  const failed2 = {
58182
60807
  result: { ...failureResult("run", problems, dependencies), data: null },
58183
60808
  exitCode: exitCode2
@@ -58226,6 +60851,19 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58226
60851
  });
58227
60852
  }
58228
60853
  if (options.avdName !== undefined && !options.dryRun) {
60854
+ if (dependencies.targetLease !== false) {
60855
+ const acquired = await acquireTargetLease({
60856
+ targetIdentity: `avd:${options.avdName}`,
60857
+ projectRoot: io.cwd,
60858
+ purpose: `run avd ${options.avdName}`
60859
+ }, { env: io.env, ...dependencies.targetLease ?? {} });
60860
+ if (!acquired.ok) {
60861
+ return await finishAutonomousFailure([
60862
+ targetPoolProblem(acquired.code, acquired.message, acquired.code === "TARGET_LEASE_CANCELLED" ? "The queued AVD was not started or changed." : "Wait for the owning workflow to finish or choose another AVD.", "target-pool", true)
60863
+ ], acquired.code === "TARGET_LEASE_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */);
60864
+ }
60865
+ avdResourceLease = acquired.lease;
60866
+ }
58229
60867
  const emulator = capability(androidCapabilities, "emulator");
58230
60868
  if (emulator.status !== "supported" || emulator.invocation === undefined) {
58231
60869
  return await finishAutonomousFailure([
@@ -58256,16 +60894,27 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58256
60894
  return await finishAutonomousFailure([autonomousProblem(preparation.failure, "target.avd")], preparation.failure.code === "AVD_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */);
58257
60895
  }
58258
60896
  const avd = preparation.avd;
58259
- preparedAvd = avd;
60897
+ const releaseAvd = avd.release.bind(avd);
60898
+ preparedAvd = {
60899
+ ...avd,
60900
+ release: async (releaseSignal) => {
60901
+ try {
60902
+ return await releaseAvd(releaseSignal);
60903
+ } finally {
60904
+ await avdResourceLease?.release();
60905
+ avdResourceLease = undefined;
60906
+ }
60907
+ }
60908
+ };
58260
60909
  autonomousEvidence = {
58261
60910
  avd: {
58262
- name: avd.name,
58263
- serial: avd.serial,
58264
- ownership: avd.ownership,
58265
- readiness: avd.readiness
60911
+ name: preparedAvd.name,
60912
+ serial: preparedAvd.serial,
60913
+ ownership: preparedAvd.ownership,
60914
+ readiness: preparedAvd.readiness
58266
60915
  }
58267
60916
  };
58268
- config4.targetSelector = avd.serial;
60917
+ config4.targetSelector = preparedAvd.serial;
58269
60918
  delete config4.targetTransportId;
58270
60919
  delete config4.rememberedSerial;
58271
60920
  delete config4.rememberedHardwareSerial;
@@ -58438,11 +61087,11 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58438
61087
  if (!acquired.ok) {
58439
61088
  const failure5 = failureResult(options.command, [
58440
61089
  {
58441
- code: acquired.code === "TARGET_BUSY" ? ProblemCode.TargetBusy : ProblemCode.TargetLeaseUnavailable,
61090
+ code: acquired.code === "TARGET_BUSY" ? ProblemCode.TargetBusy : acquired.code === "TARGET_LEASE_CANCELLED" ? ProblemCode.OperationInterrupted : ProblemCode.TargetLeaseUnavailable,
58442
61091
  category: "target.lease",
58443
61092
  severity: "error",
58444
61093
  summary: acquired.message,
58445
- detail: acquired.code === "TARGET_BUSY" ? "Wait for the owning workflow to finish, or select another Android target. Expired leases are recovered automatically." : "Check the per-user state directory permissions, then retry.",
61094
+ detail: acquired.code === "TARGET_BUSY" ? "Wait for the owning workflow to finish, or select another Android target. Expired leases are recovered automatically." : acquired.code === "TARGET_LEASE_CANCELLED" ? "The queued target was not mutated. Re-run the pool when the target is available." : "Check the per-user state directory permissions, then retry.",
58446
61095
  retryable: true,
58447
61096
  evidence: acquired.owner === undefined ? [] : [
58448
61097
  { source: "target.lease", field: "purpose", value: acquired.owner.purpose },
@@ -58461,13 +61110,14 @@ async function runCliInternal(argv, io, dependencies = {}, signal) {
58461
61110
  correlation: { commandId: "target-lease", targetId: preparedSelection.target.id }
58462
61111
  }
58463
61112
  ], dependencies);
61113
+ const leaseExitCode = acquired.code === "TARGET_LEASE_CANCELLED" ? 130 /* Interrupted */ : 20 /* Target */;
58464
61114
  if (autonomousRun) {
58465
- return await finishAutonomousFailure([...failure5.problems], 20 /* Target */);
61115
+ return await finishAutonomousFailure([...failure5.problems], leaseExitCode);
58466
61116
  }
58467
61117
  await preparedAvd?.release(signal);
58468
61118
  preparedAvd = undefined;
58469
61119
  renderFailure(failure5, options.format, io);
58470
- return 20 /* Target */;
61120
+ return leaseExitCode;
58471
61121
  }
58472
61122
  targetLease = acquired.lease;
58473
61123
  }
@@ -58939,7 +61589,7 @@ async function runCli(argv, io, dependencies = {}, signal) {
58939
61589
  return await runCliInternal(argv, io, dependencies, signal);
58940
61590
  } catch (caught) {
58941
61591
  const message = redactText(caught instanceof Error ? caught.message : String(caught)).value;
58942
- const problem2 = {
61592
+ const problem4 = {
58943
61593
  code: "INTERNAL_ERROR",
58944
61594
  category: "internal.unexpected",
58945
61595
  severity: "error",
@@ -58950,7 +61600,7 @@ async function runCli(argv, io, dependencies = {}, signal) {
58950
61600
  actions: [],
58951
61601
  correlation: { commandId: "cli" }
58952
61602
  };
58953
- renderFailure(failureResult("cli", [problem2], dependencies), inferredFormat(argv), io);
61603
+ renderFailure(failureResult("cli", [problem4], dependencies), inferredFormat(argv), io);
58954
61604
  return 70 /* Internal */;
58955
61605
  }
58956
61606
  }
@@ -58978,4 +61628,4 @@ try {
58978
61628
  process16.removeListener("SIGTERM", abort);
58979
61629
  }
58980
61630
 
58981
- //# debugId=7127D338B7D94B7264756E2164756E21
61631
+ //# debugId=2AD0AEC355B9989464756E2164756E21