npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@magnaboy/cli-android

v0.0.6

Published

Reusable Android SDK, ADB, Gradle, APK and device-test primitives for Node.js CLIs.

Readme

@magnaboy/cli-android

Reusable, shell-free Android SDK and device automation primitives for Node.js repository scripts.

Install

npm i @magnaboy/cli-android @magnaboy/cli-core

Requires Node 25+ and ESM. Android SDK tools are discovered from ANDROID_HOME, ANDROID_SDK_ROOT, or PATH; the package does not bundle the Android SDK.

ADB

import { AdbClient, findAdb } from '@magnaboy/cli-android';
import { detectHost, NativeProcessRunner } from '@magnaboy/cli-core';

const runner = new NativeProcessRunner();
const executable = await findAdb(await detectHost(runner), runner);
if (!executable) throw new Error('adb is not installed');

const adb = new AdbClient({ executable, runner });
const selected = adb.device('emulator-5554');
await selected.requireReady();
console.log(await selected.capabilities());

The serial-bound device client supports shell commands, properties, installation, uninstallation, file transfer and readiness checks while retaining raw run() and shell() escape hatches.

Gradle and APK inspection

import { createAndroidGradleCommand, inspectApk } from '@magnaboy/cli-android';

const command = createAndroidGradleCommand({
	projectDirectory: 'apps/android',
	host,
	tasks: ['lintRelease', 'assembleRelease'],
	configurationCache: false,
	kotlinCompilerExecutionStrategy: 'in-process',
	jvmArgs: '-Xmx4g -Dfile.encoding=UTF-8'
});
await runner.run(command);

const apk = await inspectApk({ apkPath: 'app-release.apk', runner });

APK inspection returns package/version/debuggable metadata and every SHA-256 signing-certificate fingerprint. Release certificate allowlists and version policy remain the caller's responsibility.

Installing an app on a device

import { formatAndroidInstallHelp, installAndroidApp, parseAndroidInstallArgs } from '@magnaboy/cli-android';

const variants = [
	{ name: 'development', task: ':app:installDebug', aliases: ['dev', 'debug'], description: 'debug build.' },
	{
		name: 'production',
		task: ':app:installRelease',
		aliases: ['prod', 'release'],
		description: 'signed release build.',
		requiredFiles: [{ path: 'app/keystore.properties', label: 'Release signing configuration' }]
	}
];

await installAndroidApp({
	projectDirectory: 'apps/android',
	options: parseAndroidInstallArgs(process.argv.slice(2), { variants }),
	variants,
	runner,
	prompter,
	help: formatAndroidInstallHelp({ usage: 'pnpm install-app --type production', variants })
});

parseAndroidInstallArgs handles --type, --device/--serial, --non-interactive, --timeout, --dry-run, --list-devices and --help. installAndroidApp finds ADB, selects and waits for the device, checks the Gradle wrapper and every variant prerequisite, then runs the install task with ANDROID_SERIAL bound to the selection. Gradle command creation, an optional per-device lock and the output streams are injectable.

Release APK verification

import { readCertificateAllowlist, resolveReleaseApk, verifyReleaseApk } from '@magnaboy/cli-android';

const apkPath = await resolveReleaseApk({ outputDirectory: 'app/build/outputs/apk/release' });
verifyReleaseApk(await inspectApk({ apkPath, runner }), {
	applicationId: 'com.example.app',
	allowedCertificates: (await readCertificateAllowlist('scripts/app-release.certs.json')).myApp
});

verifyReleaseApk rejects a wrong applicationId, a debuggable build, a missing versionName, a versionCode that does not match androidVersionCode(versionName) and an unrecognized signing certificate. Uploading and publishing the verified APK remain the caller's responsibility.

Device tests and coverage

The package also exports pure ADB-device selection, display/keyguard readiness, camera occupancy, Gradle connected-test output parsing, and Android JaCoCo summary collection. Warning output, clocks, sleeping and interactive streams can be injected, allowing deterministic tests without an attached device.

Identifying and selecting devices

