#! /usr/bin/env node
///
import "source-map-support/register.js";
import { Adb, AdbServerClient } from "@yume-chan/adb";
import { AdbServerNodeTcpConnector } from "@yume-chan/adb-server-node-tcp";
import {
ConsumableWritableStream,
WritableStream,
} from "@yume-chan/stream-extra";
import { program } from "commander";
program
.name("tango-cli")
.option("-H ", "name of adb server host", "127.0.0.1")
.option(
"-P ",
"port of adb server",
(value) => Number.parseInt(value, 10),
5037,
)
.configureHelp({
subcommandTerm(cmd) {
let usage = cmd.usage();
if (usage === "[options]" && cmd.options.length === 0) {
usage = "";
}
return `${cmd.name()} ${usage}`;
},
});
function createClient() {
const opts: { H: string; P: number } = program.opts();
const connection = new AdbServerNodeTcpConnector({
host: opts.H,
port: opts.P,
});
const client = new AdbServerClient(connection);
return client;
}
program
.command("devices")
.usage("[-l]")
.description("list connected devices (-l for long output)")
.option("-l", "long output", false)
.action(async (options: { l: boolean }) => {
function appendTransportInfo(key: string, value: string | undefined) {
if (value) {
return ` ${key}:${value}`;
}
return "";
}
const client = createClient();
const devices = await client.getDevices();
for (const device of devices) {
if (options.l) {
console.log(
// prettier-ignore
`${
device.serial.padEnd(22)
}device${
appendTransportInfo("product", device.product)
}${
appendTransportInfo("model", device.model)
}${
appendTransportInfo("device", device.device)
}${
appendTransportInfo("transport_id", device.transportId.toString())
}`,
);
} else {
console.log(`${device.serial}\tdevice`);
}
}
});
interface DeviceCommandOptions {
d: true | undefined;
e: true | undefined;
s: string | undefined;
t: bigint | undefined;
}
function createDeviceCommand(nameAndArgs: string) {
return program
.command(nameAndArgs)
.option("-d", "use USB device (error if multiple devices connected)")
.option(
"-e",
"use TCP/IP device (error if multiple TCP/IP devices available)",
)
.option(
"-s ",
"use device with given serial (overrides $ANDROID_SERIAL)",
process.env.ANDROID_SERIAL,
)
.option("-t ", "use device with given transport id", (value) =>
BigInt(value),
);
}
async function createAdb(options: DeviceCommandOptions) {
const client = createClient();
const transport = await client.createTransport(
options.d
? {
usb: true,
}
: options.e
? {
tcp: true,
}
: options.s !== undefined
? {
serial: options.s,
}
: options.t !== undefined
? {
transportId: options.t,
}
: undefined,
);
const adb = new Adb(transport);
return adb;
}
createDeviceCommand("shell [args...]")
.usage("[options] [-- ]")
.description(
"run remote shell command (interactive shell if no command given). `--` is required before command name.",
)
.configureHelp({ showGlobalOptions: true })
.action(async (args: string[], options: DeviceCommandOptions) => {
const adb = await createAdb(options);
const shell = await adb.subprocess.shell(args);
const stdinWriter = shell.stdin.getWriter();
process.stdin.setRawMode(true);
process.stdin.on("data", (data: Uint8Array) => {
ConsumableWritableStream.write(stdinWriter, data).catch((e) => {
console.error(e);
process.exit(1);
});
});
shell.stdout
.pipeTo(
new WritableStream({
write(chunk) {
process.stdout.write(chunk);
},
}),
)
.catch((e) => {
console.error(e);
process.exit(1);
});
shell.exit.then(
(code) => {
// `process.stdin.on("data")` will keep the process alive,
// so call `process.exit` explicitly.
process.exit(code);
},
(e) => {
console.error(e);
process.exit(1);
},
);
});
createDeviceCommand("logcat [args...]")
.usage("[-- ")
.description("show device log (logcat --help for more)")
.configureHelp({ showGlobalOptions: true })
.action(async (args: string[], options: DeviceCommandOptions) => {
const adb = await createAdb(options);
const logcat = await adb.subprocess.spawn(`logcat ${args.join(" ")}`);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on("SIGINT", async () => {
await logcat.kill();
});
await logcat.stdout.pipeTo(
new WritableStream({
write: (chunk) => {
process.stdout.write(chunk);
},
}),
);
});
createDeviceCommand("reboot [mode]")
.usage("[bootloader|recovery|sideload|sideload-auto-reboot]")
.description(
"reboot the device; defaults to booting system image but supports bootloader and recovery too. sideload reboots into recovery and automatically starts sideload mode, sideload-auto-reboot is the same but reboots after sideloading.",
)
.configureHelp({ showGlobalOptions: true })
.action(async (mode: string | undefined, options: DeviceCommandOptions) => {
const adb = await createAdb(options);
await adb.power.reboot(mode);
});
createDeviceCommand("usb")
.usage(" ")
.description("restart adbd listening on USB")
.configureHelp({ showGlobalOptions: true })
.action(async (options: DeviceCommandOptions) => {
const adb = await createAdb(options);
const output = await adb.tcpip.disable();
process.stdout.write(output, "utf8");
});
createDeviceCommand("tcpip port")
.usage("port")
.description("restart adbd listening on TCP on PORT")
.configureHelp({ showGlobalOptions: true })
.action(async (port: string, options: DeviceCommandOptions) => {
const adb = await createAdb(options);
const output = await adb.tcpip.setPort(Number.parseInt(port, 10));
process.stdout.write(output, "utf8");
});
program
.command("kill-server")
.description("kill the server if it is running")
.configureHelp({ showGlobalOptions: true })
.action(async () => {
const client = createClient();
await client.killServer();
});
program.parse();