adb-ready 0.0.1-alpha.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 ADDED
@@ -0,0 +1,2632 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import process6 from "node:process";
5
+
6
+ // src/cli/main.ts
7
+ import { randomUUID as randomUUID3 } from "node:crypto";
8
+ import process5 from "node:process";
9
+ // package.json
10
+ var package_default = {
11
+ name: "adb-ready",
12
+ version: "0.0.1-alpha.0",
13
+ description: "Make an Android target ready, then keep the development session working.",
14
+ type: "module",
15
+ bin: {
16
+ "adb-ready": "dist/cli.js",
17
+ adbr: "dist/cli.js"
18
+ },
19
+ files: [
20
+ "dist",
21
+ "schema",
22
+ "README.md"
23
+ ],
24
+ engines: {
25
+ node: ">=22"
26
+ },
27
+ packageManager: "bun@1.3.11",
28
+ scripts: {
29
+ dev: "bun run src/cli.ts",
30
+ build: "bun run scripts/build.mjs",
31
+ typecheck: "tsc --noEmit",
32
+ lint: "biome check .",
33
+ format: "biome check --write .",
34
+ "ui:playground": "bun run scripts/ui-playground.ts",
35
+ "ui:check": "bun run scripts/ui-playground.ts --all --non-interactive",
36
+ test: "bun test",
37
+ "test:commands": "bun run build && bun run scripts/command-matrix.mjs",
38
+ "test:integration": "bun test tests/integration",
39
+ smoke: "bun run build && bun run scripts/runtime-smoke.mjs",
40
+ "pack:check": "bun run build && bun run scripts/package-check.mjs",
41
+ "privacy:check": "bun run scripts/privacy-check.mjs",
42
+ verify: "bun run scripts/verify.mjs",
43
+ "verify:real-adb": "bun run build && bun run scripts/real-adb-check.mjs",
44
+ check: "bun run verify",
45
+ prepack: "bun run build"
46
+ },
47
+ repository: {
48
+ type: "git",
49
+ url: "git+https://github.com/Adam014/adb-ready.git"
50
+ },
51
+ bugs: {
52
+ url: "https://github.com/Adam014/adb-ready/issues"
53
+ },
54
+ homepage: "https://github.com/Adam014/adb-ready#readme",
55
+ keywords: [
56
+ "adb",
57
+ "android",
58
+ "cli",
59
+ "developer-tools",
60
+ "expo",
61
+ "react-native",
62
+ "wireless-debugging"
63
+ ],
64
+ license: "UNLICENSED",
65
+ publishConfig: {
66
+ access: "public"
67
+ },
68
+ devDependencies: {
69
+ "@arethetypeswrong/cli": "0.18.5",
70
+ "@biomejs/biome": "2.5.12",
71
+ "@types/bun": "1.4.2",
72
+ "@types/node": "22.20.2",
73
+ publint: "0.3.24",
74
+ typescript: "7.0.2"
75
+ }
76
+ };
77
+
78
+ // src/app/commands.ts
79
+ import { randomUUID as randomUUID2 } from "node:crypto";
80
+
81
+ // src/adb/client.ts
82
+ import { randomUUID } from "node:crypto";
83
+
84
+ // src/platform/process-runner.ts
85
+ import { spawn } from "node:child_process";
86
+ import process from "node:process";
87
+ var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
88
+ function createBoundedCapture(maxBytes) {
89
+ const chunks = [];
90
+ let size = 0;
91
+ let truncated = false;
92
+ return {
93
+ append(chunk) {
94
+ if (size >= maxBytes) {
95
+ truncated = true;
96
+ return;
97
+ }
98
+ const remaining = maxBytes - size;
99
+ const accepted = chunk.byteLength <= remaining ? chunk : chunk.subarray(0, remaining);
100
+ chunks.push(accepted);
101
+ size += accepted.byteLength;
102
+ if (accepted.byteLength !== chunk.byteLength) {
103
+ truncated = true;
104
+ }
105
+ },
106
+ text() {
107
+ const merged = new Uint8Array(size);
108
+ let offset = 0;
109
+ for (const chunk of chunks) {
110
+ merged.set(chunk, offset);
111
+ offset += chunk.byteLength;
112
+ }
113
+ return new TextDecoder("utf-8", { fatal: false }).decode(merged);
114
+ },
115
+ get truncated() {
116
+ return truncated;
117
+ }
118
+ };
119
+ }
120
+ async function runProcess(request) {
121
+ const args = [...request.args ?? []];
122
+ const started = new Date;
123
+ const startedAt = started.toISOString();
124
+ const maxBufferBytes = request.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
125
+ if (!Number.isSafeInteger(maxBufferBytes) || maxBufferBytes < 0) {
126
+ throw new RangeError("maxBufferBytes must be a non-negative safe integer");
127
+ }
128
+ if (request.timeoutMs !== undefined && (!Number.isSafeInteger(request.timeoutMs) || request.timeoutMs < 0)) {
129
+ throw new RangeError("timeoutMs must be a non-negative safe integer");
130
+ }
131
+ const stdout = createBoundedCapture(maxBufferBytes);
132
+ const stderr = createBoundedCapture(maxBufferBytes);
133
+ const stdio = request.stdio ?? "capture";
134
+ let timedOut = false;
135
+ let aborted = request.signal?.aborted ?? false;
136
+ let spawnError;
137
+ return await new Promise((resolve) => {
138
+ const child = spawn(request.executable, args, {
139
+ cwd: request.cwd,
140
+ env: { ...process.env, ...request.env },
141
+ shell: false,
142
+ stdio: stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
143
+ windowsHide: true
144
+ });
145
+ if (stdio === "capture") {
146
+ child.stdout?.on("data", (chunk) => stdout.append(chunk));
147
+ child.stderr?.on("data", (chunk) => stderr.append(chunk));
148
+ }
149
+ const killSignal = request.killSignal ?? "SIGTERM";
150
+ const abort = () => {
151
+ aborted = true;
152
+ if (child.exitCode === null && child.signalCode === null) {
153
+ child.kill(killSignal);
154
+ }
155
+ };
156
+ request.signal?.addEventListener("abort", abort, { once: true });
157
+ if (request.signal?.aborted) {
158
+ abort();
159
+ }
160
+ const timeout = request.timeoutMs === undefined ? undefined : setTimeout(() => {
161
+ timedOut = true;
162
+ if (child.exitCode === null && child.signalCode === null) {
163
+ child.kill(killSignal);
164
+ }
165
+ }, request.timeoutMs);
166
+ child.once("error", (error) => {
167
+ spawnError = {
168
+ ...error.code === undefined ? {} : { code: error.code },
169
+ message: error.message
170
+ };
171
+ });
172
+ child.once("close", (exitCode, signal) => {
173
+ if (timeout !== undefined) {
174
+ clearTimeout(timeout);
175
+ }
176
+ request.signal?.removeEventListener("abort", abort);
177
+ const finished = new Date;
178
+ resolve({
179
+ executable: request.executable,
180
+ args,
181
+ startedAt,
182
+ finishedAt: finished.toISOString(),
183
+ durationMs: Math.max(0, finished.getTime() - started.getTime()),
184
+ exitCode: spawnError === undefined ? exitCode : null,
185
+ signal: spawnError === undefined ? signal : null,
186
+ stdout: stdout.text(),
187
+ stderr: stderr.text(),
188
+ stdoutTruncated: stdout.truncated,
189
+ stderrTruncated: stderr.truncated,
190
+ timedOut,
191
+ aborted,
192
+ ...spawnError === undefined ? {} : { spawnError }
193
+ });
194
+ });
195
+ });
196
+ }
197
+
198
+ // src/adb/parsers.ts
199
+ var KNOWN_STATES = new Set([
200
+ "bootloader",
201
+ "device",
202
+ "offline",
203
+ "recovery",
204
+ "sideload",
205
+ "unauthorized"
206
+ ]);
207
+ function normalizeState(tokens) {
208
+ if (tokens[0] === "no" && tokens[1] === "permissions") {
209
+ return { state: "no-permissions", consumed: 2 };
210
+ }
211
+ const candidate = tokens[0];
212
+ if (candidate !== undefined && KNOWN_STATES.has(candidate)) {
213
+ return { state: candidate, consumed: 1 };
214
+ }
215
+ return { state: "unknown", consumed: candidate === undefined ? 0 : 1 };
216
+ }
217
+ function parseAdbDevices(output) {
218
+ const devices = [];
219
+ let sawHeader = false;
220
+ for (const rawLine of output.replaceAll(`\r
221
+ `, `
222
+ `).split(`
223
+ `)) {
224
+ const line = rawLine.trim();
225
+ if (line === "") {
226
+ continue;
227
+ }
228
+ if (line.startsWith("List of devices attached")) {
229
+ sawHeader = true;
230
+ continue;
231
+ }
232
+ if (!sawHeader || line.startsWith("* daemon") || line.startsWith("adb server")) {
233
+ continue;
234
+ }
235
+ const tokens = line.split(/\s+/u);
236
+ const serial = tokens.shift();
237
+ if (serial === undefined || serial === "") {
238
+ continue;
239
+ }
240
+ const { state, consumed } = normalizeState(tokens);
241
+ const details = tokens.slice(consumed);
242
+ const properties = {};
243
+ const unparsed = [];
244
+ for (const token of details) {
245
+ const separator = token.indexOf(":");
246
+ if (separator <= 0) {
247
+ unparsed.push(token);
248
+ continue;
249
+ }
250
+ properties[token.slice(0, separator)] = token.slice(separator + 1);
251
+ }
252
+ devices.push({
253
+ serial,
254
+ state,
255
+ ...properties.product === undefined ? {} : { product: properties.product },
256
+ ...properties.model === undefined ? {} : { model: properties.model.replaceAll("_", " ") },
257
+ ...properties.device === undefined ? {} : { device: properties.device },
258
+ ...properties.transport_id === undefined ? {} : { transportId: properties.transport_id },
259
+ ...properties.usb === undefined ? {} : { usb: properties.usb },
260
+ properties,
261
+ unparsed
262
+ });
263
+ }
264
+ return devices;
265
+ }
266
+ function parseAdbVersion(output) {
267
+ const normalized = output.replaceAll(`\r
268
+ `, `
269
+ `).trim();
270
+ const protocolVersion = normalized.match(/Android Debug Bridge version\s+([^\s]+)/iu)?.[1];
271
+ const platformToolsVersion = normalized.match(/^Version\s+([^\s]+)/imu)?.[1];
272
+ const installedAs = normalized.match(/^Installed as\s+(.+)$/imu)?.[1]?.trim();
273
+ return {
274
+ ...protocolVersion === undefined ? {} : { protocolVersion },
275
+ ...platformToolsVersion === undefined ? {} : { platformToolsVersion },
276
+ ...installedAs === undefined ? {} : { installedAs },
277
+ raw: normalized
278
+ };
279
+ }
280
+ function parseFeatureList(output) {
281
+ return [
282
+ ...new Set(output.trim().split(/[\s,]+/u).map((feature) => feature.trim()).filter(Boolean))
283
+ ].sort();
284
+ }
285
+ function parseKeyValueLines(output) {
286
+ const values = {};
287
+ for (const rawLine of output.replaceAll(`\r
288
+ `, `
289
+ `).split(`
290
+ `)) {
291
+ const separator = rawLine.indexOf(":");
292
+ if (separator <= 0) {
293
+ continue;
294
+ }
295
+ const key = rawLine.slice(0, separator).trim();
296
+ const value = rawLine.slice(separator + 1).trim();
297
+ if (key !== "") {
298
+ values[key] = value;
299
+ }
300
+ }
301
+ return values;
302
+ }
303
+
304
+ // src/adb/client.ts
305
+ function processMetadata(result) {
306
+ return {
307
+ durationMs: result.durationMs,
308
+ exitCode: result.exitCode,
309
+ signal: result.signal,
310
+ timedOut: result.timedOut,
311
+ aborted: result.aborted,
312
+ stdoutTruncated: result.stdoutTruncated,
313
+ stderrTruncated: result.stderrTruncated,
314
+ ...result.spawnError === undefined ? {} : {
315
+ spawnError: {
316
+ ...result.spawnError.code === undefined ? {} : { code: result.spawnError.code },
317
+ message: result.spawnError.message
318
+ }
319
+ }
320
+ };
321
+ }
322
+
323
+ class AdbClient {
324
+ #options;
325
+ #runner;
326
+ #idFactory;
327
+ constructor(options) {
328
+ this.#options = options;
329
+ this.#runner = options.runner ?? runProcess;
330
+ this.#idFactory = options.idFactory ?? randomUUID;
331
+ }
332
+ async version(signal) {
333
+ return await this.#observe("version", "Checking ADB version", ["version"], parseAdbVersion, signal, false);
334
+ }
335
+ async hostFeatures(signal) {
336
+ return await this.#observe("host-features", "Checking ADB host features", ["host-features"], parseFeatureList, signal);
337
+ }
338
+ async serverStatus(signal) {
339
+ return await this.#observe("server-status", "Checking ADB server status", ["server-status"], parseKeyValueLines, signal);
340
+ }
341
+ async devices(signal) {
342
+ return await this.#observe("devices", "Discovering Android targets", ["devices", "-l"], parseAdbDevices, signal);
343
+ }
344
+ #serverArguments() {
345
+ return [
346
+ ...this.#options.host === undefined ? [] : ["-H", this.#options.host],
347
+ ...this.#options.port === undefined ? [] : ["-P", String(this.#options.port)]
348
+ ];
349
+ }
350
+ async#observe(operation, message, args, parse, signal, useServerArguments = true) {
351
+ const operationId = this.#idFactory();
352
+ const correlation = { commandId: this.#options.correlation.commandId, operationId };
353
+ const finalArgs = [...useServerArguments ? this.#serverArguments() : [], ...args];
354
+ this.#options.bus.emit({
355
+ type: "operation.started",
356
+ source: `adb.${operation}`,
357
+ severity: "info",
358
+ message,
359
+ correlation,
360
+ data: { executable: this.#options.executable, args: finalArgs }
361
+ });
362
+ const request = {
363
+ executable: this.#options.executable,
364
+ args: finalArgs,
365
+ ...this.#options.timeoutMs === undefined ? {} : { timeoutMs: this.#options.timeoutMs },
366
+ ...signal === undefined ? {} : { signal }
367
+ };
368
+ const result = await this.#runner(request);
369
+ const succeeded = result.spawnError === undefined && result.exitCode === 0 && !result.timedOut && !result.aborted;
370
+ this.#options.bus.emit({
371
+ type: succeeded ? "operation.completed" : "operation.failed",
372
+ source: `adb.${operation}`,
373
+ severity: succeeded ? "info" : "error",
374
+ message: succeeded ? `${message} completed` : `${message} failed`,
375
+ correlation,
376
+ data: processMetadata(result)
377
+ });
378
+ return {
379
+ operationId,
380
+ process: result,
381
+ value: parse(result.stdout)
382
+ };
383
+ }
384
+ }
385
+
386
+ // src/domain/contracts.ts
387
+ var SCHEMA_VERSION = 1;
388
+
389
+ // src/core/event-bus.ts
390
+ class EventBus {
391
+ #clock;
392
+ #listeners = new Set;
393
+ #sequence = 0;
394
+ constructor(clock = () => new Date) {
395
+ this.#clock = clock;
396
+ }
397
+ subscribe(listener) {
398
+ this.#listeners.add(listener);
399
+ return () => this.#listeners.delete(listener);
400
+ }
401
+ emit(input) {
402
+ const event = {
403
+ schemaVersion: SCHEMA_VERSION,
404
+ sequence: ++this.#sequence,
405
+ timestamp: this.#clock().toISOString(),
406
+ type: input.type,
407
+ source: input.source,
408
+ severity: input.severity,
409
+ message: input.message,
410
+ correlation: input.correlation,
411
+ ...input.data === undefined ? {} : { data: input.data }
412
+ };
413
+ for (const listener of this.#listeners) {
414
+ listener(event);
415
+ }
416
+ return event;
417
+ }
418
+ }
419
+
420
+ // src/core/redaction.ts
421
+ import { homedir } from "node:os";
422
+ var REDACTED = "[REDACTED]";
423
+ function escapeRegExp(value) {
424
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
425
+ }
426
+ function replaceAndCount(input, expression, replacement) {
427
+ let replacements = 0;
428
+ const value = input.replace(expression, (...args) => {
429
+ replacements += 1;
430
+ return typeof replacement === "string" ? replacement : replacement(...args);
431
+ });
432
+ return { value, replacements };
433
+ }
434
+ function redactText(input, options = {}) {
435
+ let value = input;
436
+ let replacements = 0;
437
+ const patterns = [
438
+ {
439
+ expression: /\b((?:authorization\s*[:=]\s*)?Bearer)\s+[^\s,;]+/giu,
440
+ replacement: (_match, prefix) => `${prefix} ${REDACTED}`
441
+ },
442
+ {
443
+ expression: /\b(api[_-]?key|access[_-]?token|auth|password|passwd|secret)\b(\s*[:=]\s*)(["']?)[^\s,"';]+\3/giu,
444
+ replacement: (_match, name, separator) => `${name}${separator}${REDACTED}`
445
+ },
446
+ {
447
+ expression: /\b(authorization)(\s*[:=]\s*)(?!Bearer\b)(["']?)[^\s,"';]+\3/giu,
448
+ replacement: (_match, name, separator) => `${name}${separator}${REDACTED}`
449
+ },
450
+ {
451
+ expression: /\b(pair(?:ing)?(?:\s+code)?)(\s*[:=]\s*)\d{6}\b/giu,
452
+ replacement: (_match, name, separator) => `${name}${separator}${REDACTED}`
453
+ },
454
+ {
455
+ expression: /([?&](?:access_token|api_key|apikey|auth|password|secret|token)=)[^&#\s]*/giu,
456
+ replacement: (_match, prefix) => `${prefix}${REDACTED}`
457
+ }
458
+ ];
459
+ for (const pattern of patterns) {
460
+ const result = replaceAndCount(value, pattern.expression, pattern.replacement);
461
+ value = result.value;
462
+ replacements += result.replacements;
463
+ }
464
+ const homeDirectory = options.homeDirectory ?? homedir();
465
+ if (homeDirectory.length > 1) {
466
+ const result = replaceAndCount(value, new RegExp(escapeRegExp(homeDirectory), "gu"), "~");
467
+ value = result.value;
468
+ replacements += result.replacements;
469
+ }
470
+ for (const literal of options.additionalLiterals ?? []) {
471
+ if (literal === "") {
472
+ continue;
473
+ }
474
+ const result = replaceAndCount(value, new RegExp(escapeRegExp(literal), "gu"), REDACTED);
475
+ value = result.value;
476
+ replacements += result.replacements;
477
+ }
478
+ return { value, replacements };
479
+ }
480
+
481
+ // src/domain/problems.ts
482
+ var ProblemCode = {
483
+ AdbCommandFailed: "ADB_COMMAND_FAILED",
484
+ AdbNotFound: "ADB_NOT_FOUND",
485
+ AdbOptionalProbeFailed: "ADB_OPTIONAL_PROBE_FAILED",
486
+ AdbServerUnavailable: "ADB_SERVER_UNAVAILABLE",
487
+ AdbTimeout: "ADB_TIMEOUT",
488
+ InteractiveSelectionUnavailable: "INTERACTIVE_SELECTION_UNAVAILABLE",
489
+ MultipleTargets: "MULTIPLE_TARGETS",
490
+ NoSelectableTarget: "NO_SELECTABLE_TARGET",
491
+ NoTargets: "NO_TARGETS",
492
+ OperationInterrupted: "OPERATION_INTERRUPTED",
493
+ TargetSelectionCancelled: "TARGET_SELECTION_CANCELLED",
494
+ TargetNoPermissions: "TARGET_NO_PERMISSIONS",
495
+ TargetOffline: "TARGET_OFFLINE",
496
+ TargetUnauthorized: "TARGET_UNAUTHORIZED",
497
+ TargetUnknownState: "TARGET_UNKNOWN_STATE"
498
+ };
499
+ function retryDevicesAction() {
500
+ return {
501
+ id: "retry_device_probe",
502
+ title: "Check visible devices again",
503
+ kind: "command",
504
+ risk: "read-only",
505
+ automatic: true,
506
+ idempotent: true,
507
+ command: { executable: "adb", args: ["devices", "-l"] }
508
+ };
509
+ }
510
+ function adbNotFoundProblem(correlation, requestedPath) {
511
+ return {
512
+ code: ProblemCode.AdbNotFound,
513
+ category: "environment.executable",
514
+ severity: "error",
515
+ summary: "ADB was not found.",
516
+ detail: requestedPath === undefined ? "Install Android SDK Platform-Tools or provide an explicit ADB path." : `The configured ADB executable could not be used: ${requestedPath}`,
517
+ retryable: true,
518
+ evidence: requestedPath === undefined ? [] : [{ source: "configuration", field: "adb.path", value: requestedPath }],
519
+ actions: [
520
+ {
521
+ id: "configure_adb_path",
522
+ title: "Provide the ADB executable with --adb PATH",
523
+ kind: "user",
524
+ risk: "none",
525
+ automatic: false
526
+ }
527
+ ],
528
+ correlation
529
+ };
530
+ }
531
+ function processEvidence(result) {
532
+ const redacted = redactText(result.stderr.trim());
533
+ return [
534
+ { source: "process", field: "exitCode", value: result.exitCode },
535
+ { source: "process", field: "signal", value: result.signal },
536
+ { source: "process", field: "timedOut", value: result.timedOut },
537
+ ...redacted.value === "" ? [] : [
538
+ {
539
+ source: "process",
540
+ field: "stderr",
541
+ value: redacted.value.slice(0, 2000),
542
+ redacted: redacted.replacements > 0
543
+ }
544
+ ]
545
+ ];
546
+ }
547
+ function adbProcessProblem(operation, result, correlation) {
548
+ if (result.spawnError?.code === "ENOENT") {
549
+ return adbNotFoundProblem(correlation, result.executable);
550
+ }
551
+ if (result.aborted) {
552
+ return {
553
+ code: ProblemCode.OperationInterrupted,
554
+ category: "process.interrupted",
555
+ severity: "error",
556
+ summary: `ADB ${operation} was interrupted.`,
557
+ detail: "The operation stopped after receiving an interruption request.",
558
+ retryable: true,
559
+ evidence: processEvidence(result),
560
+ actions: [],
561
+ correlation
562
+ };
563
+ }
564
+ if (result.timedOut) {
565
+ return {
566
+ code: ProblemCode.AdbTimeout,
567
+ category: "adb.timeout",
568
+ severity: "error",
569
+ summary: `ADB ${operation} timed out.`,
570
+ detail: "The operation did not finish within the configured timeout.",
571
+ retryable: true,
572
+ evidence: processEvidence(result),
573
+ actions: [retryDevicesAction()],
574
+ correlation
575
+ };
576
+ }
577
+ const daemonUnavailable = /cannot connect to daemon|failed to start daemon|server.*failed/iu.test(result.stderr);
578
+ return {
579
+ code: daemonUnavailable ? ProblemCode.AdbServerUnavailable : ProblemCode.AdbCommandFailed,
580
+ category: daemonUnavailable ? "adb.server" : "adb.operation",
581
+ severity: "error",
582
+ summary: daemonUnavailable ? "The ADB server is unavailable." : `ADB ${operation} failed.`,
583
+ detail: daemonUnavailable ? "ADB Ready could not communicate with the configured ADB server." : "ADB returned an unsuccessful result for this operation.",
584
+ retryable: true,
585
+ evidence: processEvidence(result),
586
+ actions: [retryDevicesAction()],
587
+ correlation
588
+ };
589
+ }
590
+ function adbOptionalProbeProblem(operation, result, correlation) {
591
+ const failure = adbProcessProblem(operation, result, correlation);
592
+ return {
593
+ ...failure,
594
+ code: ProblemCode.AdbOptionalProbeFailed,
595
+ category: "adb.capability",
596
+ severity: "warning",
597
+ summary: `Optional ADB ${operation} probe failed.`,
598
+ detail: "Core diagnostics can continue, but this ADB capability could not be inspected reliably."
599
+ };
600
+ }
601
+ function targetEvidence(device) {
602
+ return [
603
+ { source: "adb.devices", field: "serial", value: device.serial },
604
+ { source: "adb.devices", field: "state", value: device.state }
605
+ ];
606
+ }
607
+ function problemsForDevices(devices, correlation, severity = "error") {
608
+ const problems = [];
609
+ for (const device of devices) {
610
+ if (device.state === "unauthorized") {
611
+ problems.push({
612
+ code: ProblemCode.TargetUnauthorized,
613
+ category: "target.authorization",
614
+ severity,
615
+ summary: "An Android target has not authorized this computer.",
616
+ detail: "Accept the RSA authorization prompt on the target, then retry the probe.",
617
+ retryable: true,
618
+ evidence: targetEvidence(device),
619
+ actions: [
620
+ {
621
+ id: "accept_device_prompt",
622
+ title: "Accept the RSA prompt on the Android target",
623
+ kind: "user",
624
+ risk: "none",
625
+ automatic: false
626
+ },
627
+ retryDevicesAction()
628
+ ],
629
+ correlation
630
+ });
631
+ } else if (device.state === "offline") {
632
+ problems.push({
633
+ code: ProblemCode.TargetOffline,
634
+ category: "target.transport",
635
+ severity,
636
+ summary: "An Android target is offline.",
637
+ detail: "ADB knows this transport, but it cannot currently communicate with the target.",
638
+ retryable: true,
639
+ evidence: targetEvidence(device),
640
+ actions: [retryDevicesAction()],
641
+ correlation
642
+ });
643
+ } else if (device.state === "no-permissions") {
644
+ problems.push({
645
+ code: ProblemCode.TargetNoPermissions,
646
+ category: "target.permissions",
647
+ severity,
648
+ summary: "The host does not have permission to use an Android target.",
649
+ detail: "Check the host USB permissions and Android developer authorization setup.",
650
+ retryable: true,
651
+ evidence: targetEvidence(device),
652
+ actions: [
653
+ {
654
+ id: "review_usb_permissions",
655
+ title: "Review host USB permission setup",
656
+ kind: "documentation",
657
+ risk: "none",
658
+ automatic: false
659
+ }
660
+ ],
661
+ correlation
662
+ });
663
+ } else if (device.state === "unknown") {
664
+ problems.push({
665
+ code: ProblemCode.TargetUnknownState,
666
+ category: "target.state",
667
+ severity: "warning",
668
+ summary: "ADB reported an unknown target state.",
669
+ detail: "The target remains visible, but ADB Ready will not assume it is usable.",
670
+ retryable: true,
671
+ evidence: targetEvidence(device),
672
+ actions: [retryDevicesAction()],
673
+ correlation
674
+ });
675
+ }
676
+ }
677
+ return problems;
678
+ }
679
+ function noTargetsProblem(correlation) {
680
+ return {
681
+ code: ProblemCode.NoTargets,
682
+ category: "target.selection",
683
+ severity: "warning",
684
+ summary: "No Android targets are visible.",
685
+ detail: "Connect a target over USB, start an emulator, or enable Wireless debugging.",
686
+ retryable: true,
687
+ evidence: [{ source: "adb.devices", field: "count", value: 0 }],
688
+ actions: [retryDevicesAction()],
689
+ correlation
690
+ };
691
+ }
692
+
693
+ // src/platform/executable.ts
694
+ import { constants } from "node:fs";
695
+ import { access, stat } from "node:fs/promises";
696
+ import { homedir as homedir2 } from "node:os";
697
+ import path from "node:path";
698
+ import process2 from "node:process";
699
+ async function isExecutable(candidate, platform) {
700
+ try {
701
+ await access(candidate, platform === "win32" ? constants.F_OK : constants.X_OK);
702
+ return (await stat(candidate)).isFile();
703
+ } catch {
704
+ return false;
705
+ }
706
+ }
707
+ function windowsExtensions(env) {
708
+ const value = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
709
+ return value.split(";").map((extension) => extension.trim().toLowerCase()).filter(Boolean);
710
+ }
711
+ function candidateNames(name, platform, env) {
712
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
713
+ if (platform !== "win32" || pathApi.extname(name) !== "") {
714
+ return [name];
715
+ }
716
+ return [name, ...windowsExtensions(env).map((extension) => `${name}${extension}`)];
717
+ }
718
+ async function resolveFromPath(name, env, platform) {
719
+ const pathValue = env.PATH ?? env.Path ?? env.path;
720
+ if (pathValue === undefined) {
721
+ return;
722
+ }
723
+ const delimiter = platform === "win32" ? ";" : ":";
724
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
725
+ for (const directory of pathValue.split(delimiter)) {
726
+ if (directory.trim() === "") {
727
+ continue;
728
+ }
729
+ for (const candidateName of candidateNames(name, platform, env)) {
730
+ const candidate = pathApi.resolve(directory.replace(/^"|"$/g, ""), candidateName);
731
+ if (await isExecutable(candidate, platform)) {
732
+ return candidate;
733
+ }
734
+ }
735
+ }
736
+ return;
737
+ }
738
+ function sdkCandidates(env, platform, homeDirectory) {
739
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
740
+ const executable = platform === "win32" ? "adb.exe" : "adb";
741
+ const roots = [env.ANDROID_SDK_ROOT, env.ANDROID_HOME].filter((value) => value !== undefined && value.trim() !== "");
742
+ if (platform === "darwin") {
743
+ roots.push(pathApi.join(homeDirectory, "Library", "Android", "sdk"));
744
+ } else if (platform === "win32") {
745
+ if (env.LOCALAPPDATA !== undefined) {
746
+ roots.push(pathApi.join(env.LOCALAPPDATA, "Android", "Sdk"));
747
+ }
748
+ } else {
749
+ roots.push(pathApi.join(homeDirectory, "Android", "Sdk"));
750
+ }
751
+ return [...new Set(roots.map((root) => pathApi.join(root, "platform-tools", executable)))];
752
+ }
753
+ async function locateAdb(options = {}) {
754
+ const env = options.env ?? process2.env;
755
+ const platform = options.platform ?? process2.platform;
756
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
757
+ if (options.explicitPath !== undefined) {
758
+ const explicit = options.explicitPath.trim();
759
+ if (explicit === "") {
760
+ return;
761
+ }
762
+ if (pathApi.isAbsolute(explicit) || explicit.includes("/") || explicit.includes("\\")) {
763
+ const candidate = pathApi.resolve(explicit);
764
+ return await isExecutable(candidate, platform) ? candidate : undefined;
765
+ }
766
+ return await resolveFromPath(explicit, env, platform);
767
+ }
768
+ const fromPath = await resolveFromPath("adb", env, platform);
769
+ if (fromPath !== undefined) {
770
+ return fromPath;
771
+ }
772
+ for (const candidate of sdkCandidates(env, platform, options.homeDirectory ?? homedir2())) {
773
+ if (await isExecutable(candidate, platform)) {
774
+ return candidate;
775
+ }
776
+ }
777
+ return;
778
+ }
779
+
780
+ // src/platform/runtime.ts
781
+ import process3 from "node:process";
782
+ function detectRuntime() {
783
+ const versions = process3.versions;
784
+ if (versions.bun !== undefined) {
785
+ return {
786
+ name: "bun",
787
+ version: versions.bun,
788
+ ...versions.node === undefined ? {} : { nodeCompatibilityVersion: versions.node },
789
+ platform: process3.platform,
790
+ architecture: process3.arch
791
+ };
792
+ }
793
+ if (versions.deno !== undefined) {
794
+ return {
795
+ name: "deno",
796
+ version: versions.deno,
797
+ ...versions.node === undefined ? {} : { nodeCompatibilityVersion: versions.node },
798
+ platform: process3.platform,
799
+ architecture: process3.arch
800
+ };
801
+ }
802
+ if (versions.node !== undefined) {
803
+ return {
804
+ name: "node",
805
+ version: versions.node,
806
+ platform: process3.platform,
807
+ architecture: process3.arch
808
+ };
809
+ }
810
+ return {
811
+ name: "unknown",
812
+ version: "unknown",
813
+ platform: process3.platform,
814
+ architecture: process3.arch
815
+ };
816
+ }
817
+
818
+ // src/app/commands.ts
819
+ function processSucceeded(result) {
820
+ return result.spawnError === undefined && result.exitCode === 0 && !result.timedOut && !result.aborted;
821
+ }
822
+ function exitCodeForProblems(problems) {
823
+ const errors = problems.filter(({ severity }) => severity === "error");
824
+ if (errors.length === 0) {
825
+ return 0 /* Success */;
826
+ }
827
+ if (errors.some(({ code }) => code === ProblemCode.OperationInterrupted)) {
828
+ return 130 /* Interrupted */;
829
+ }
830
+ if (errors.some(({ category }) => category.startsWith("environment."))) {
831
+ return 10 /* Environment */;
832
+ }
833
+ if (errors.some(({ category }) => category.startsWith("target."))) {
834
+ return 20 /* Target */;
835
+ }
836
+ return 30 /* AdbOperation */;
837
+ }
838
+ function createContext(command, dependencies) {
839
+ const clock = dependencies.clock ?? (() => new Date);
840
+ const commandId = (dependencies.idFactory ?? randomUUID2)();
841
+ const bus = dependencies.bus ?? new EventBus(clock);
842
+ const started = clock();
843
+ bus.emit({
844
+ type: "command.started",
845
+ source: `command.${command}`,
846
+ severity: "info",
847
+ message: `Running ${command}`,
848
+ correlation: { commandId }
849
+ });
850
+ return { bus, clock, command, commandId, started };
851
+ }
852
+ function finish(context, data, problems) {
853
+ const finished = context.clock();
854
+ const exitCode = exitCodeForProblems(problems);
855
+ const ok = exitCode === 0 /* Success */;
856
+ const result = {
857
+ schemaVersion: SCHEMA_VERSION,
858
+ command: context.command,
859
+ commandId: context.commandId,
860
+ ok,
861
+ startedAt: context.started.toISOString(),
862
+ finishedAt: finished.toISOString(),
863
+ durationMs: Math.max(0, finished.getTime() - context.started.getTime()),
864
+ data,
865
+ problems
866
+ };
867
+ context.bus.emit({
868
+ type: ok ? "command.completed" : "command.failed",
869
+ source: `command.${context.command}`,
870
+ severity: ok ? "info" : "error",
871
+ message: ok ? `${context.command} completed` : `${context.command} failed`,
872
+ correlation: { commandId: context.commandId },
873
+ data: { exitCode, problemCount: problems.length }
874
+ });
875
+ return { result, exitCode };
876
+ }
877
+ function redactedPath(value) {
878
+ return redactText(value).value;
879
+ }
880
+ function redactedRecord(values) {
881
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, redactText(value).value]));
882
+ }
883
+ async function resolveAdb(context, config, dependencies) {
884
+ const operationId = (dependencies.idFactory ?? randomUUID2)();
885
+ const correlation = { commandId: context.commandId, operationId };
886
+ context.bus.emit({
887
+ type: "operation.started",
888
+ source: "host.adb-path",
889
+ severity: "info",
890
+ message: "Locating ADB",
891
+ correlation
892
+ });
893
+ const executable = await (dependencies.locateAdb ?? locateAdb)({
894
+ ...config.adbPath === undefined ? {} : { explicitPath: config.adbPath }
895
+ });
896
+ context.bus.emit({
897
+ type: executable === undefined ? "operation.failed" : "operation.completed",
898
+ source: "host.adb-path",
899
+ severity: executable === undefined ? "error" : "info",
900
+ message: executable === undefined ? "ADB was not found" : "ADB was found",
901
+ correlation,
902
+ ...executable === undefined ? {} : { data: { path: redactedPath(executable) } }
903
+ });
904
+ return executable;
905
+ }
906
+ function createClient(executable, context, config, dependencies) {
907
+ return new AdbClient({
908
+ executable,
909
+ bus: context.bus,
910
+ correlation: { commandId: context.commandId },
911
+ ...config.adbHost === undefined ? {} : { host: config.adbHost },
912
+ ...config.adbPort === undefined ? {} : { port: config.adbPort },
913
+ ...config.timeoutMs === undefined ? {} : { timeoutMs: config.timeoutMs },
914
+ ...dependencies.runner === undefined ? {} : { runner: dependencies.runner },
915
+ ...dependencies.idFactory === undefined ? {} : { idFactory: dependencies.idFactory }
916
+ });
917
+ }
918
+ function operationProblem(operation, observation, commandId) {
919
+ return adbProcessProblem(operation, observation.process, {
920
+ commandId,
921
+ operationId: observation.operationId
922
+ });
923
+ }
924
+ async function runDoctor(config = {}, dependencies = {}, signal) {
925
+ const context = createContext("doctor", dependencies);
926
+ const problems = [];
927
+ const executable = await resolveAdb(context, config, dependencies);
928
+ if (executable === undefined) {
929
+ problems.push(adbNotFoundProblem({ commandId: context.commandId }, config.adbPath));
930
+ return finish(context, null, problems);
931
+ }
932
+ const client = createClient(executable, context, config, dependencies);
933
+ const version = await client.version(signal);
934
+ if (!processSucceeded(version.process)) {
935
+ problems.push(operationProblem("version", version, context.commandId));
936
+ return finish(context, null, problems);
937
+ }
938
+ const hostFeatures = await client.hostFeatures(signal);
939
+ if (!processSucceeded(hostFeatures.process)) {
940
+ const problem = operationProblem("host-features", hostFeatures, context.commandId);
941
+ if (problem.code === ProblemCode.OperationInterrupted) {
942
+ problems.push(problem);
943
+ return finish(context, null, problems);
944
+ }
945
+ problems.push(adbOptionalProbeProblem("host-features", hostFeatures.process, problem.correlation));
946
+ }
947
+ let serverStatus = null;
948
+ if (processSucceeded(hostFeatures.process) && hostFeatures.value.includes("server_status")) {
949
+ const status = await client.serverStatus(signal);
950
+ if (processSucceeded(status.process)) {
951
+ serverStatus = redactedRecord(status.value);
952
+ } else {
953
+ const problem = operationProblem("server-status", status, context.commandId);
954
+ if (problem.code === ProblemCode.OperationInterrupted) {
955
+ problems.push(problem);
956
+ return finish(context, null, problems);
957
+ }
958
+ problems.push(adbOptionalProbeProblem("server-status", status.process, problem.correlation));
959
+ }
960
+ }
961
+ const devices = await client.devices(signal);
962
+ if (!processSucceeded(devices.process)) {
963
+ problems.push(operationProblem("devices", devices, context.commandId));
964
+ return finish(context, null, problems);
965
+ }
966
+ problems.push(...problemsForDevices(devices.value, { commandId: context.commandId }, "warning"));
967
+ if (devices.value.length === 0) {
968
+ problems.push(noTargetsProblem({ commandId: context.commandId }));
969
+ }
970
+ const versionData = {
971
+ ...version.value.protocolVersion === undefined ? {} : { protocolVersion: version.value.protocolVersion },
972
+ ...version.value.platformToolsVersion === undefined ? {} : { platformToolsVersion: version.value.platformToolsVersion },
973
+ ...version.value.installedAs === undefined ? {} : { installedAs: redactedPath(version.value.installedAs) }
974
+ };
975
+ return finish(context, {
976
+ runtime: (dependencies.runtime ?? detectRuntime)(),
977
+ adb: {
978
+ path: redactedPath(executable),
979
+ version: versionData,
980
+ hostFeatures: processSucceeded(hostFeatures.process) ? hostFeatures.value : [],
981
+ serverStatus
982
+ },
983
+ devices: devices.value
984
+ }, problems);
985
+ }
986
+ async function runDevices(config = {}, dependencies = {}, signal) {
987
+ const context = createContext("devices", dependencies);
988
+ const problems = [];
989
+ const executable = await resolveAdb(context, config, dependencies);
990
+ if (executable === undefined) {
991
+ problems.push(adbNotFoundProblem({ commandId: context.commandId }, config.adbPath));
992
+ return finish(context, null, problems);
993
+ }
994
+ const client = createClient(executable, context, config, dependencies);
995
+ const devices = await client.devices(signal);
996
+ if (!processSucceeded(devices.process)) {
997
+ problems.push(operationProblem("devices", devices, context.commandId));
998
+ return finish(context, null, problems);
999
+ }
1000
+ problems.push(...problemsForDevices(devices.value, { commandId: context.commandId }, "warning"));
1001
+ if (devices.value.length === 0) {
1002
+ problems.push(noTargetsProblem({ commandId: context.commandId }));
1003
+ }
1004
+ return finish(context, { adbPath: redactedPath(executable), devices: devices.value }, problems);
1005
+ }
1006
+
1007
+ // src/config/loader.ts
1008
+ import { readFile, stat as stat2 } from "node:fs/promises";
1009
+ import { homedir as homedir3 } from "node:os";
1010
+ import path2 from "node:path";
1011
+ import process4 from "node:process";
1012
+ var DEFAULTS = { timeoutMs: 5000 };
1013
+ var ROOT_KEYS = new Set(["$schema", "version", "adb", "timeoutMs", "output"]);
1014
+ var ADB_KEYS = new Set(["path", "host", "port"]);
1015
+ var OUTPUT_KEYS = new Set(["color", "unicode", "animation", "interactive"]);
1016
+ function isObject(value) {
1017
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1018
+ }
1019
+ function error(source, location, pathValue, message, code = "CONFIG_INVALID_VALUE") {
1020
+ return {
1021
+ code,
1022
+ path: pathValue,
1023
+ message,
1024
+ source,
1025
+ ...location === undefined ? {} : { location }
1026
+ };
1027
+ }
1028
+ function validateUnknownKeys(value, allowed, prefix, source, location, errors) {
1029
+ for (const key of Object.keys(value)) {
1030
+ if (!allowed.has(key)) {
1031
+ errors.push(error(source, location, `${prefix}${key}`, `Unknown configuration key: ${prefix}${key}`));
1032
+ }
1033
+ }
1034
+ }
1035
+ function validateDocument(document, source, location) {
1036
+ const errors = [];
1037
+ const values = {};
1038
+ if (!isObject(document)) {
1039
+ return {
1040
+ values,
1041
+ errors: [error(source, location, "$", "Configuration must be a JSON object.")]
1042
+ };
1043
+ }
1044
+ validateUnknownKeys(document, ROOT_KEYS, "", source, location, errors);
1045
+ if (document.$schema !== undefined && typeof document.$schema !== "string") {
1046
+ errors.push(error(source, location, "$schema", "$schema must be a string."));
1047
+ }
1048
+ if (document.version !== 1) {
1049
+ errors.push(error(source, location, "version", "version must be exactly 1."));
1050
+ }
1051
+ if (document.timeoutMs !== undefined) {
1052
+ if (!Number.isSafeInteger(document.timeoutMs) || document.timeoutMs < 1 || document.timeoutMs > 2147483647) {
1053
+ errors.push(error(source, location, "timeoutMs", "timeoutMs must be an integer from 1 to 2147483647."));
1054
+ } else {
1055
+ values.timeoutMs = document.timeoutMs;
1056
+ }
1057
+ }
1058
+ if (document.adb !== undefined) {
1059
+ if (!isObject(document.adb)) {
1060
+ errors.push(error(source, location, "adb", "adb must be an object."));
1061
+ } else {
1062
+ validateUnknownKeys(document.adb, ADB_KEYS, "adb.", source, location, errors);
1063
+ for (const [key, configKey] of [
1064
+ ["path", "adbPath"],
1065
+ ["host", "adbHost"]
1066
+ ]) {
1067
+ const candidate = document.adb[key];
1068
+ if (candidate !== undefined) {
1069
+ if (typeof candidate !== "string" || candidate.trim() === "") {
1070
+ errors.push(error(source, location, `adb.${key}`, `adb.${key} must be a non-empty string.`));
1071
+ } else {
1072
+ values[configKey] = candidate;
1073
+ }
1074
+ }
1075
+ }
1076
+ if (document.adb.port !== undefined) {
1077
+ if (!Number.isSafeInteger(document.adb.port) || document.adb.port < 1 || document.adb.port > 65535) {
1078
+ errors.push(error(source, location, "adb.port", "adb.port must be an integer from 1 to 65535."));
1079
+ } else {
1080
+ values.adbPort = document.adb.port;
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ if (document.output !== undefined) {
1086
+ if (!isObject(document.output)) {
1087
+ errors.push(error(source, location, "output", "output must be an object."));
1088
+ } else {
1089
+ validateUnknownKeys(document.output, OUTPUT_KEYS, "output.", source, location, errors);
1090
+ for (const [key, configKey] of [
1091
+ ["color", "color"],
1092
+ ["unicode", "unicode"],
1093
+ ["animation", "animation"],
1094
+ ["interactive", "interactive"]
1095
+ ]) {
1096
+ const candidate = document.output[key];
1097
+ if (candidate !== undefined) {
1098
+ if (typeof candidate !== "boolean") {
1099
+ errors.push(error(source, location, `output.${key}`, `output.${key} must be a boolean.`));
1100
+ } else {
1101
+ values[configKey] = candidate;
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+ return { values, errors };
1108
+ }
1109
+ async function fileExists(file) {
1110
+ try {
1111
+ return (await stat2(file)).isFile();
1112
+ } catch {
1113
+ return false;
1114
+ }
1115
+ }
1116
+ function defaultUserConfigPath(platform, env, homeDirectory) {
1117
+ const platformPath = platform === "win32" ? path2.win32 : path2.posix;
1118
+ if (env.XDG_CONFIG_HOME !== undefined && env.XDG_CONFIG_HOME.trim() !== "") {
1119
+ return platformPath.join(env.XDG_CONFIG_HOME, "adb-ready", "config.json");
1120
+ }
1121
+ if (platform === "win32" && env.APPDATA !== undefined && env.APPDATA.trim() !== "") {
1122
+ return platformPath.join(env.APPDATA, "adb-ready", "config.json");
1123
+ }
1124
+ if (platform === "darwin") {
1125
+ return platformPath.join(homeDirectory, "Library", "Application Support", "adb-ready", "config.json");
1126
+ }
1127
+ return platformPath.join(homeDirectory, ".config", "adb-ready", "config.json");
1128
+ }
1129
+ async function findProjectConfig(startDirectory) {
1130
+ let current = path2.resolve(startDirectory);
1131
+ while (true) {
1132
+ const candidate = path2.join(current, "adb-ready.config.json");
1133
+ if (await fileExists(candidate)) {
1134
+ return candidate;
1135
+ }
1136
+ const parent = path2.dirname(current);
1137
+ if (parent === current) {
1138
+ return;
1139
+ }
1140
+ current = parent;
1141
+ }
1142
+ }
1143
+ async function readConfigDocument(file, source, required) {
1144
+ let raw;
1145
+ try {
1146
+ raw = await readFile(file, "utf8");
1147
+ } catch (caught) {
1148
+ const code = caught.code;
1149
+ if (!required && code === "ENOENT") {
1150
+ return { values: {}, errors: [], loaded: false };
1151
+ }
1152
+ return {
1153
+ values: {},
1154
+ errors: [
1155
+ error(source, file, "$", `Configuration file could not be read: ${file}`, "CONFIG_NOT_FOUND")
1156
+ ],
1157
+ loaded: false
1158
+ };
1159
+ }
1160
+ let document;
1161
+ try {
1162
+ document = JSON.parse(raw);
1163
+ } catch (caught) {
1164
+ return {
1165
+ values: {},
1166
+ errors: [
1167
+ error(source, file, "$", `Invalid JSON: ${caught instanceof Error ? caught.message : "unknown parse error"}`, "CONFIG_INVALID_JSON")
1168
+ ],
1169
+ loaded: true
1170
+ };
1171
+ }
1172
+ const validated = validateDocument(document, source, file);
1173
+ return { ...validated, loaded: true };
1174
+ }
1175
+ function parseEnvironment(env) {
1176
+ const values = {};
1177
+ const errors = [];
1178
+ const strings = [
1179
+ ["ADB_READY_ADB_PATH", "adbPath"],
1180
+ ["ADB_READY_ADB_HOST", "adbHost"]
1181
+ ];
1182
+ for (const [name, key] of strings) {
1183
+ const candidate = env[name];
1184
+ if (candidate !== undefined) {
1185
+ if (candidate.trim() === "") {
1186
+ errors.push(error("environment", name, name, `${name} cannot be empty.`));
1187
+ } else {
1188
+ values[key] = candidate;
1189
+ }
1190
+ }
1191
+ }
1192
+ const integers = [
1193
+ ["ADB_READY_ADB_PORT", "adbPort", 65535],
1194
+ ["ADB_READY_TIMEOUT_MS", "timeoutMs", 2147483647]
1195
+ ];
1196
+ for (const [name, key, maximum] of integers) {
1197
+ const candidate = env[name];
1198
+ if (candidate !== undefined) {
1199
+ const parsed = Number(candidate);
1200
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > maximum) {
1201
+ errors.push(error("environment", name, name, `${name} must be an integer from 1 to ${maximum}.`));
1202
+ } else {
1203
+ values[key] = parsed;
1204
+ }
1205
+ }
1206
+ }
1207
+ const booleans = [
1208
+ ["ADB_READY_COLOR", "color"],
1209
+ ["ADB_READY_UNICODE", "unicode"],
1210
+ ["ADB_READY_ANIMATION", "animation"],
1211
+ ["ADB_READY_INTERACTIVE", "interactive"]
1212
+ ];
1213
+ for (const [name, key] of booleans) {
1214
+ const candidate = env[name];
1215
+ if (candidate !== undefined) {
1216
+ const normalized = candidate.trim().toLowerCase();
1217
+ if (["1", "true", "yes", "on"].includes(normalized)) {
1218
+ values[key] = true;
1219
+ } else if (["0", "false", "no", "off"].includes(normalized)) {
1220
+ values[key] = false;
1221
+ } else {
1222
+ errors.push(error("environment", name, name, `${name} must be true/false, 1/0, yes/no, or on/off.`));
1223
+ }
1224
+ }
1225
+ }
1226
+ return { values, errors };
1227
+ }
1228
+ function applyValues(target, provenance, values, source, location) {
1229
+ for (const key of Object.keys(values)) {
1230
+ const value = values[key];
1231
+ if (value !== undefined) {
1232
+ Object.assign(target, { [key]: value });
1233
+ provenance[key] = { source, ...location === undefined ? {} : { location } };
1234
+ }
1235
+ }
1236
+ }
1237
+ async function loadConfig(options = {}) {
1238
+ const cwd = options.cwd ?? process4.cwd();
1239
+ const env = options.env ?? process4.env;
1240
+ const platform = options.platform ?? process4.platform;
1241
+ const homeDirectory = options.homeDirectory ?? homedir3();
1242
+ const userFile = options.userConfigPath ?? defaultUserConfigPath(platform, env, homeDirectory);
1243
+ const environmentProjectFile = env.ADB_READY_CONFIG?.trim();
1244
+ const projectFile = options.projectConfigPath ?? (environmentProjectFile === undefined || environmentProjectFile === "" ? options.explicitProjectConfig ? undefined : await findProjectConfig(cwd) : path2.resolve(cwd, environmentProjectFile));
1245
+ const projectRequired = (options.explicitProjectConfig ?? false) || environmentProjectFile !== undefined && environmentProjectFile !== "";
1246
+ const errors = [];
1247
+ const values = {};
1248
+ const provenance = {};
1249
+ const files = {};
1250
+ applyValues(values, provenance, DEFAULTS, "default");
1251
+ const user = await readConfigDocument(userFile, "user", false);
1252
+ errors.push(...user.errors);
1253
+ if (user.loaded) {
1254
+ files.user = userFile;
1255
+ applyValues(values, provenance, user.values, "user", userFile);
1256
+ }
1257
+ if (projectFile !== undefined) {
1258
+ const project = await readConfigDocument(projectFile, "project", projectRequired);
1259
+ errors.push(...project.errors);
1260
+ if (project.loaded) {
1261
+ files.project = projectFile;
1262
+ applyValues(values, provenance, project.values, "project", projectFile);
1263
+ }
1264
+ } else if (projectRequired) {
1265
+ errors.push(error("project", undefined, "$", "An explicit project configuration path is required.", "CONFIG_NOT_FOUND"));
1266
+ }
1267
+ const environment = parseEnvironment(env);
1268
+ errors.push(...environment.errors);
1269
+ applyValues(values, provenance, environment.values, "environment");
1270
+ applyValues(values, provenance, options.cli ?? {}, "cli");
1271
+ if (errors.length > 0) {
1272
+ return { ok: false, errors };
1273
+ }
1274
+ return {
1275
+ ok: true,
1276
+ config: {
1277
+ values,
1278
+ provenance,
1279
+ files
1280
+ }
1281
+ };
1282
+ }
1283
+
1284
+ // src/ui/ascii-scene.ts
1285
+ var FACE_SHADES = ".,:;=+*#%@";
1286
+ var LINK_PATH = [
1287
+ { x: 1.34, y: -0.42, z: 0 },
1288
+ { x: 0.72, y: -1.02, z: 0 },
1289
+ { x: -0.7, y: -1.02, z: 0 },
1290
+ { x: -1.34, y: -0.42, z: 0 },
1291
+ { x: -1.34, y: 0.42, z: 0 },
1292
+ { x: -0.7, y: 1.02, z: 0 },
1293
+ { x: 0.72, y: 1.02, z: 0 },
1294
+ { x: 1.34, y: 0.42, z: 0 }
1295
+ ];
1296
+ function rotate(point, angleX, angleY) {
1297
+ const sinX = Math.sin(angleX);
1298
+ const cosX = Math.cos(angleX);
1299
+ const sinY = Math.sin(angleY);
1300
+ const cosY = Math.cos(angleY);
1301
+ const afterX = {
1302
+ x: point.x,
1303
+ y: point.y * cosX - point.z * sinX,
1304
+ z: point.y * sinX + point.z * cosX
1305
+ };
1306
+ return {
1307
+ x: afterX.x * cosY + afterX.z * sinY,
1308
+ y: afterX.y,
1309
+ z: -afterX.x * sinY + afterX.z * cosY
1310
+ };
1311
+ }
1312
+ function orientForLink(point, link, offset = true) {
1313
+ if (link === 0) {
1314
+ return point;
1315
+ }
1316
+ return {
1317
+ x: point.z + (offset ? 0.2 : 0),
1318
+ y: point.y,
1319
+ z: -point.x
1320
+ };
1321
+ }
1322
+ function normalize(point) {
1323
+ const length = Math.hypot(point.x, point.y, point.z) || 1;
1324
+ return { x: point.x / length, y: point.y / length, z: point.z / length };
1325
+ }
1326
+ function mix(origin, along, alongAmount, across, acrossAmount, depthAmount) {
1327
+ return {
1328
+ x: origin.x + along.x * alongAmount + across.x * acrossAmount,
1329
+ y: origin.y + along.y * alongAmount + across.y * acrossAmount,
1330
+ z: origin.z + depthAmount
1331
+ };
1332
+ }
1333
+ function renderLinkCoreFrame(options) {
1334
+ const width = Math.max(10, Math.floor(options.width ?? 36));
1335
+ const height = Math.max(5, Math.floor(options.height ?? 14));
1336
+ const pixels = Array.from({ length: width * height }, () => " ");
1337
+ const depth = new Float64Array(width * height);
1338
+ const camera = 4.8;
1339
+ const focalX = width * 1.18;
1340
+ const focalY = focalX * 0.48;
1341
+ const light = normalize({ x: -0.45, y: 0.7, z: 1 });
1342
+ const plot = (localPoint, localNormal, link, edge = false) => {
1343
+ const point = rotate(orientForLink(localPoint, link), options.angleX, options.angleY);
1344
+ const normal = normalize(rotate(orientForLink(localNormal, link, false), options.angleX, options.angleY));
1345
+ const inverseDepth = 1 / (camera - point.z);
1346
+ const x = Math.round(width / 2 + point.x * focalX * inverseDepth);
1347
+ const y = Math.round(height / 2 - point.y * focalY * inverseDepth);
1348
+ if (x < 0 || x >= width || y < 0 || y >= height) {
1349
+ return;
1350
+ }
1351
+ const index = x + y * width;
1352
+ const biasedDepth = inverseDepth + (edge ? 0.0008 : 0);
1353
+ if (biasedDepth <= (depth[index] ?? 0)) {
1354
+ return;
1355
+ }
1356
+ depth[index] = biasedDepth;
1357
+ const diffuse = Math.max(0, normal.x * light.x + normal.y * light.y + normal.z * light.z);
1358
+ const rim = (1 - Math.abs(normal.z)) ** 2 * 0.22;
1359
+ const luminance = Math.min(1, 0.16 + diffuse * 0.72 + rim + link * 0.04);
1360
+ const shadeIndex = Math.min(FACE_SHADES.length - 1, Math.floor(luminance * FACE_SHADES.length));
1361
+ pixels[index] = edge ? luminance > 0.58 ? "@" : "#" : FACE_SHADES[shadeIndex] ?? ".";
1362
+ };
1363
+ const halfBand = 0.19;
1364
+ const halfDepth = 0.16;
1365
+ const step = 0.045;
1366
+ for (const link of [0, 1]) {
1367
+ for (let segment = 0;segment < LINK_PATH.length - 1; segment += 1) {
1368
+ const from = LINK_PATH[segment];
1369
+ const to = LINK_PATH[segment + 1];
1370
+ if (from === undefined || to === undefined) {
1371
+ continue;
1372
+ }
1373
+ const delta = { x: to.x - from.x, y: to.y - from.y, z: 0 };
1374
+ const length = Math.hypot(delta.x, delta.y);
1375
+ const along = { x: delta.x / length, y: delta.y / length, z: 0 };
1376
+ const across = { x: -along.y, y: along.x, z: 0 };
1377
+ for (let distance = 0;distance <= length; distance += step) {
1378
+ for (let lateral = -halfBand;lateral <= halfBand; lateral += step) {
1379
+ plot(mix(from, along, distance, across, lateral, halfDepth), { x: 0, y: 0, z: 1 }, link);
1380
+ plot(mix(from, along, distance, across, lateral, -halfDepth), { x: 0, y: 0, z: -1 }, link);
1381
+ }
1382
+ for (let extrusion = -halfDepth;extrusion <= halfDepth; extrusion += step) {
1383
+ plot(mix(from, along, distance, across, halfBand, extrusion), across, link);
1384
+ plot(mix(from, along, distance, across, -halfBand, extrusion), { x: -across.x, y: -across.y, z: 0 }, link);
1385
+ }
1386
+ for (const lateral of [-halfBand, halfBand]) {
1387
+ for (const extrusion of [-halfDepth, halfDepth]) {
1388
+ plot(mix(from, along, distance, across, lateral, extrusion), { x: 0, y: 0, z: extrusion > 0 ? 1 : -1 }, link, true);
1389
+ }
1390
+ }
1391
+ }
1392
+ }
1393
+ }
1394
+ return Array.from({ length: height }, (_, row) => pixels.slice(row * width, (row + 1) * width).join("").trimEnd());
1395
+ }
1396
+
1397
+ // src/ui/style.ts
1398
+ var ANSI_PATTERN = /[\x1B\x9B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\\/#&.:=?%@~_]+)*)?\x07)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/gu;
1399
+ var CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/gu;
1400
+ function sanitizeTerminalText(value) {
1401
+ return value.replace(ANSI_PATTERN, "").replaceAll("\r", " ").replaceAll(`
1402
+ `, " ").replaceAll("\t", " ").replace(CONTROL_PATTERN, "").replace(/\s{2,}/gu, " ").trim();
1403
+ }
1404
+ function symbols(capabilities) {
1405
+ return capabilities.unicode ? {
1406
+ active: "◆",
1407
+ failure: "✕",
1408
+ pending: "○",
1409
+ recovered: "↻",
1410
+ skipped: "–",
1411
+ success: "✓",
1412
+ warning: "!",
1413
+ branch: "├─",
1414
+ end: "└─"
1415
+ } : {
1416
+ active: ">",
1417
+ failure: "x",
1418
+ pending: "o",
1419
+ recovered: "~",
1420
+ skipped: "-",
1421
+ success: "+",
1422
+ warning: "!",
1423
+ branch: "|-",
1424
+ end: "`-"
1425
+ };
1426
+ }
1427
+ function color(code, value, capabilities) {
1428
+ return capabilities.color ? `\x1B[${String(code)}m${value}\x1B[0m` : value;
1429
+ }
1430
+ var style = {
1431
+ accent: (value, capabilities) => color(36, value, capabilities),
1432
+ dim: (value, capabilities) => color(2, value, capabilities),
1433
+ failure: (value, capabilities) => color(31, value, capabilities),
1434
+ strong: (value, capabilities) => color(1, value, capabilities),
1435
+ success: (value, capabilities) => color(32, value, capabilities),
1436
+ warning: (value, capabilities) => color(33, value, capabilities)
1437
+ };
1438
+
1439
+ // src/ui/select.ts
1440
+ function truncate(value, width) {
1441
+ if (value.length <= width) {
1442
+ return value;
1443
+ }
1444
+ return width <= 3 ? value.slice(0, width) : `${value.slice(0, width - 3)}...`;
1445
+ }
1446
+ function wrap(value, width) {
1447
+ const clean = sanitizeTerminalText(value);
1448
+ if (clean.length <= width) {
1449
+ return [clean];
1450
+ }
1451
+ const lines = [];
1452
+ let line = "";
1453
+ for (const word of clean.split(" ")) {
1454
+ if (word.length > width) {
1455
+ if (line !== "") {
1456
+ lines.push(line);
1457
+ line = "";
1458
+ }
1459
+ for (let start = 0;start < word.length; start += width) {
1460
+ lines.push(word.slice(start, start + width));
1461
+ }
1462
+ continue;
1463
+ }
1464
+ const candidate = line === "" ? word : `${line} ${word}`;
1465
+ if (candidate.length > width) {
1466
+ lines.push(line);
1467
+ line = word;
1468
+ } else {
1469
+ line = candidate;
1470
+ }
1471
+ }
1472
+ if (line !== "") {
1473
+ lines.push(line);
1474
+ }
1475
+ return lines;
1476
+ }
1477
+ function nextEnabled(options, current, direction) {
1478
+ for (let offset = 1;offset <= options.length; offset += 1) {
1479
+ const candidate = (current + direction * offset + options.length) % options.length;
1480
+ if (options[candidate]?.disabled !== true) {
1481
+ return candidate;
1482
+ }
1483
+ }
1484
+ return current;
1485
+ }
1486
+ function initialIndex(options) {
1487
+ const recommended = options.findIndex((option) => option.recommended === true && option.disabled !== true);
1488
+ if (recommended !== -1) {
1489
+ return recommended;
1490
+ }
1491
+ return options.findIndex((option) => option.disabled !== true);
1492
+ }
1493
+ function renderMenu(options, selected, previousLineCount, frame) {
1494
+ const { capabilities, sink } = options;
1495
+ const glyphs = symbols(capabilities);
1496
+ if (previousLineCount > 0) {
1497
+ sink.write(`\x1B[${String(previousLineCount)}F`);
1498
+ }
1499
+ const top = capabilities.unicode ? "╭─" : "+-";
1500
+ const bottom = capabilities.unicode ? "╰─" : "+-";
1501
+ const rail = capabilities.unicode ? "│" : "|";
1502
+ const compact = capabilities.columns < 60;
1503
+ const help = capabilities.unicode ? "↑↓ move · 1-9 jump · enter open" : "up/down · 1-9 jump · enter open";
1504
+ const lines = [...options.preamble?.(frame) ?? []];
1505
+ lines.push(`${style.dim(top, capabilities)} ${style.strong(truncate(sanitizeTerminalText(options.title), Math.max(8, capabilities.columns - 4)), capabilities)}`);
1506
+ options.options.forEach((option, index) => {
1507
+ const isSelected = index === selected;
1508
+ const pointer = isSelected ? style.accent(glyphs.active, capabilities) : style.dim(option.disabled === true ? glyphs.skipped : glyphs.pending, capabilities);
1509
+ const number = style.dim(`[${String(index + 1)}]`, capabilities);
1510
+ const disabled = option.disabled === true ? " · unavailable" : "";
1511
+ const recommended = option.recommended === true ? " · recommended" : "";
1512
+ const label = truncate(`${sanitizeTerminalText(option.label)}${disabled}${recommended}`, Math.max(8, capabilities.columns - 10));
1513
+ const styledLabel = option.disabled === true ? style.dim(label, capabilities) : isSelected ? style.strong(label, capabilities) : label;
1514
+ lines.push(` ${pointer} ${number} ${styledLabel}`);
1515
+ if (option.description !== undefined) {
1516
+ for (const description of wrap(option.description, Math.max(10, capabilities.columns - 9))) {
1517
+ const descriptionRail = isSelected ? style.accent(rail, capabilities) : " ";
1518
+ lines.push(` ${descriptionRail} ${style.dim(description, capabilities)}`);
1519
+ }
1520
+ }
1521
+ });
1522
+ lines.push(`${style.dim(bottom, capabilities)} ${style.dim(help, capabilities)}`);
1523
+ if (compact) {
1524
+ lines.push(` ${style.dim("esc close", capabilities)}`);
1525
+ } else {
1526
+ lines[lines.length - 1] += style.dim(" · esc close", capabilities);
1527
+ }
1528
+ for (const line of lines) {
1529
+ sink.write(`\x1B[2K${line}
1530
+ `);
1531
+ }
1532
+ return lines.length;
1533
+ }
1534
+ async function selectOne(options) {
1535
+ if (!options.capabilities.interactive || options.input.setRawMode === undefined) {
1536
+ return { kind: "unavailable" };
1537
+ }
1538
+ let selected = initialIndex(options.options);
1539
+ if (selected === -1) {
1540
+ return { kind: "unavailable" };
1541
+ }
1542
+ return await new Promise((resolve) => {
1543
+ const wasRaw = options.input.isRaw === true;
1544
+ let lineCount = 0;
1545
+ let frame = 0;
1546
+ let settled = false;
1547
+ let refreshTimer;
1548
+ const attempt = (operation) => {
1549
+ try {
1550
+ operation();
1551
+ } catch {}
1552
+ };
1553
+ const cleanup = () => {
1554
+ attempt(() => options.input.off("data", onData));
1555
+ attempt(() => options.signal?.removeEventListener("abort", onAbort));
1556
+ if (refreshTimer !== undefined) {
1557
+ clearInterval(refreshTimer);
1558
+ }
1559
+ if (!wasRaw) {
1560
+ attempt(() => options.input.setRawMode?.(false));
1561
+ }
1562
+ attempt(() => options.input.pause());
1563
+ attempt(() => options.sink.write("\x1B[?25h"));
1564
+ };
1565
+ const settle = (result) => {
1566
+ if (settled) {
1567
+ return;
1568
+ }
1569
+ settled = true;
1570
+ cleanup();
1571
+ resolve(result);
1572
+ };
1573
+ const draw = () => {
1574
+ lineCount = renderMenu(options, selected, lineCount, frame);
1575
+ };
1576
+ const onAbort = () => settle({ kind: "cancelled", reason: "signal" });
1577
+ const onData = (chunk) => {
1578
+ const input = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
1579
+ if (input.includes("\x03")) {
1580
+ settle({ kind: "cancelled", reason: "interrupt" });
1581
+ return;
1582
+ }
1583
+ if (input === "\x1B") {
1584
+ settle({ kind: "cancelled", reason: "escape" });
1585
+ return;
1586
+ }
1587
+ if (input.includes("\x1B[A")) {
1588
+ selected = nextEnabled(options.options, selected, -1);
1589
+ draw();
1590
+ return;
1591
+ }
1592
+ if (input.includes("\x1B[B")) {
1593
+ selected = nextEnabled(options.options, selected, 1);
1594
+ draw();
1595
+ return;
1596
+ }
1597
+ const shortcut = input.match(/^[1-9]$/u);
1598
+ if (shortcut !== null) {
1599
+ const index = Number(shortcut[0]) - 1;
1600
+ if (options.options[index] !== undefined && options.options[index]?.disabled !== true) {
1601
+ selected = index;
1602
+ draw();
1603
+ }
1604
+ return;
1605
+ }
1606
+ if (input.includes("\r") || input.includes(`
1607
+ `)) {
1608
+ const selectedOption = options.options[selected];
1609
+ if (selectedOption !== undefined && selectedOption.disabled !== true) {
1610
+ settle({ kind: "selected", value: selectedOption.value });
1611
+ }
1612
+ }
1613
+ };
1614
+ try {
1615
+ options.sink.write("\x1B[?25l");
1616
+ options.input.setRawMode?.(true);
1617
+ options.input.resume();
1618
+ options.input.on("data", onData);
1619
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1620
+ draw();
1621
+ if (options.preamble !== undefined && options.capabilities.animation) {
1622
+ refreshTimer = setInterval(() => {
1623
+ frame += 1;
1624
+ draw();
1625
+ }, Math.max(32, options.refreshIntervalMs ?? 90));
1626
+ }
1627
+ } catch {
1628
+ settle({ kind: "unavailable" });
1629
+ return;
1630
+ }
1631
+ if (options.signal?.aborted) {
1632
+ onAbort();
1633
+ }
1634
+ });
1635
+ }
1636
+
1637
+ // src/ui/home.ts
1638
+ function fit(value, width) {
1639
+ if (value.length <= width) {
1640
+ return value.padEnd(width);
1641
+ }
1642
+ return width <= 3 ? value.slice(0, width) : `${value.slice(0, width - 3)}...`;
1643
+ }
1644
+ function productPanel(version, capabilities) {
1645
+ const unicode = capabilities.unicode;
1646
+ const width = 36;
1647
+ const inner = width - 4;
1648
+ const top = unicode ? `╭─${"─".repeat(width - 4)}╮` : `+-${"-".repeat(width - 4)}+`;
1649
+ const bottom = unicode ? `╰${"─".repeat(width - 2)}╯` : `+${"-".repeat(width - 2)}+`;
1650
+ const side = unicode ? "│" : "|";
1651
+ const rows = [
1652
+ "ADB READY",
1653
+ "Android setup. No guesswork.",
1654
+ "",
1655
+ "CHECK ADB and your local setup",
1656
+ "SEE every visible target",
1657
+ "USE human or JSON output",
1658
+ "",
1659
+ `adb-ready v${version}`
1660
+ ];
1661
+ return [
1662
+ style.dim(top, capabilities),
1663
+ ...rows.map((row, index) => {
1664
+ const content = fit(row, inner);
1665
+ const styled = index === 0 ? style.strong(content, capabilities) : index === 1 ? style.accent(content, capabilities) : style.dim(content, capabilities);
1666
+ return `${style.dim(side, capabilities)} ${styled} ${style.dim(side, capabilities)}`;
1667
+ }),
1668
+ style.dim(bottom, capabilities)
1669
+ ];
1670
+ }
1671
+ function center(value, width) {
1672
+ const padding = Math.max(0, Math.floor((width - value.length) / 2));
1673
+ return `${" ".repeat(padding)}${value}`;
1674
+ }
1675
+ function homePreamble(frame, version, capabilities) {
1676
+ const angleX = 0.38 + Math.sin(frame * 0.035) * 0.12;
1677
+ const angleY = frame * 0.045;
1678
+ if (capabilities.columns < 76) {
1679
+ const width = Math.max(20, Math.min(34, capabilities.columns - 4));
1680
+ const core2 = renderLinkCoreFrame({ angleX, angleY, width, height: 12 });
1681
+ return [
1682
+ ...core2.map((line) => style.accent(center(line, capabilities.columns), capabilities)),
1683
+ "",
1684
+ style.strong(center("ADB READY", capabilities.columns), capabilities),
1685
+ style.dim(center("Android setup. No guesswork.", capabilities.columns), capabilities),
1686
+ style.dim(center(`v${version}`, capabilities.columns), capabilities),
1687
+ ""
1688
+ ];
1689
+ }
1690
+ const coreWidth = 36;
1691
+ const core = renderLinkCoreFrame({ angleX, angleY, width: coreWidth, height: 14 });
1692
+ const panel = productPanel(version, capabilities);
1693
+ const panelOffset = 2;
1694
+ return core.map((line, index) => {
1695
+ const left = style.accent(line.padEnd(coreWidth), capabilities);
1696
+ const right = index >= panelOffset ? panel[index - panelOffset] ?? "" : "";
1697
+ return `${left} ${right}`.trimEnd();
1698
+ }).concat("");
1699
+ }
1700
+ var COMPACT_WORDMARK = [
1701
+ " ## ### ### ### #### ## ### # #",
1702
+ "# # # # # # # # # # # # # # #",
1703
+ "#### # # ### ### ### #### # # ## ",
1704
+ "# # # # # # # # # # # # # # ",
1705
+ "# # ### ### # # #### # # ### # "
1706
+ ];
1707
+ function compactSessionPreamble(capabilities) {
1708
+ const unicode = capabilities.unicode;
1709
+ const width = Math.max(20, Math.min(45, capabilities.columns));
1710
+ const inner = width - 4;
1711
+ const top = unicode ? `╭${"─".repeat(width - 2)}╮` : `+${"-".repeat(width - 2)}+`;
1712
+ const bottom = unicode ? `╰${"─".repeat(width - 2)}╯` : `+${"-".repeat(width - 2)}+`;
1713
+ const side = unicode ? "│" : "|";
1714
+ const wordmark = width >= 45 ? COMPACT_WORDMARK : ["ADB READY"];
1715
+ const rows = wordmark.map((line) => {
1716
+ const content = fit(center(line, inner), inner);
1717
+ return `${style.dim(side, capabilities)} ${style.accent(content, capabilities)} ${style.dim(side, capabilities)}`;
1718
+ });
1719
+ return ["", style.dim(top, capabilities), ...rows, style.dim(bottom, capabilities), ""];
1720
+ }
1721
+ function clearInteractiveScreen(sink, capabilities) {
1722
+ if (capabilities.interactive) {
1723
+ sink.write("\x1B[2J\x1B[H");
1724
+ }
1725
+ }
1726
+ async function showHomeScreen(options) {
1727
+ if (!options.capabilities.interactive) {
1728
+ return { kind: "unavailable" };
1729
+ }
1730
+ const presentation = options.presentation ?? "full";
1731
+ if (presentation === "full") {
1732
+ clearInteractiveScreen(options.sink, options.capabilities);
1733
+ }
1734
+ const selection = await selectOne({
1735
+ title: presentation === "full" ? "WHAT DO YOU WANT TO DO?" : "ACTIONS",
1736
+ options: [
1737
+ {
1738
+ value: "doctor",
1739
+ label: "Check my setup",
1740
+ description: "Validate runtime, ADB, server, and target access",
1741
+ recommended: true
1742
+ },
1743
+ {
1744
+ value: "devices",
1745
+ label: "Show Android targets",
1746
+ description: "See connected devices and running emulators"
1747
+ },
1748
+ {
1749
+ value: "version",
1750
+ label: "Show version",
1751
+ description: `ADB Ready ${options.version}`
1752
+ },
1753
+ {
1754
+ value: "help",
1755
+ label: "View command reference",
1756
+ description: "Explore commands, flags, and automation output"
1757
+ },
1758
+ { value: "exit", label: "Exit", description: "Close ADB Ready" }
1759
+ ],
1760
+ input: options.input,
1761
+ sink: options.sink,
1762
+ capabilities: presentation === "full" ? options.capabilities : { ...options.capabilities, animation: false },
1763
+ preamble: (frame) => presentation === "full" ? homePreamble(frame, options.version, options.capabilities) : compactSessionPreamble(options.capabilities),
1764
+ ...options.refreshIntervalMs === undefined ? {} : { refreshIntervalMs: options.refreshIntervalMs },
1765
+ ...options.signal === undefined ? {} : { signal: options.signal }
1766
+ });
1767
+ if (selection.kind === "unavailable") {
1768
+ return selection;
1769
+ }
1770
+ if (selection.kind === "cancelled") {
1771
+ return selection.reason === "escape" ? { kind: "action", action: "exit" } : { kind: "cancelled", reason: selection.reason };
1772
+ }
1773
+ return { kind: "action", action: selection.value };
1774
+ }
1775
+
1776
+ // src/ui/spinner.ts
1777
+ var scheduler = {
1778
+ setInterval: (callback, milliseconds) => setInterval(callback, milliseconds),
1779
+ clearInterval: (handle) => clearInterval(handle)
1780
+ };
1781
+
1782
+ class Spinner {
1783
+ #sink;
1784
+ #capabilities;
1785
+ #scheduler;
1786
+ #timer;
1787
+ #frame = 0;
1788
+ #message = "";
1789
+ #cursorHidden = false;
1790
+ constructor(sink, capabilities, timerScheduler = scheduler) {
1791
+ this.#sink = sink;
1792
+ this.#capabilities = capabilities;
1793
+ this.#scheduler = timerScheduler;
1794
+ }
1795
+ start(message) {
1796
+ this.stopAnimation();
1797
+ this.#message = sanitizeTerminalText(message);
1798
+ if (!this.#capabilities.animation) {
1799
+ return;
1800
+ }
1801
+ this.hideCursor();
1802
+ this.drawFrame();
1803
+ this.#timer = this.#scheduler.setInterval(() => this.drawFrame(), 80);
1804
+ }
1805
+ succeed(message = this.#message) {
1806
+ this.finish("success", message);
1807
+ }
1808
+ fail(message = this.#message) {
1809
+ this.finish("failure", message);
1810
+ }
1811
+ warn(message = this.#message) {
1812
+ this.finish("warning", message);
1813
+ }
1814
+ dispose() {
1815
+ this.stopAnimation();
1816
+ this.showCursor();
1817
+ }
1818
+ finish(status, message) {
1819
+ this.stopAnimation();
1820
+ if (this.#capabilities.animation) {
1821
+ this.#sink.write("\r\x1B[2K");
1822
+ }
1823
+ const glyphs = symbols(this.#capabilities);
1824
+ const symbol = status === "success" ? style.success(glyphs.success, this.#capabilities) : status === "warning" ? style.warning(glyphs.warning, this.#capabilities) : style.failure(glyphs.failure, this.#capabilities);
1825
+ this.#sink.write(`${symbol} ${sanitizeTerminalText(message)}
1826
+ `);
1827
+ this.showCursor();
1828
+ }
1829
+ drawFrame() {
1830
+ const frames = this.#capabilities.unicode ? ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] : ["-", "\\", "|", "/"];
1831
+ const frame = frames[this.#frame % frames.length] ?? frames[0] ?? ">";
1832
+ this.#frame += 1;
1833
+ this.#sink.write(`\r\x1B[2K${style.accent(frame, this.#capabilities)} ${this.#message}`);
1834
+ }
1835
+ stopAnimation() {
1836
+ if (this.#timer !== undefined) {
1837
+ this.#scheduler.clearInterval(this.#timer);
1838
+ this.#timer = undefined;
1839
+ }
1840
+ }
1841
+ hideCursor() {
1842
+ if (!this.#cursorHidden) {
1843
+ this.#sink.write("\x1B[?25l");
1844
+ this.#cursorHidden = true;
1845
+ }
1846
+ }
1847
+ showCursor() {
1848
+ if (this.#cursorHidden) {
1849
+ this.#sink.write("\x1B[?25h");
1850
+ this.#cursorHidden = false;
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ // src/ui/progress-renderer.ts
1856
+ class ProgressRenderer {
1857
+ #spinner;
1858
+ #unsubscribe;
1859
+ #verbose;
1860
+ #sink;
1861
+ constructor(options) {
1862
+ this.#sink = options.sink;
1863
+ this.#verbose = options.verbose ?? false;
1864
+ this.#spinner = new Spinner(options.sink, options.capabilities, options.scheduler);
1865
+ this.#unsubscribe = options.bus.subscribe((event) => this.onEvent(event));
1866
+ }
1867
+ dispose() {
1868
+ this.#unsubscribe();
1869
+ this.#spinner.dispose();
1870
+ }
1871
+ onEvent(event) {
1872
+ if (event.type === "operation.started") {
1873
+ this.#spinner.start(event.message);
1874
+ } else if (event.type === "operation.completed") {
1875
+ this.#spinner.succeed(event.message.replace(/ completed$/u, ""));
1876
+ } else if (event.type === "operation.failed") {
1877
+ this.#spinner.fail(event.message.replace(/ failed$/u, ""));
1878
+ } else if (this.#verbose) {
1879
+ this.#sink.write(`› ${event.message}
1880
+ `);
1881
+ }
1882
+ }
1883
+ }
1884
+
1885
+ // src/ui/result-renderer.ts
1886
+ function clean(value) {
1887
+ return sanitizeTerminalText(String(value));
1888
+ }
1889
+ function isRecord(value) {
1890
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1891
+ }
1892
+ function isDoctorData(value) {
1893
+ return isRecord(value) && isRecord(value.runtime) && isRecord(value.adb) && Array.isArray(value.devices);
1894
+ }
1895
+ function hasDevices(value) {
1896
+ return isRecord(value) && Array.isArray(value.devices);
1897
+ }
1898
+ function deviceLabel(device) {
1899
+ const identity = device.model ?? device.product ?? device.device;
1900
+ return identity === undefined ? `${clean(device.serial)} · ${clean(device.state)}` : `${clean(identity)} · ${clean(device.serial)} · ${clean(device.state)}`;
1901
+ }
1902
+ function problemLines(problem, capabilities, verbose) {
1903
+ const glyphs = symbols(capabilities);
1904
+ const marker = problem.severity === "error" ? style.failure(glyphs.failure, capabilities) : style.warning(glyphs.warning, capabilities);
1905
+ const lines = [
1906
+ `${marker} ${style.strong(clean(problem.summary), capabilities)}`,
1907
+ ` ${clean(problem.detail)}`
1908
+ ];
1909
+ const action = problem.actions[0];
1910
+ if (action !== undefined) {
1911
+ lines.push(` ${style.accent("Next:", capabilities)} ${clean(action.title)}`);
1912
+ }
1913
+ if (verbose) {
1914
+ for (const evidence of problem.evidence) {
1915
+ const field = evidence.field === undefined ? evidence.source : `${evidence.source}.${evidence.field}`;
1916
+ lines.push(` ${style.dim(`${clean(field)}: ${clean(JSON.stringify(evidence.value))}`, capabilities)}`);
1917
+ }
1918
+ }
1919
+ return lines;
1920
+ }
1921
+ function renderHuman(result, options) {
1922
+ const { capabilities, sink } = options;
1923
+ const glyphs = symbols(capabilities);
1924
+ const lines = [];
1925
+ lines.push(`${style.strong("ADB Ready", capabilities)} ${style.dim(`· ${clean(result.command)}`, capabilities)}`, "");
1926
+ if (result.command === "doctor" && isDoctorData(result.data)) {
1927
+ const data = result.data;
1928
+ const runtime = `${data.runtime.name} ${data.runtime.version}`;
1929
+ const adbVersion = data.adb.version.platformToolsVersion ?? "unknown version";
1930
+ lines.push(`${style.success(glyphs.success, capabilities)} Runtime ${clean(runtime)} · ${clean(data.runtime.platform)}/${clean(data.runtime.architecture)}`, `${style.success(glyphs.success, capabilities)} ADB Platform-Tools ${clean(adbVersion)}`, `${style.success(glyphs.success, capabilities)} Path ${clean(data.adb.path)}`, `${style.success(glyphs.success, capabilities)} Features ${String(data.adb.hostFeatures.length)} detected`, "");
1931
+ }
1932
+ if (hasDevices(result.data)) {
1933
+ const devices = result.data.devices;
1934
+ lines.push(style.strong(`Targets (${String(devices.length)})`, capabilities));
1935
+ if (devices.length === 0) {
1936
+ lines.push(`${style.dim(glyphs.end, capabilities)} None visible`);
1937
+ } else {
1938
+ devices.forEach((device, index) => {
1939
+ const branch = index === devices.length - 1 ? glyphs.end : glyphs.branch;
1940
+ lines.push(`${style.dim(branch, capabilities)} ${deviceLabel(device)}`);
1941
+ });
1942
+ }
1943
+ if ("selected" in result.data && result.data.selected !== undefined) {
1944
+ lines.push(`${style.accent(glyphs.active, capabilities)} Selected ${deviceLabel(result.data.selected)}`);
1945
+ }
1946
+ }
1947
+ if (result.problems.length > 0) {
1948
+ lines.push("", style.strong("Diagnostics", capabilities));
1949
+ for (const problem of result.problems) {
1950
+ lines.push(...problemLines(problem, capabilities, options.verbose ?? false));
1951
+ }
1952
+ }
1953
+ const status = result.ok ? `${style.success(glyphs.success, capabilities)} Completed in ${String(result.durationMs)}ms` : `${style.failure(glyphs.failure, capabilities)} Failed in ${String(result.durationMs)}ms`;
1954
+ lines.push("", status);
1955
+ sink.write(`${lines.join(`
1956
+ `)}
1957
+ `);
1958
+ }
1959
+ function renderPlain(result, sink) {
1960
+ sink.write(`command=${clean(result.command)}
1961
+ `);
1962
+ sink.write(`ok=${String(result.ok)}
1963
+ `);
1964
+ sink.write(`duration_ms=${String(result.durationMs)}
1965
+ `);
1966
+ if (isDoctorData(result.data)) {
1967
+ const data = result.data;
1968
+ sink.write(`runtime=${clean(data.runtime.name)}
1969
+ `);
1970
+ sink.write(`runtime_version=${clean(data.runtime.version)}
1971
+ `);
1972
+ sink.write(`adb_version=${clean(data.adb.version.platformToolsVersion ?? "unknown")}
1973
+ `);
1974
+ }
1975
+ if (hasDevices(result.data)) {
1976
+ sink.write(`device_count=${String(result.data.devices.length)}
1977
+ `);
1978
+ result.data.devices.forEach((device, index) => {
1979
+ sink.write(`device_${String(index)}=${deviceLabel(device)}
1980
+ `);
1981
+ });
1982
+ if ("selected" in result.data && result.data.selected !== undefined) {
1983
+ sink.write(`selected=${deviceLabel(result.data.selected)}
1984
+ `);
1985
+ }
1986
+ }
1987
+ for (const problem of result.problems) {
1988
+ sink.write(`problem=${clean(problem.code)}:${clean(problem.summary)}
1989
+ `);
1990
+ }
1991
+ }
1992
+ function renderResult(result, options) {
1993
+ if (options.format === "json") {
1994
+ options.sink.write(`${JSON.stringify(result)}
1995
+ `);
1996
+ } else if (options.format === "ndjson") {
1997
+ options.sink.write(`${JSON.stringify({ kind: "result", ...result })}
1998
+ `);
1999
+ } else if (options.format === "plain") {
2000
+ renderPlain(result, options.sink);
2001
+ } else {
2002
+ renderHuman(result, options);
2003
+ }
2004
+ }
2005
+
2006
+ class NdjsonEventRenderer {
2007
+ #unsubscribe;
2008
+ constructor(bus, sink) {
2009
+ this.#unsubscribe = bus.subscribe((event) => this.write(event, sink));
2010
+ }
2011
+ dispose() {
2012
+ this.#unsubscribe();
2013
+ }
2014
+ write(event, sink) {
2015
+ sink.write(`${JSON.stringify({ kind: "event", ...event })}
2016
+ `);
2017
+ }
2018
+ }
2019
+
2020
+ // src/ui/terminal.ts
2021
+ function environmentFlag(value) {
2022
+ if (value === undefined) {
2023
+ return false;
2024
+ }
2025
+ return !["", "0", "false", "no", "off"].includes(value.trim().toLowerCase());
2026
+ }
2027
+ function resolveTerminalCapabilities(preferences) {
2028
+ const env = preferences.env ?? {};
2029
+ const human = preferences.format === "human";
2030
+ const dumb = env.TERM?.toLowerCase() === "dumb";
2031
+ const ci = environmentFlag(env.CI);
2032
+ const reducedMotion = environmentFlag(env.ADB_READY_REDUCED_MOTION);
2033
+ const noColorEnvironment = Object.hasOwn(env, "NO_COLOR");
2034
+ const locale = `${env.LC_ALL ?? ""} ${env.LC_CTYPE ?? ""} ${env.LANG ?? ""}`;
2035
+ const localeSupportsUnicode = /UTF-?8/iu.test(locale);
2036
+ const interactive = human && preferences.nonInteractive === false && preferences.interactive !== false && preferences.inputIsTTY && preferences.outputIsTTY && !ci && !dumb;
2037
+ const colorPreference = preferences.color ?? !noColorEnvironment;
2038
+ const unicodePreference = preferences.unicode ?? localeSupportsUnicode;
2039
+ return {
2040
+ interactive,
2041
+ color: human && preferences.outputIsTTY && !dumb && colorPreference,
2042
+ unicode: human && unicodePreference,
2043
+ animation: interactive && preferences.animation !== false && !reducedMotion && !environmentFlag(env.CI),
2044
+ columns: Math.max(20, preferences.columns ?? 80)
2045
+ };
2046
+ }
2047
+
2048
+ // src/cli/arguments.ts
2049
+ var COMMANDS = new Set(["devices", "doctor", "help", "version"]);
2050
+ var BOOLEAN_OPTIONS = new Set([
2051
+ "-h",
2052
+ "--help",
2053
+ "-V",
2054
+ "--version",
2055
+ "--json",
2056
+ "--quiet",
2057
+ "--verbose",
2058
+ "--non-interactive",
2059
+ "--color",
2060
+ "--no-color",
2061
+ "--unicode",
2062
+ "--no-unicode",
2063
+ "--animation",
2064
+ "--no-animation",
2065
+ "--select"
2066
+ ]);
2067
+ function failure(code, message, option) {
2068
+ return { ok: false, code, message, ...option === undefined ? {} : { option } };
2069
+ }
2070
+ function parseDuration(value) {
2071
+ const match = value.match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/u);
2072
+ if (match === null) {
2073
+ return;
2074
+ }
2075
+ const amount = Number(match[1]);
2076
+ const unit = match[2] ?? "ms";
2077
+ const multiplier = unit === "m" ? 60000 : unit === "s" ? 1000 : 1;
2078
+ const milliseconds = amount * multiplier;
2079
+ return Number.isSafeInteger(milliseconds) && milliseconds > 0 ? milliseconds : undefined;
2080
+ }
2081
+ function splitLongOption(argument) {
2082
+ const separator = argument.indexOf("=");
2083
+ if (separator === -1) {
2084
+ return { option: argument };
2085
+ }
2086
+ return { option: argument.slice(0, separator), inlineValue: argument.slice(separator + 1) };
2087
+ }
2088
+ function parseArguments(argv) {
2089
+ let command;
2090
+ let helpTarget;
2091
+ let format = "human";
2092
+ let quiet = false;
2093
+ let verbose = false;
2094
+ let nonInteractive = false;
2095
+ let color2;
2096
+ let unicode;
2097
+ let animation;
2098
+ let timeoutMs;
2099
+ let adbPath;
2100
+ let adbHost;
2101
+ let adbPort;
2102
+ let configPath;
2103
+ let select = false;
2104
+ for (let index = 0;index < argv.length; index += 1) {
2105
+ const argument = argv[index];
2106
+ if (argument === undefined) {
2107
+ continue;
2108
+ }
2109
+ if (!argument.startsWith("-")) {
2110
+ if (!COMMANDS.has(argument)) {
2111
+ return failure("CLI_USAGE", `Unknown command: ${argument}`);
2112
+ }
2113
+ const candidate = argument;
2114
+ if (command === undefined) {
2115
+ command = candidate;
2116
+ continue;
2117
+ }
2118
+ if (command === "help" && (candidate === "doctor" || candidate === "devices")) {
2119
+ helpTarget = candidate;
2120
+ continue;
2121
+ }
2122
+ return failure("CLI_USAGE", `Unexpected argument: ${argument}`);
2123
+ }
2124
+ const { option, inlineValue } = splitLongOption(argument);
2125
+ if (inlineValue !== undefined && BOOLEAN_OPTIONS.has(option)) {
2126
+ return failure("CLI_INVALID_OPTION", `${option} does not accept a value.`, option);
2127
+ }
2128
+ const readValue = () => {
2129
+ if (inlineValue !== undefined) {
2130
+ return inlineValue;
2131
+ }
2132
+ const value = argv[index + 1];
2133
+ if (value === undefined || value.startsWith("-")) {
2134
+ return failure("CLI_INVALID_VALUE", `${option} requires a value.`, option);
2135
+ }
2136
+ index += 1;
2137
+ return value;
2138
+ };
2139
+ if (option === "-h" || option === "--help") {
2140
+ if (command === "doctor" || command === "devices") {
2141
+ helpTarget = command;
2142
+ }
2143
+ command = "help";
2144
+ } else if (option === "-V" || option === "--version") {
2145
+ command = "version";
2146
+ } else if (option === "--json") {
2147
+ format = "json";
2148
+ } else if (option === "--format") {
2149
+ const value = readValue();
2150
+ if (typeof value !== "string") {
2151
+ return value;
2152
+ }
2153
+ if (!new Set(["human", "json", "ndjson", "plain"]).has(value)) {
2154
+ return failure("CLI_INVALID_VALUE", `Invalid format: ${value}. Expected human, plain, json, or ndjson.`, option);
2155
+ }
2156
+ format = value;
2157
+ } else if (option === "--quiet") {
2158
+ quiet = true;
2159
+ } else if (option === "--verbose") {
2160
+ verbose = true;
2161
+ } else if (option === "--non-interactive") {
2162
+ nonInteractive = true;
2163
+ } else if (option === "--color") {
2164
+ color2 = true;
2165
+ } else if (option === "--no-color") {
2166
+ color2 = false;
2167
+ } else if (option === "--unicode") {
2168
+ unicode = true;
2169
+ } else if (option === "--no-unicode") {
2170
+ unicode = false;
2171
+ } else if (option === "--animation") {
2172
+ animation = true;
2173
+ } else if (option === "--no-animation") {
2174
+ animation = false;
2175
+ } else if (option === "--select") {
2176
+ select = true;
2177
+ } else if (option === "--timeout") {
2178
+ const value = readValue();
2179
+ if (typeof value !== "string") {
2180
+ return value;
2181
+ }
2182
+ timeoutMs = parseDuration(value);
2183
+ if (timeoutMs === undefined) {
2184
+ return failure("CLI_INVALID_VALUE", `Invalid timeout: ${value}. Use a positive duration such as 500ms, 5s, or 1m.`, option);
2185
+ }
2186
+ } else if (option === "--adb") {
2187
+ const value = readValue();
2188
+ if (typeof value !== "string") {
2189
+ return value;
2190
+ }
2191
+ if (value.trim() === "") {
2192
+ return failure("CLI_INVALID_VALUE", "--adb cannot be empty.", option);
2193
+ }
2194
+ adbPath = value;
2195
+ } else if (option === "--adb-host") {
2196
+ const value = readValue();
2197
+ if (typeof value !== "string") {
2198
+ return value;
2199
+ }
2200
+ if (value.trim() === "") {
2201
+ return failure("CLI_INVALID_VALUE", "--adb-host cannot be empty.", option);
2202
+ }
2203
+ adbHost = value;
2204
+ } else if (option === "--adb-port") {
2205
+ const value = readValue();
2206
+ if (typeof value !== "string") {
2207
+ return value;
2208
+ }
2209
+ const parsed = Number(value);
2210
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {
2211
+ return failure("CLI_INVALID_VALUE", `Invalid ADB server port: ${value}.`, option);
2212
+ }
2213
+ adbPort = parsed;
2214
+ } else if (option === "--config") {
2215
+ const value = readValue();
2216
+ if (typeof value !== "string") {
2217
+ return value;
2218
+ }
2219
+ if (value.trim() === "") {
2220
+ return failure("CLI_INVALID_VALUE", "--config cannot be empty.", option);
2221
+ }
2222
+ configPath = value;
2223
+ } else {
2224
+ return failure("CLI_INVALID_OPTION", `Unknown option: ${option}`, option);
2225
+ }
2226
+ }
2227
+ if (quiet && verbose) {
2228
+ return failure("CLI_USAGE", "--quiet and --verbose cannot be used together.");
2229
+ }
2230
+ command ??= "help";
2231
+ if (select && command !== "devices") {
2232
+ return failure("CLI_USAGE", "--select can only be used with the devices command.", "--select");
2233
+ }
2234
+ if (select && nonInteractive) {
2235
+ return failure("CLI_USAGE", "--select cannot be combined with --non-interactive.", "--select");
2236
+ }
2237
+ if (select && format !== "human") {
2238
+ return failure("CLI_USAGE", "--select can only be used with human output.", "--select");
2239
+ }
2240
+ return {
2241
+ ok: true,
2242
+ options: {
2243
+ command,
2244
+ ...helpTarget === undefined ? {} : { helpTarget },
2245
+ format,
2246
+ quiet,
2247
+ verbose,
2248
+ nonInteractive,
2249
+ ...color2 === undefined ? {} : { color: color2 },
2250
+ ...unicode === undefined ? {} : { unicode },
2251
+ ...animation === undefined ? {} : { animation },
2252
+ ...timeoutMs === undefined ? {} : { timeoutMs },
2253
+ ...adbPath === undefined ? {} : { adbPath },
2254
+ ...adbHost === undefined ? {} : { adbHost },
2255
+ ...adbPort === undefined ? {} : { adbPort },
2256
+ ...configPath === undefined ? {} : { configPath },
2257
+ select
2258
+ }
2259
+ };
2260
+ }
2261
+
2262
+ // src/cli/main.ts
2263
+ var VERSION = package_default.version;
2264
+ var HELP = `ADB Ready
2265
+
2266
+ Make an Android target ready, then keep the development session working.
2267
+
2268
+ Usage:
2269
+ adb-ready [command] [options]
2270
+ adbr [command] [options]
2271
+
2272
+ Commands:
2273
+ doctor Inspect the local ADB environment
2274
+ devices List visible Android targets
2275
+ help [doctor|devices] Show help
2276
+ version Show version
2277
+
2278
+ Output:
2279
+ --format FORMAT human, plain, json, or ndjson
2280
+ --json Alias for --format json
2281
+ --quiet Hide successful human output
2282
+ --verbose Include diagnostic evidence
2283
+ --[no-]color Override color detection
2284
+ --[no-]unicode Override Unicode detection
2285
+ --[no-]animation Override motion detection
2286
+
2287
+ Execution:
2288
+ --non-interactive Never prompt or control the terminal
2289
+ --timeout DURATION Positive duration such as 500ms, 5s, or 1m
2290
+ --adb PATH Use an explicit ADB executable
2291
+ --adb-host HOST Use an explicit ADB server host
2292
+ --adb-port PORT Use an explicit ADB server port
2293
+ --config PATH Use an explicit project configuration file
2294
+ --select Interactively select from listed devices
2295
+
2296
+ Other:
2297
+ -h, --help Show help
2298
+ -V, --version Show version
2299
+ `;
2300
+ var COMMAND_HELP = {
2301
+ doctor: `Usage: adb-ready doctor [options]
2302
+
2303
+ Runs read-only host, ADB capability, server, and target diagnostics.
2304
+ `,
2305
+ devices: `Usage: adb-ready devices [options]
2306
+
2307
+ Lists every target visible to ADB. Add --select to open the keyboard picker.
2308
+ `
2309
+ };
2310
+ function inferredFormat(argv) {
2311
+ let format = "human";
2312
+ for (let index = 0;index < argv.length; index += 1) {
2313
+ const argument = argv[index];
2314
+ if (argument === "--json") {
2315
+ format = "json";
2316
+ } else if (argument?.startsWith("--format=")) {
2317
+ const value = argument.slice("--format=".length);
2318
+ if (value === "human" || value === "plain" || value === "json" || value === "ndjson") {
2319
+ format = value;
2320
+ }
2321
+ } else if (argument === "--format") {
2322
+ const value = argv[index + 1];
2323
+ if (value === "human" || value === "plain" || value === "json" || value === "ndjson") {
2324
+ format = value;
2325
+ }
2326
+ index += 1;
2327
+ }
2328
+ }
2329
+ return format;
2330
+ }
2331
+ function capabilities(options, values, io, target, fallbackFormat) {
2332
+ const stream = target === "error" ? io.error : io.output;
2333
+ return resolveTerminalCapabilities({
2334
+ format: options?.format ?? fallbackFormat,
2335
+ nonInteractive: options?.nonInteractive === true || values.interactive === false,
2336
+ ...values.color === undefined ? {} : { color: values.color },
2337
+ ...values.unicode === undefined ? {} : { unicode: values.unicode },
2338
+ ...values.animation === undefined ? {} : { animation: values.animation },
2339
+ ...values.interactive === undefined ? {} : { interactive: values.interactive },
2340
+ env: io.env,
2341
+ inputIsTTY: io.input.isTTY === true,
2342
+ outputIsTTY: stream.isTTY === true,
2343
+ ...stream.columns === undefined ? {} : { columns: stream.columns }
2344
+ });
2345
+ }
2346
+ function failureResult(command, problems, dependencies) {
2347
+ const clock = dependencies.clock ?? (() => new Date);
2348
+ const now = clock();
2349
+ const commandId = (dependencies.idFactory ?? randomUUID3)();
2350
+ return {
2351
+ schemaVersion: SCHEMA_VERSION,
2352
+ command,
2353
+ commandId,
2354
+ ok: false,
2355
+ startedAt: now.toISOString(),
2356
+ finishedAt: now.toISOString(),
2357
+ durationMs: 0,
2358
+ data: null,
2359
+ problems: problems.map((problem) => ({
2360
+ ...problem,
2361
+ correlation: { ...problem.correlation, commandId }
2362
+ }))
2363
+ };
2364
+ }
2365
+ function inputProblem(code, summary, detail, commandId = "cli") {
2366
+ return {
2367
+ code,
2368
+ category: "input.cli",
2369
+ severity: "error",
2370
+ summary,
2371
+ detail,
2372
+ retryable: true,
2373
+ evidence: [],
2374
+ actions: [],
2375
+ correlation: { commandId }
2376
+ };
2377
+ }
2378
+ function configProblems(errors) {
2379
+ return errors.map((error2) => ({
2380
+ code: error2.code,
2381
+ category: "input.configuration",
2382
+ severity: "error",
2383
+ summary: error2.message,
2384
+ detail: `Invalid ${error2.source} configuration at ${error2.path}.`,
2385
+ retryable: true,
2386
+ evidence: [
2387
+ { source: "configuration", field: "path", value: error2.path },
2388
+ ...error2.location === undefined ? [] : [
2389
+ {
2390
+ source: "configuration",
2391
+ field: "location",
2392
+ value: redactText(error2.location).value
2393
+ }
2394
+ ]
2395
+ ],
2396
+ actions: [],
2397
+ correlation: { commandId: "configuration" }
2398
+ }));
2399
+ }
2400
+ function renderFailure(result, format, io) {
2401
+ renderResult(result, {
2402
+ format,
2403
+ capabilities: capabilities(undefined, {}, io, "output", format),
2404
+ sink: format === "human" ? io.error : io.output,
2405
+ verbose: false
2406
+ });
2407
+ }
2408
+ function cliConfig(options) {
2409
+ return {
2410
+ ...options.adbPath === undefined ? {} : { adbPath: options.adbPath },
2411
+ ...options.adbHost === undefined ? {} : { adbHost: options.adbHost },
2412
+ ...options.adbPort === undefined ? {} : { adbPort: options.adbPort },
2413
+ ...options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs },
2414
+ ...options.color === undefined ? {} : { color: options.color },
2415
+ ...options.unicode === undefined ? {} : { unicode: options.unicode },
2416
+ ...options.animation === undefined ? {} : { animation: options.animation },
2417
+ ...options.nonInteractive ? { interactive: false } : {}
2418
+ };
2419
+ }
2420
+ function selectionProblem(result, commandId) {
2421
+ if (result === "interrupt" || result === "signal") {
2422
+ return {
2423
+ problem: inputProblem(ProblemCode.OperationInterrupted, "Target selection was interrupted.", "Run the command again when you are ready to select a target.", commandId),
2424
+ exitCode: 130 /* Interrupted */
2425
+ };
2426
+ }
2427
+ if (result === "unavailable") {
2428
+ return {
2429
+ problem: inputProblem(ProblemCode.InteractiveSelectionUnavailable, "Interactive target selection is unavailable.", "Run this command in a capable terminal or omit --select.", commandId),
2430
+ exitCode: 2 /* InvalidInput */
2431
+ };
2432
+ }
2433
+ return {
2434
+ problem: inputProblem(ProblemCode.TargetSelectionCancelled, "Target selection was cancelled.", "No target was selected.", commandId),
2435
+ exitCode: 2 /* InvalidInput */
2436
+ };
2437
+ }
2438
+ async function runInteractiveSession(io, dependencies, terminal, signal) {
2439
+ let presentation = "full";
2440
+ while (true) {
2441
+ const home = await showHomeScreen({
2442
+ version: VERSION,
2443
+ input: io.input,
2444
+ sink: io.error,
2445
+ capabilities: terminal,
2446
+ presentation,
2447
+ ...signal === undefined ? {} : { signal }
2448
+ });
2449
+ if (home.kind === "cancelled") {
2450
+ return 130 /* Interrupted */;
2451
+ }
2452
+ if (home.kind !== "action") {
2453
+ return 2 /* InvalidInput */;
2454
+ }
2455
+ if (home.action === "exit") {
2456
+ return 0 /* Success */;
2457
+ }
2458
+ clearInteractiveScreen(io.error, terminal);
2459
+ await runCliInternal([home.action], io, dependencies, signal);
2460
+ if (signal?.aborted === true) {
2461
+ return 130 /* Interrupted */;
2462
+ }
2463
+ presentation = "menu";
2464
+ }
2465
+ }
2466
+ async function selectDevice(execution, io, terminal, signal) {
2467
+ const data = execution.result.data;
2468
+ if (data === null) {
2469
+ return execution;
2470
+ }
2471
+ const selectable = data.devices.filter(({ state }) => state === "device");
2472
+ if (selectable.length === 0) {
2473
+ const problem = inputProblem(ProblemCode.NoSelectableTarget, "No ready Android target can be selected.", "Connect or recover a target until ADB reports the device state.", execution.result.commandId);
2474
+ return {
2475
+ exitCode: 20 /* Target */,
2476
+ result: { ...execution.result, ok: false, problems: [...execution.result.problems, problem] }
2477
+ };
2478
+ }
2479
+ const selection = await selectOne({
2480
+ title: "Select an Android target",
2481
+ options: data.devices.map((device) => ({
2482
+ value: device,
2483
+ label: device.model ?? device.serial,
2484
+ description: `${device.serial} · ${device.state}`,
2485
+ disabled: device.state !== "device",
2486
+ recommended: selectable.length === 1 && device.serial === selectable[0]?.serial
2487
+ })),
2488
+ input: io.input,
2489
+ sink: io.error,
2490
+ capabilities: terminal,
2491
+ ...signal === undefined ? {} : { signal }
2492
+ });
2493
+ if (selection.kind === "selected") {
2494
+ return {
2495
+ ...execution,
2496
+ result: {
2497
+ ...execution.result,
2498
+ data: { ...data, selected: selection.value }
2499
+ }
2500
+ };
2501
+ }
2502
+ const failure2 = selectionProblem(selection.kind === "unavailable" ? "unavailable" : selection.reason, execution.result.commandId);
2503
+ return {
2504
+ exitCode: failure2.exitCode,
2505
+ result: {
2506
+ ...execution.result,
2507
+ ok: false,
2508
+ problems: [...execution.result.problems, failure2.problem]
2509
+ }
2510
+ };
2511
+ }
2512
+ async function runCliInternal(argv, io, dependencies = {}, signal) {
2513
+ const fallbackFormat = inferredFormat(argv);
2514
+ let effectiveArgv = argv;
2515
+ if (argv.length === 0) {
2516
+ const homeCapabilities = capabilities(undefined, {}, io, "error", "human");
2517
+ if (homeCapabilities.interactive) {
2518
+ return await runInteractiveSession(io, dependencies, homeCapabilities, signal);
2519
+ } else {
2520
+ effectiveArgv = ["help"];
2521
+ }
2522
+ }
2523
+ const parsed = parseArguments(effectiveArgv);
2524
+ if (!parsed.ok) {
2525
+ const failure2 = failureResult("cli", [inputProblem(parsed.code, parsed.message, "Run adb-ready --help for usage.")], dependencies);
2526
+ renderFailure(failure2, fallbackFormat, io);
2527
+ return 2 /* InvalidInput */;
2528
+ }
2529
+ const options = parsed.options;
2530
+ if (options.command === "help") {
2531
+ io.output.write(options.helpTarget === undefined ? HELP : COMMAND_HELP[options.helpTarget]);
2532
+ return 0 /* Success */;
2533
+ }
2534
+ if (options.command === "version") {
2535
+ io.output.write(`${VERSION}
2536
+ `);
2537
+ return 0 /* Success */;
2538
+ }
2539
+ const loaded = await (dependencies.loadConfig ?? loadConfig)({
2540
+ cwd: io.cwd,
2541
+ env: io.env,
2542
+ ...options.configPath === undefined ? {} : { projectConfigPath: options.configPath },
2543
+ explicitProjectConfig: options.configPath !== undefined,
2544
+ cli: cliConfig(options)
2545
+ });
2546
+ if (!loaded.ok) {
2547
+ const failure2 = failureResult(options.command, configProblems(loaded.errors), dependencies);
2548
+ renderFailure(failure2, options.format, io);
2549
+ return 2 /* InvalidInput */;
2550
+ }
2551
+ const values = loaded.config.values;
2552
+ const errorCapabilities = capabilities(options, values, io, "error", options.format);
2553
+ const outputCapabilities = capabilities(options, values, io, "output", options.format);
2554
+ const bus = dependencies.bus ?? new EventBus(dependencies.clock);
2555
+ const progress = options.format === "human" && !options.quiet ? new ProgressRenderer({
2556
+ bus,
2557
+ sink: io.error,
2558
+ capabilities: errorCapabilities,
2559
+ verbose: options.verbose
2560
+ }) : undefined;
2561
+ const events = options.format === "ndjson" ? new NdjsonEventRenderer(bus, io.output) : undefined;
2562
+ const commandDependencies = { ...dependencies, bus };
2563
+ const config = {
2564
+ ...values.adbPath === undefined ? {} : { adbPath: values.adbPath },
2565
+ ...values.adbHost === undefined ? {} : { adbHost: values.adbHost },
2566
+ ...values.adbPort === undefined ? {} : { adbPort: values.adbPort },
2567
+ timeoutMs: values.timeoutMs
2568
+ };
2569
+ let execution;
2570
+ try {
2571
+ execution = options.command === "doctor" ? await runDoctor(config, commandDependencies, signal) : await runDevices(config, commandDependencies, signal);
2572
+ } finally {
2573
+ progress?.dispose();
2574
+ events?.dispose();
2575
+ }
2576
+ if (options.command === "devices" && options.select) {
2577
+ execution = await selectDevice(execution, io, errorCapabilities, signal);
2578
+ }
2579
+ if (options.format !== "human" || !options.quiet || !execution.result.ok) {
2580
+ const human = options.format === "human";
2581
+ renderResult(execution.result, {
2582
+ format: options.format,
2583
+ capabilities: human ? errorCapabilities : outputCapabilities,
2584
+ sink: human ? io.error : io.output,
2585
+ verbose: options.verbose
2586
+ });
2587
+ }
2588
+ return execution.exitCode;
2589
+ }
2590
+ async function runCli(argv, io, dependencies = {}, signal) {
2591
+ try {
2592
+ return await runCliInternal(argv, io, dependencies, signal);
2593
+ } catch (caught) {
2594
+ const message = redactText(caught instanceof Error ? caught.message : String(caught)).value;
2595
+ const problem = {
2596
+ code: "INTERNAL_ERROR",
2597
+ category: "internal.unexpected",
2598
+ severity: "error",
2599
+ summary: "ADB Ready encountered an unexpected internal error.",
2600
+ detail: "The operation stopped safely. Re-run with --verbose when reporting this issue.",
2601
+ retryable: false,
2602
+ evidence: message === "" ? [] : [{ source: "internal", field: "message", value: message }],
2603
+ actions: [],
2604
+ correlation: { commandId: "cli" }
2605
+ };
2606
+ renderFailure(failureResult("cli", [problem], dependencies), inferredFormat(argv), io);
2607
+ return 70 /* Internal */;
2608
+ }
2609
+ }
2610
+ function processIo() {
2611
+ return {
2612
+ input: process5.stdin,
2613
+ output: process5.stdout,
2614
+ error: process5.stderr,
2615
+ cwd: process5.cwd(),
2616
+ env: process5.env
2617
+ };
2618
+ }
2619
+
2620
+ // src/cli.ts
2621
+ var controller = new AbortController;
2622
+ var abort = () => controller.abort();
2623
+ process6.once("SIGINT", abort);
2624
+ process6.once("SIGTERM", abort);
2625
+ try {
2626
+ process6.exitCode = await runCli(process6.argv.slice(2), processIo(), {}, controller.signal);
2627
+ } finally {
2628
+ process6.removeListener("SIGINT", abort);
2629
+ process6.removeListener("SIGTERM", abort);
2630
+ }
2631
+
2632
+ //# debugId=E7C1326D860AA56164756E2164756E21