import { parseDeviceAliases, resolveAndroidDevices } from '@magnaboy/cli-android';

const devices = await resolveAndroidDevices(adb, {
	spec: 'pixel,s26',
	aliases: parseDeviceAliases('pixel=akita s26=SM_S942B')
});
for (const device of devices) console.log(device.slug, device.serial);

resolveAndroidDevices lists attached phones, collapses transports, matches a spec, and names each result. listAndroidDevices stops after the naming step, and matchDevices, mergeDeviceTransports and assignDeviceSlugs are exported separately so each step can be tested or reused.

A phone reachable over USB and wireless at the same time answers on two serials. Capturing both would run two collectors against one set of kernel counters and silently halve every number derived from them, so mergeDeviceTransports keys on ro.serialno and keeps one entry per phone, preferring USB because a large perf.data crawls over wireless adb. Every transport is kept in transports.

A spec is a comma-separated list of aliases, serials, models or product names, or all. Matching is case-insensitive with underscores and spaces folded to hyphens, so SM_S942B finds sm-s942b. A token matching more than one attached phone is an error rather than a guess, because picking one would silently measure the wrong device.

assignDeviceSlugs gives each device a filesystem-safe name for directories and column headings. The model alone reads best, and a serial tail is appended only when two selected devices share a model, so a two-phone session never grows unreadable labels.

Counter snapshots

import { counterSnapshotScript, diffCounterSnapshots, parseCounterSnapshot } from '@magnaboy/cli-android';

const read = async (phase: 'before' | 'after') =>
	parseCounterSnapshot((await device.shell([counterSnapshotScript(packageName, phase)])).stdout);

const before = await read('before');
// ... the measured window ...
const diff = diffCounterSnapshots(before, await read('after'), { fallbackSeconds: 30 });

counterSnapshotScript builds one shell script that collects the process and per-thread /proc counters, GPU residency, dumpsys gpu, and battery state. It is deliberately a single adb shell invocation: read section by section it was seven round trips, and on wireless adb that made a nominal 5s window's before/after counters span 12.7s, so every rate derived from them was wrong by a factor of 2.5. The device boottime is captured inside the same shell, which is what lets a Perfetto trace be clipped to exactly the measured window, since ftrace timestamps are CLOCK_BOOTTIME.

diffCounterSnapshots divides by the interval the counters were actually read over, snapshot latency included, not the duration that was requested; pass fallbackSeconds for when the boottime marks cannot supply one. If the process restarted mid-window its counters reset, so the CPU and thread deltas are omitted rather than reported as negative numbers. GPU residency becomes frequency-seconds, a residency-weighted mean clock, and a windowShort flag, because the per-operating-point counters advance on state change and a short window ends mid-interval and under-counts.

batteryDischargeMilliwatts returns whole-phone power or the reason it is unavailable. Net charge can fall while the phone is on USB, and that fall is not discharge power, so both snapshots must show an unplugged, discharging battery before a number is reported.

Parsing device output

parseProcStat, parseProcStatus, parseTaskFiles and parseThreadTable read /proc. A process comm is unquoted and may contain spaces and parentheses, so the positional fields are taken from after the final ). An absent schedstat stays null rather than becoming zero, which would read as a thread that ran for no time at all. Note that waitSeconds is schedstat field 1: it is documented as runqueue wait, but some kernels report a value that cannot be that, so do not read it as starvation.

parseFrameStats, parseMemoryInfo, parseBatteryState and parseThermalState read dumpsys. Every battery pattern is line-anchored, because an unanchored voltage: also matches Max charging voltage: 0 and reports 0V for every window. parseThermalState matches the overall mStatus as a whole line, since each Temperature{...} block carries its own and an unanchored pattern reports a throttled device as nominal whenever the first sensor happened to be cool.

AdbClient converts CRLF to LF in captured output by default, because adb on Windows terminates every line that way and a parser written against POSIX output silently fails to match. Pass normalizeLineEndings: false when the exact bytes matter. AdbDeviceClient.getProperties reads several properties in one round trip, and logcat streams to a file as a ManagedProcess.