diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml
index 03ced1d..c8082aa 100644
--- a/.github/workflows/bundle-size.yml
+++ b/.github/workflows/bundle-size.yml
@@ -40,7 +40,7 @@ jobs:
with:
build_command: "npm run build:ci"
# Every glob starts with `*`: the v4.29.2 measure action walks each path prefix separately (a bare top-level file matches nothing, and mixing `*` globs with directory globs counts files twice). CLDMV/.github#326/#327 fix this upstream.
- dist_paths: "*index.mjs,*index.cjs,*devcheck.mjs,*dist/**,*types/index.d.mts*,*types/devcheck*.mts,*types/dist/**"
+ dist_paths: "*index.mjs,*index.cjs,*dist/**,*types/index.d.mts*,*types/dist/**"
# warning_pct: 5
# warning_bytes: 500
# comment_mode: "update"
diff --git a/README.md b/README.md
index c888662..d12a674 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,16 @@
-# DroidSock
+# @cldmv/droidsock
-A complete, from-scratch implementation of the Android Debug Bridge (ADB) protocol in Node.js. This library provides full ADB functionality including device connection, RSA authentication, shell command execution, and file transfers - eliminating clicking sounds on Android TV devices!
+**DroidSock** is a complete, from-scratch implementation of the Android Debug Bridge (ADB) protocol in Node.js. This library provides full ADB functionality including device connection, RSA authentication, shell command execution, and file transfers - eliminating clicking sounds on Android TV devices!
-[![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] [![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] [![npm last update]][npm_last_update_url] [![coverage]][coverage_url]
+It talks to devices directly over TCP, with no `adb` binary or ADB server in between, and composes its protocol layers into a single API tree with [`@cldmv/slothlet`](https://github.com/CLDMV/slothlet), so every connected device becomes its own persistent API leaf.
-[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
+> _ADB over the wire, in pure Node.js - no adb binary required._
+
+[![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] [![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] [![npm last update]][npm_last_update_url] [![coverage]][coverage_url]
> [!NOTE]
> **Current status:**
@@ -17,30 +19,26 @@ A complete, from-scratch implementation of the Android Debug Bridge (ADB) protoc
> - **File transfer**: `mkdir` / `remove` / `move` / `copy` / `chmod` / `diskUsage` / `find` / `stat` work today via shell commands. `list` prefers a binary-safe SYNC-based implementation with automatic shell fallback.
> - **Experimental**: `push` / `pull` / `pushV2` / `pullV2` / `listSync` / `listV2` / `statV2` (real ADB SYNC sub-protocol usage, both the legacy 32-bit and newer 64-bit variants), `device.reboot()`, `device.forward()` / `device.reverse()`, `device.install()` (both the classic push-then-install and modern streaming install paths), and `pairing.pair()` (Wi-Fi pairing) are all implemented - built from the ADB protocol spec and covered by unit tests (several exercised against real loopback TCP/TLS servers, not purely mocks) - but **none of them have been run against a real device yet**. See [#1](https://github.com/CLDMV/droidsock/issues/1).
+[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
+
---
## โจ What's New
-### Latest: v2.0.0 (September 2026)
+### Latest: v2.0.3 (October 2026)
-- **๐จ Breaking**: the `device` module is now split into `device` (single-target `connect(host, port)` / `disconnect(host, port)` / `remove(host, port)`) and `devices` (collection-wide `list()` / `disconnect()` (all) / `remove()` (all) / `get(idOrLeaf)`) - connected devices are still mounted as composed API leaves at `api.devices.`. A device leaf now persists across a disconnect - `disconnect()` only tears down the socket (and stays synchronous), `connect()` on the same host:port later reconnects that same leaf without re-supplying options, and `remove()` is the new, separate "forget this device" operation (the one that's actually `async`).
-- **IPv6 support** - `device.connect()`, `discover.subnet()`, and `discover.mdns()` all accept IPv6 addresses/CIDRs now, not just IPv4.
-- **`devices.get(idOrLeaf)`** (new) - looks up a connected device leaf by `"host:port"` string or by the leaf object itself; the safe way to re-resolve a leaf reference instead of holding onto a stale one.
-- **`device.reverse()`** (experimental) - completes port forwarding with the device โ host direction.
-- **`pairing.pair()`** (experimental) - Wi-Fi pairing (SPAKE2-over-Ed25519 + TLS 1.3) for Android 11+ wireless debugging.
-- **Streaming APK install** (experimental) - `device.install()` now tries `exec:cmd`-based streaming install first, falling back to the classic push-then-install flow.
-- **SYNC V2 (64-bit)** - `pushV2` / `pullV2` / `statV2` / `listV2` lift the legacy 32-bit size ceiling, with optional brotli compression.
-- **Hardening** - closed a shell-injection gap in `devices.mjs`'s and `shell.mjs`'s convenience shortcuts, capped several unbounded device-controlled memory allocations in the SYNC V2 paths, and fixed a handful of mid-transfer disconnect/failure edge cases.
-- [View full v2.0.0 Changelog](./docs/changelog/v2/v2.0.0.md)
+- **Bundler-friendly CommonJS entry** - `index.cjs` now loads the ESM entry with a plain `require()` instead of `createRequire`, so esbuild and webpack can follow it, and on a Node.js version without `require(esm)` it fails with a clear message pointing to `import()`. The ESM entry and the API are unchanged ([#58](https://github.com/CLDMV/droidsock/pull/58)).
+- **`devcheck` no longer published** - the source-checkout-only `devcheck.mjs` and its `./devcheck` subpath export are gone from the package. It never did anything in an installed copy, but importing `@cldmv/droidsock/devcheck` now throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, so remove any such import ([#58](https://github.com/CLDMV/droidsock/pull/58)).
+- [View full v2.0.3 Changelog](https://github.com/CLDMV/droidsock/blob/master/docs/changelog/v2/v2.0.3.md)
### Recent Releases
-- **v1.2.0** (September 2026) - Device discovery (`discover.subnet()` CIDR sweep, `discover.mdns()` for wireless-debugging-advertised devices, both experimental) and a shell-injection fix across every `files.*` shell-based method ([Changelog](./docs/changelog/v1/v1.2.0.md))
-- **v1.1.1** (September 2026) - Documentation formatting fix (padded slashes between adjacent code spans) - no code changes ([PR #16](https://github.com/CLDMV/droidsock/pull/16))
-- **v1.1.0** (September 2026) - Fixed the `list()`/`stat()` regression from v1.0.0, and added a binary-safe SYNC-based `list()` with shell fallback, real ADB `reboot:` support, TCP port forwarding, and local APK install ([Changelog](./docs/changelog/v1/v1.1.0.md))
-- **v1.0.0** (September 2026) - First tagged release - a callable quick-path default export (dropping `connect()`/`listDevices()`), a real test suite with measured coverage, and a full CI/release pipeline ([Changelog](./docs/changelog/v1/v1.0.0.md))
+- **v2.0.2** (October 2026) - Dev tooling only: shared CLDMV fix-headers config and a required PR check that never reports as skipped, no runtime change ([Changelog](https://github.com/CLDMV/droidsock/blob/master/docs/changelog/v2/v2.0.2.md))
+- **v2.0.1** (October 2026) - No runtime change, but `engines.node` rose to `>=22.12.0` (from `>=20.19.0`) to match the vitest 5 toolchain; also syncs the v4 workflows with the v4.29.2 templates ([Changelog](https://github.com/CLDMV/droidsock/blob/master/docs/changelog/v2/v2.0.1.md))
+- **v2.0.0** (September 2026) - Breaking: the `device` module splits into `device` and `devices`, device leaves persist across a disconnect, plus IPv6, `devices.get()`, and experimental `device.reverse()`, Wi-Fi pairing, streaming install and SYNC V2 ([Changelog](https://github.com/CLDMV/droidsock/blob/master/docs/changelog/v2/v2.0.0.md))
+- **v1.2.0** (September 2026) - Device discovery (`discover.subnet()` CIDR sweep, `discover.mdns()` for wireless-debugging-advertised devices, both experimental) and a shell-injection fix across every `files.*` shell-based method ([Changelog](https://github.com/CLDMV/droidsock/blob/master/docs/changelog/v1/v1.2.0.md))
-๐ **For complete version history and detailed release notes, see [docs/changelog/](./docs/changelog/) folder.**
+๐ **For complete version history and detailed release notes, see the [docs/changelog/](https://github.com/CLDMV/droidsock/tree/master/docs/changelog/) folder.**
---
@@ -55,16 +53,27 @@ A complete, from-scratch implementation of the Android Debug Bridge (ADB) protoc
- โ
**Port Forwarding** (experimental): `adb forward`/`adb reverse`-equivalent TCP tunneling, both directions
- โ
**APK Install** (experimental): `adb install`-equivalent local APK installation - classic push-then-install and modern streaming (`exec:cmd package install`) paths
- โ
**Wi-Fi Pairing** (experimental): `adb pair`-equivalent PIN-based pairing (SPAKE2-over-Ed25519 + TLS 1.3) for Android 11+ wireless debugging
-- โ
**Device Discovery**: Support for multiple devices via configuration
+- โ
**Device Discovery** (experimental): `discover.subnet()` CIDR sweep and `discover.mdns()` for devices advertising wireless debugging, plus support for multiple devices via configuration
- โ
**Error Handling**: Robust error handling and connection recovery
-## Installation
+---
+
+## ๐ฆ Installation
+
+### Requirements
+
+- **Node.js v22.12.0 or higher** (the package's `engines.node` floor)
+- Both `import` and `require()` are supported. `require()` loads the ESM entry through Node's synchronous `require(esm)`, which needs Node.js ^20.19.0 or >=22.12.0; on older versions, load the package with `import()` instead.
+
+### Install
```bash
npm install @cldmv/droidsock
```
-## Quick Start
+---
+
+## ๐ Quick Start
```javascript
import droidsock from "@cldmv/droidsock";
@@ -92,9 +101,13 @@ const logcat = device.logcat({
await device.disconnect();
```
-## Device Configuration
+CommonJS works the same way: `const droidsock = require("@cldmv/droidsock")` (also available as `createDroidSock`).
-Use the `references/devices.json` file to configure your devices:
+---
+
+## โ๏ธ Device Configuration
+
+The example scripts read device addresses from `references/devices.json`. The folder is git-ignored, so create the file yourself:
```json
{
@@ -114,13 +127,17 @@ Use the `references/devices.json` file to configure your devices:
}
```
-## API Reference
+---
+
+## ๐ API Reference
`droidsock(options)` (also `createDroidSock`) creates the API instance; `api.device.connect(host, port, options)` connects to a device (IPv4 or IPv6) and returns its live leaf - also reachable afterward at `api.devices["_"]` (a `.` becomes `_`, a `:` becomes `__`) - exposing connection state, shell execution/streaming, file operations (`push` / `pull` / `list` / `stat`), reboot, port forwarding, and APK install. `api.device.disconnect(host, port)` tears down one device's connection without forgetting it - reconnect later with `connect()` on the same host:port, no need to re-supply options; `api.device.remove(host, port)` forgets it entirely. `api.devices.list()` / `disconnect()` (all) / `remove()` (all) / `get(idOrLeaf)` manage the set of known devices as a whole.
-๐ **See [docs/API.md](./docs/API.md) for the full method reference**, including every option and the experimental/scope caveats on `push` / `pull` / `list` / `forward` / `reverse` / `install`.
+๐ **See [docs/API.md](https://github.com/CLDMV/droidsock/blob/master/docs/API.md) for the full method reference**, including every option and the experimental/scope caveats on `push` / `pull` / `list` / `forward` / `reverse` / `install`.
+
+---
-## Examples
+## ๐ก Examples
### Basic Usage
@@ -145,7 +162,9 @@ node examples/streaming-example.mjs top
node examples/streaming-example.mjs files
```
-## Architecture
+---
+
+## ๐๏ธ Architecture
`src/droidsock.mjs` composes the layers below into a single api tree via [`@cldmv/slothlet`](https://github.com/CLDMV/slothlet):
@@ -162,9 +181,11 @@ node examples/streaming-example.mjs files
11. **Device / Devices Layers** (`src/api/device.mjs`, `src/api/devices.mjs`): High-level per-device API composing the layers above, split by single-target (`device.connect`/`disconnect`/`remove`) vs. collection-wide (`devices.list`/`disconnect`/`remove`/`get`) operations. Each device is a real, persistent slothlet leaf at `api.devices.`, assigned there by `connect()` rather than held in a private module variable, so its methods keep working `self`/context access exactly like any other leaf - the leaf outlives any one connection, and only `remove()` unmounts it
12. **Config / Log Layers** (`src/api/config.mjs`, `src/api/log.mjs`): Shared configuration and logging
-๐ **See [docs/PROTOCOL.md](./docs/PROTOCOL.md) for wire-level protocol details** (packet structure, auth flow, SYNC sub-protocol framing, reboot/forward service usage).
+๐ **See [docs/PROTOCOL.md](https://github.com/CLDMV/droidsock/blob/master/docs/PROTOCOL.md) for wire-level protocol details** (packet structure, auth flow, SYNC sub-protocol framing, reboot/forward service usage).
-## Troubleshooting
+---
+
+## ๐ Troubleshooting
### Connection Issues
@@ -185,28 +206,71 @@ node examples/streaming-example.mjs files
- "Stream not open": Ensure connection is established
- "File not found": Check paths and permissions
-## Development
+---
+
+## ๐งช Development
The implementation is built directly from the public ADB protocol documentation (AOSP `SYNC.TXT` and the wire-protocol references), cross-checked against Google's own reference client (`google/python-adb`) where the public docs are ambiguous, and covered by a mocked Vitest suite. The core connection/shell/stream-multiplexing path has real device usage behind it; the newer SYNC-protocol and service additions (`push` / `pull` / `listSync` / `reboot` / `forward` / `install`) have not yet been run against a real device - see the status note at the top of this README and [#1](https://github.com/CLDMV/droidsock/issues/1).
-## License
+---
+
+## ๐ Documentation
-Apache-2.0 - see [LICENSE](LICENSE) for details.
+- **[API Reference](https://github.com/CLDMV/droidsock/blob/master/docs/API.md)** - every method and option, with the experimental and scope caveats
+- **[Protocol Details](https://github.com/CLDMV/droidsock/blob/master/docs/PROTOCOL.md)** - packet structure, auth flow, SYNC sub-protocol framing, reboot / forward service usage
+- **[Changelog](https://github.com/CLDMV/droidsock/tree/master/docs/changelog/)** - per-version release notes
-## Contributing
+[![CodeFactor]][codefactor_url] [![OpenSSF Scorecard]][ossf_scorecard_url] [![npms.io score]][npms_url] [![npm unpacked size]][npm_size_url] [![Repo size]][repo_size_url]
+
+---
+
+## ๐ค Contributing
This is a complete implementation of the ADB protocol. For improvements or bug fixes, please submit issues or pull requests.
+[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
+
+---
+
+## ๐ Links
+
+- **npm**: [@cldmv/droidsock](https://www.npmjs.com/package/@cldmv/droidsock)
+- **GitHub**: [CLDMV/droidsock](https://github.com/CLDMV/droidsock)
+- **Issues**: [GitHub Issues](https://github.com/CLDMV/droidsock/issues)
+- **Changelog**: [docs/changelog/](https://github.com/CLDMV/droidsock/tree/master/docs/changelog/)
+
+---
+
+## ๐ License
+
+[![GitHub license]][github_license_url] [![npm license]][npm_license_url]
+
+Apache-2.0 - see [LICENSE](https://github.com/CLDMV/droidsock/blob/HEAD/LICENSE) for details.
+
[npm version]: https://img.shields.io/npm/v/%40cldmv%2Fdroidsock.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
[npm_version_url]: https://www.npmjs.com/package/@cldmv/droidsock
-[npm downloads]: https://img.shields.io/npm/dm/%40cldmv%2Fdroidsock.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
-[npm_downloads_url]: https://www.npmjs.com/package/@cldmv/droidsock
-[github downloads]: https://img.shields.io/github/downloads/CLDMV/droidsock/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
-[github_downloads_url]: https://github.com/CLDMV/droidsock/releases
[last commit]: https://img.shields.io/github/last-commit/CLDMV/droidsock?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
[last_commit_url]: https://github.com/CLDMV/droidsock/commits
[npm last update]: https://img.shields.io/npm/last-update/%40cldmv%2Fdroidsock?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
[npm_last_update_url]: https://www.npmjs.com/package/@cldmv/droidsock
+[codefactor]: https://img.shields.io/codefactor/grade/github/CLDMV/droidsock?style=for-the-badge&logo=codefactor&logoColor=white&labelColor=F44A6A
+[codefactor_url]: https://www.codefactor.io/repository/github/cldmv/droidsock
+[openssf scorecard]: https://img.shields.io/ossf-scorecard/github.com/CLDMV/droidsock?style=for-the-badge&label=OpenSSF%20Scorecard
+[ossf_scorecard_url]: https://scorecard.dev/viewer/?uri=github.com/CLDMV/droidsock
+[npms.io score]: https://img.shields.io/npms-io/final-score/%40cldmv%2Fdroidsock?style=for-the-badge&logo=npms&logoColor=white&labelColor=0B5D57
+[npms_url]: https://npms.io/search?q=%40cldmv%2Fdroidsock
+[npm downloads]: https://img.shields.io/npm/dm/%40cldmv%2Fdroidsock.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
+[npm_downloads_url]: https://www.npmjs.com/package/@cldmv/droidsock
+[github downloads]: https://img.shields.io/github/downloads/CLDMV/droidsock/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
+[github_downloads_url]: https://github.com/CLDMV/droidsock/releases
+[npm unpacked size]: https://img.shields.io/npm/unpacked-size/%40cldmv%2Fdroidsock.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
+[npm_size_url]: https://www.npmjs.com/package/@cldmv/droidsock
+[repo size]: https://img.shields.io/github/repo-size/CLDMV/droidsock?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
+[repo_size_url]: https://github.com/CLDMV/droidsock
+[github license]: https://img.shields.io/github/license/CLDMV/droidsock.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
+[github_license_url]: https://github.com/CLDMV/droidsock/blob/HEAD/LICENSE
+[npm license]: https://img.shields.io/npm/l/%40cldmv%2Fdroidsock.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
+[npm_license_url]: https://www.npmjs.com/package/@cldmv/droidsock
[coverage]: https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FCLDMV%2Fdroidsock%2Fbadges%2Fcoverage.json&style=for-the-badge&logo=vitest&logoColor=white
[coverage_url]: https://github.com/CLDMV/droidsock/blob/badges/coverage.json
[contributors]: https://img.shields.io/github/contributors/CLDMV/droidsock.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
diff --git a/docs/changelog/v1/v1.1.1.md b/docs/changelog/v1/v1.1.1.md
new file mode 100644
index 0000000..26d3a13
--- /dev/null
+++ b/docs/changelog/v1/v1.1.1.md
@@ -0,0 +1,24 @@
+# DroidSock v1.1.1 Changelog
+
+**Release Date**: September 2026
+**Release Type**: Patch
+
+---
+
+## Overview
+
+v1.1.1 is a documentation-only release. **No runtime code changed**: the shipped `index.mjs`, `index.cjs` and `dist/` files are the same as in v1.1.0.
+
+---
+
+## ๐ Documentation
+
+### Pad slashes between adjacent code spans ([#15](https://github.com/CLDMV/droidsock/pull/15), release [#16](https://github.com/CLDMV/droidsock/pull/16))
+
+Runs of adjacent inline code spans separated by bare slashes (for example `` `mkdir`/`remove`/`move` ``) now have a space on each side of the slash (`` `mkdir` / `remove` / `move` ``) throughout `README.md`, `docs/API.md` and `docs/PROTOCOL.md`. Some Markdown renderers merge unpadded spans into one, which made the method lists hard to read. The wording and the documented API are unchanged.
+
+---
+
+## Upgrade notes
+
+- No breaking changes. This is a drop-in replacement for v1.1.0, and no runtime code changed.
diff --git a/docs/changelog/v2/v2.0.1.md b/docs/changelog/v2/v2.0.1.md
new file mode 100644
index 0000000..6b148ad
--- /dev/null
+++ b/docs/changelog/v2/v2.0.1.md
@@ -0,0 +1,76 @@
+# DroidSock v2.0.1 Changelog
+
+**Release Date**: October 2026
+**Release Type**: Patch
+
+---
+
+## Overview
+
+v2.0.1 is a maintenance release. **No runtime code changed**: `src/`, `index.mjs` and `index.cjs` are the same as in v2.0.0, so the API behaves exactly as before.
+
+The one change a consumer can see is the declared Node.js floor: `engines.node` rose from `>=20.19.0` to `>=22.12.0`, despite this being a patch release. It was raised to match the test toolchain (vitest 5), not because the library's own code needs a newer Node.js. See Breaking Changes below for who it affects.
+
+The rest of the release moves the test toolchain to vitest 5, syncs the CI and release workflows with the `CLDMV/.github` v4.29.2 templates, restores the verbatim Apache-2.0 license text, and makes the test suite more reliable under load.
+
+---
+
+## ๐ฅ Breaking Changes
+
+### `engines.node` raised to `>=22.12.0` ([#38](https://github.com/CLDMV/droidsock/pull/38))
+
+The package's declared Node.js floor moved from `>=20.19.0` to `>=22.12.0` in a patch release. The library code did not change, so it still runs on Node.js 20.19 and later, but the declaration now excludes Node.js 20.19 to 22.11:
+
+- With npm's default settings, installing on those versions prints an `EBADENGINE` warning and continues.
+- With `engine-strict=true` (or a package manager configured to enforce `engines`), installing on those versions fails.
+
+**Upgrade step:** run on Node.js 22.12.0 or later. Projects that must stay on Node.js 20.19 to 22.11 can pin `@cldmv/droidsock@2.0.0`, which has the same runtime code.
+
+## ๐ง CI & tooling
+
+### Test toolchain moved to vitest 5 ([#36](https://github.com/CLDMV/droidsock/pull/36), [#38](https://github.com/CLDMV/droidsock/pull/38))
+
+`vitest` and `@vitest/coverage-v8` moved to 5.x together. vitest 5 needs Node.js `^22.12.0 || ^24.0.0 || >=26.0.0`, so the CI test matrix now runs from Node.js 22.12.0 up to 26.
+
+### Sync the v4 workflows with the CLDMV/.github v4.29.2 templates ([#46](https://github.com/CLDMV/droidsock/pull/46))
+
+Every workflow caller under `.github/workflows/` now matches the v4.29.2 templates. New callers are `release-merge.yml` (squash-merges the approved `next โ master` release PR with its curated body as the commit message), `member-auto-merge.yml`, `dependabot-recreate.yml`, `provenance.yml` (SLSA build provenance for published releases), `pr-notify.yml` and `bundle-size.yml`, which reports how the published files change in size on each PR. The existing callers and `.github/dependabot.yml` were refreshed to the same template version. The same PR stops the bundle-size check from counting files twice.
+
+### Required PR Check no longer satisfied by a skipped run ([#50](https://github.com/CLDMV/droidsock/pull/50))
+
+On a PR from a branch in this repository, the `pull_request` run's copy of the `โ
Required PR Check` mirror job was skipped, because the push run reports the check for the same commit. GitHub counts a skipped job as passing for a required check, so a PR could look mergeable while its tests were still running. The skipped job now shows under a different name, so only the push run's result satisfies the ruleset.
+
+### Grouped Dependabot bumps ([#39](https://github.com/CLDMV/droidsock/pull/39))
+
+`vitest` + `@vitest/*`, the eslint family and the prettier family each bump in one grouped PR. These packages peer each other with major-locked ranges, so separate PRs broke `npm ci` with `ERESOLVE` whenever one moved without the others.
+
+### More reliable tests under load ([#48](https://github.com/CLDMV/droidsock/pull/48))
+
+A new Vitest setup file (`tests/setup/warm-droidsock.mjs`) composes and shuts down one droidsock instance before each test file is collected. The first instance in a file pays a one-time cold cost that was measured at 7 to 11 seconds on a busy machine, which raced the 10-second hook timeout and failed whichever test happened to run first. The device and connection tests also now assert against the real session instead of slothlet's mirror views.
+
+## ๐ License
+
+- `LICENSE` once again carries the verbatim Apache-2.0 text ([#45](https://github.com/CLDMV/droidsock/pull/45)). The license itself did not change.
+
+## ๐ง Dependencies
+
+Runtime:
+
+- `@cldmv/slothlet`: the lockfile moves from 3.15.0 to 3.20.0 (patch group bumps, including [#40](https://github.com/CLDMV/droidsock/pull/40)). The declared range stays `^3.15.0`, so consumers resolve whatever 3.x their own install picks.
+
+Dev-only:
+
+- `vitest` 4.1.11 โ 5.0.2 and `@vitest/coverage-v8` 4.1.11 โ 5.0.2 ([#36](https://github.com/CLDMV/droidsock/pull/36))
+- `@eslint/css` 1.4.0 โ 2.0.0 ([#42](https://github.com/CLDMV/droidsock/pull/42))
+- `@types/node` 26.4.0 โ 26.6.3, `eslint` 10.9.1 โ 10.11.0, `@eslint/json` 2.0.1 โ 2.1.0, `globals` 17.11.0 โ 17.12.0, `prettier` 3.9.6 โ 3.9.9, `@cldmv/fix-headers` 1.3.10 โ 1.3.12, `@cldmv/jsonv` 1.0.2 โ 1.0.9, `@cldmv/eslint-plugin-jsonv` 1.0.3 โ 1.0.10 and `@cldmv/prettier-plugin-jsonv` 1.0.1 โ 1.0.6 (grouped bumps [#32](https://github.com/CLDMV/droidsock/pull/32), [#33](https://github.com/CLDMV/droidsock/pull/33), [#41](https://github.com/CLDMV/droidsock/pull/41), [#43](https://github.com/CLDMV/droidsock/pull/43), [#44](https://github.com/CLDMV/droidsock/pull/44))
+
+## ๐ Documentation
+
+- **NEW:** [docs/changelog/v2/v2.0.1.md](./v2.0.1.md): this changelog, added after the release.
+
+---
+
+## Upgrade notes
+
+- The runtime code is identical to [v2.0.0](./v2.0.0.md).
+- Run on Node.js 22.12.0 or later to match the new `engines.node` declaration, or pin v2.0.0 if you need Node.js 20.19 to 22.11 with strict engine checks.
diff --git a/docs/changelog/v2/v2.0.2.md b/docs/changelog/v2/v2.0.2.md
new file mode 100644
index 0000000..0c5331c
--- /dev/null
+++ b/docs/changelog/v2/v2.0.2.md
@@ -0,0 +1,45 @@
+# DroidSock v2.0.2 Changelog
+
+**Release Date**: October 2026
+**Release Type**: Patch
+
+---
+
+## Overview
+
+v2.0.2 changes only development tooling and CI. **No runtime code changed**: the shipped `index.mjs`, `index.cjs` and `dist/` files differ from v2.0.1 only in their file-header comments, so the API behaves exactly as before.
+
+The release moves header maintenance onto the shared CLDMV `@cldmv/fix-headers` configuration, makes the required PR check report a real result on in-repo PRs, and brings in a set of dependency bumps.
+
+---
+
+## ๐ง CI & tooling
+
+### Shared fix-headers configuration ([#54](https://github.com/CLDMV/droidsock/pull/54))
+
+`npm run fix:headers` now runs the `fix-headers` CLI directly against a checked-in `.configs/fix-headers.json`, which extends `@cldmv/configs/fix-headers.json`. The local `tools/fix-headers.mjs` wrapper is gone. Running the shared config once rewrote the file headers across the repository into the uniform CLDMV format, which is why almost every file shows a header-only change in this release.
+
+### Run the in-repo PR mirror job instead of skipping it ([#55](https://github.com/CLDMV/droidsock/pull/55))
+
+v2.0.1 renamed the skipped copy of the `โ
Required PR Check` mirror job so it couldn't satisfy the ruleset. The job now always runs instead, and exits early with a note when the push run owns the status for that commit, so it never reports as skipped at all.
+
+## ๐ง Dependencies
+
+Runtime:
+
+- `@cldmv/slothlet`: the lockfile moves from 3.20.0 to 3.21.0 ([#57](https://github.com/CLDMV/droidsock/pull/57) / [#51](https://github.com/CLDMV/droidsock/pull/51)). The declared range stays `^3.15.0`.
+
+Dev-only:
+
+- `@cldmv/fix-headers` 1.3.12 โ 2.1.2, plus `@cldmv/configs` 1.2.1 added for the shared config ([#54](https://github.com/CLDMV/droidsock/pull/54))
+- `@cldmv/vitest-runner` 1.2.0 โ 1.5.1, `@cldmv/jsonv` 1.0.9 โ 1.1.1, `@cldmv/prettier-plugin-jsonv` 1.0.6 โ 1.1.0 and `@cldmv/eslint-plugin-jsonv` 1.0.10 โ 1.0.13 (grouped bumps [#51](https://github.com/CLDMV/droidsock/pull/51), [#57](https://github.com/CLDMV/droidsock/pull/57))
+
+## ๐ Documentation
+
+- **NEW:** [docs/changelog/v2/v2.0.2.md](./v2.0.2.md): this changelog, added after the release.
+
+---
+
+## Upgrade notes
+
+- No breaking changes. This is a drop-in replacement for v2.0.1, and no runtime code changed.
diff --git a/docs/changelog/v2/v2.0.3.md b/docs/changelog/v2/v2.0.3.md
new file mode 100644
index 0000000..db20c53
--- /dev/null
+++ b/docs/changelog/v2/v2.0.3.md
@@ -0,0 +1,60 @@
+# DroidSock v2.0.3 Changelog
+
+**Release Date**: October 2026
+**Release Type**: Patch
+**Branch**: `release/2.0.3`
+
+---
+
+## Overview
+
+v2.0.3 cleans up the CommonJS entry and the published file list. `index.cjs` now loads the ESM entry with a plain `require()`, which bundlers can follow, and fails with a clear message on Node.js versions that can't `require()` ES modules. The ESM entry and the API itself are unchanged.
+
+The release also stops publishing `devcheck.mjs`, a source-checkout-only development check, and removes its `./devcheck` subpath export. Removing an export is technically a breaking change despite this being a patch release, so it's listed under Breaking Changes below. In practice the module did nothing in an installed copy of the package.
+
+---
+
+## ๐ฅ Breaking Changes
+
+### `@cldmv/droidsock/devcheck` is no longer exported or published ([#58](https://github.com/CLDMV/droidsock/pull/58))
+
+`package.json` no longer lists a `./devcheck` export, and `devcheck.mjs` and `types/devcheck.d.mts` are no longer in the published files. Importing `@cldmv/droidsock/devcheck` now fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
+
+The module was never part of the documented API. It has no exports; its only job is to warn a developer working in a source checkout who forgot to set `NODE_OPTIONS=--conditions=droidsock-dev`. It only acts when a `src/` folder sits next to it, and the published package has no `src/`, so importing it from an installed copy did nothing. `index.mjs` still loads it fire-and-forget and already tolerates it being missing, so the main entry is unaffected.
+
+**Upgrade step:** if anything imports `@cldmv/droidsock/devcheck`, delete that import. Nothing replaces it, because it never did anything outside this repository.
+
+## ๐ Bug Fixes
+
+### Require the ESM entry directly, and fail clearly without `require(esm)` ([#58](https://github.com/CLDMV/droidsock/pull/58))
+
+`index.cjs` used `createRequire(__filename)` to load `index.mjs`. That idiom predates Node's native `require(esm)`, and bundlers such as esbuild and webpack don't follow a `createRequire`-constructed `require` the way they follow a literal `require()` call, so a CommonJS build that bundled droidsock could miss the ESM entry.
+
+- `index.cjs` now calls `require("./index.mjs")` directly and exports the same functions as before (`module.exports` is the `droidsock` quick path, with `createDroidSock`, `DroidSock`, `ADB` and `AndroidDebugBridge` as properties).
+- On a Node.js version without `require(esm)` (before 20.19.0, or 22.0.0 to 22.11.x), `index.cjs` throws an `ERR_REQUIRE_ESM` error whose message names the supported versions and points to `import()`, instead of a bare loader error. The package's `engines.node` is already `>=22.12.0`, so this only matters for installs that ignore `engines`.
+- `index.mjs` already avoided top-level `await`, so it needed no change.
+- New `tests/cjs/entry.test.cjs` checks run under Node's own test runner after Vitest, from both `npm test` and `npm run coverage` (through the new `test:cjs` script). They check that `require()` returns the same functions as `import`, and that the version check fires when `require(esm)` is turned off. They don't call `droidsock()`, since that opens a real ADB connection.
+
+## ๐ง CI & tooling
+
+- `bundle-size.yml` no longer lists the `devcheck` files in `dist_paths`, matching the new published file list ([#58](https://github.com/CLDMV/droidsock/pull/58)).
+
+## ๐ Documentation
+
+- **NEW:** [docs/changelog/v2/v2.0.3.md](./v2.0.3.md): this changelog.
+- **NEW:** backfilled [v1.1.1](../v1/v1.1.1.md), [v2.0.1](./v2.0.1.md) and [v2.0.2](./v2.0.2.md).
+- README restructured to the standard CLDMV layout, with a new Requirements section that states the Node.js floor for `import` and `require()`.
+
+## ๐ง Dependencies
+
+Both changes are to development dependencies; the package has no runtime dependency changes and the published package is unaffected. Only `package.json` and the lockfile changed in these bumps, and no file headers were restamped.
+
+- `@cldmv/fix-headers` `^2.1.2` โ `^2.2.0` ([#61](https://github.com/CLDMV/droidsock/pull/61) took it to `^2.1.4`, [#63](https://github.com/CLDMV/droidsock/pull/63) to `^2.2.0`). Version 2.1.4 no longer writes a JavaScript comment into JSON or Markdown files, processes every repeated `--input`, and never walks dependency folders such as `node_modules`. Version 2.2.0 makes the `@Last modified by` header tag follow content edits only, so a header-only rewrite keeps the recorded editor instead of replacing it. It requires Node.js `>=22.12.0`, which matches the package's `engines.node`.
+- `@cldmv/configs` `^1.2.1` โ `^1.2.4` ([#63](https://github.com/CLDMV/droidsock/pull/63)). It provides the shared `fix-headers` configuration that `.configs/fix-headers.json` extends. Version 1.2.4 turns off `forceAuthorUpdate` and `forceLastModifiedAuthorUpdate` (both were on in 1.2.1), so the shared configuration no longer overwrites the recorded author or last editor.
+
+---
+
+## Upgrade notes
+
+- If anything imports `@cldmv/droidsock/devcheck`, remove that import. Nothing else needs to change.
+- `import droidsock from "@cldmv/droidsock"` and `require("@cldmv/droidsock")` both work as before on Node.js 22.12.0 or later.
diff --git a/index.cjs b/index.cjs
index 09de4f3..520f2b7 100644
--- a/index.cjs
+++ b/index.cjs
@@ -21,11 +21,20 @@
*
* @module droidsock
*/
+"use strict";
-const { createRequire } = require("module");
-const requireESM = createRequire(__filename);
+// index.cjs is a thin wrapper: it loads index.mjs through Node's synchronous require(esm).
+// Node.js versions without require(esm) would fail with a bare ERR_REQUIRE_ESM, so fail
+// early with a message that says what to do instead.
+if (!process.features?.require_module) {
+ const error = new Error(
+ `@cldmv/droidsock: require() needs Node.js ^20.19.0 or >=22.12.0 (this is ${process.version}). On older Node.js, load the package with import() instead.`
+ );
+ error.code = "ERR_REQUIRE_ESM";
+ throw error;
+}
-const { default: droidsock } = requireESM("./index.mjs");
+const { default: droidsock } = require("./index.mjs");
// Export main function - the quick path, also callable with options
module.exports = droidsock; // Default export
diff --git a/package-lock.json b/package-lock.json
index 46cdb9f..0f29cd5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@cldmv/droidsock",
- "version": "2.0.2",
+ "version": "2.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@cldmv/droidsock",
- "version": "2.0.2",
+ "version": "2.0.3",
"license": "Apache-2.0",
"dependencies": {
"@cldmv/slothlet": "^3.15.0",
@@ -14,9 +14,9 @@
"selfsigned": "^5.5.0"
},
"devDependencies": {
- "@cldmv/configs": "^1.2.1",
+ "@cldmv/configs": "^1.2.4",
"@cldmv/eslint-plugin-jsonv": "^1.0.3",
- "@cldmv/fix-headers": "^2.1.2",
+ "@cldmv/fix-headers": "^2.2.0",
"@cldmv/jsonv": "^1.0.2",
"@cldmv/prettier-plugin-jsonv": "^1.0.1",
"@cldmv/vitest-runner": "^1.2.0",
@@ -126,9 +126,9 @@
}
},
"node_modules/@cldmv/configs": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@cldmv/configs/-/configs-1.2.1.tgz",
- "integrity": "sha512-R3GhDwdTqJwuRZ8/kVlUew0ezhZPxdFSJXky8jzfbzQEXDYQLZgPILSwzzKYHgRKu9P2ghXznzYKRPrsImiTNg==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@cldmv/configs/-/configs-1.2.4.tgz",
+ "integrity": "sha512-7HPqAgCKqol3fHpawEsXJ5ZqHxlDPZk1puoFu43f/gaNfhWwufDTzjkvLk90yVEumyz4N3pUwIxfbHUkUTpDEg==",
"dev": true,
"license": "Apache-2.0",
"funding": {
@@ -159,9 +159,9 @@
}
},
"node_modules/@cldmv/fix-headers": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/@cldmv/fix-headers/-/fix-headers-2.1.2.tgz",
- "integrity": "sha512-1OnUKIkFRMJfJJ4ypyBHsb6mqbCV4q77M/JcYunuB4Ibbg1/6bXK1qCFev5+UWbhgtpPKmiy2VfPCHpf4P1ToQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cldmv/fix-headers/-/fix-headers-2.2.0.tgz",
+ "integrity": "sha512-EQTAKCo0B639q2bde+vO5ciYbtvNgAJFm0DBthIJQnmTFJmQDUlObp5EsTRgg5CUlFoUnGMX+q35LC02QvYU4w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
diff --git a/package.json b/package.json
index 6544d06..40932ea 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@cldmv/droidsock",
- "version": "2.0.2",
+ "version": "2.0.3",
"description": "Complete Node.js implementation of the Android Debug Bridge (ADB) protocol",
"main": "./index.cjs",
"module": "./index.mjs",
@@ -11,10 +11,6 @@
"import": "./index.mjs",
"require": "./index.cjs"
},
- "./devcheck": {
- "types": "./types/devcheck.d.mts",
- "import": "./devcheck.mjs"
- },
"./main": {
"droidsock-dev": {
"types": "./types/src/droidsock.d.mts",
@@ -32,10 +28,11 @@
"build": "node build.mjs",
"build:types": "tsc --project .configs/tsconfig.dts.jsonc",
"build:ci": "npm run build && npm run build:types && npm run test:types",
- "test": "node tests/run-vitest.mjs",
+ "test": "node tests/run-vitest.mjs && npm run test:cjs",
+ "test:cjs": "CI=1 node --test tests/cjs/entry.test.cjs",
"test:watch": "vitest --config .configs/vitest.config.mjs",
"test:types": "tsc --noEmit --project .configs/tsconfig.dts.jsonc",
- "coverage": "node tests/run-vitest.mjs --coverage-quiet",
+ "coverage": "node tests/run-vitest.mjs --coverage-quiet && npm run test:cjs",
"ci:coverage": "npm run coverage",
"lint": "eslint --config .configs/eslint.config.mjs .",
"lint:fix": "eslint --config .configs/eslint.config.mjs . --fix",
@@ -94,20 +91,18 @@
"files": [
"index.mjs",
"index.cjs",
- "devcheck.mjs",
"README.md",
"LICENSE",
"types/dist/",
"types/index.d.mts",
"types/index.d.mts.map",
- "types/devcheck.d.mts",
"dist/"
],
"sideEffects": false,
"devDependencies": {
- "@cldmv/configs": "^1.2.1",
+ "@cldmv/configs": "^1.2.4",
"@cldmv/eslint-plugin-jsonv": "^1.0.3",
- "@cldmv/fix-headers": "^2.1.2",
+ "@cldmv/fix-headers": "^2.2.0",
"@cldmv/jsonv": "^1.0.2",
"@cldmv/prettier-plugin-jsonv": "^1.0.1",
"@cldmv/vitest-runner": "^1.2.0",
diff --git a/tests/cjs/entry.test.cjs b/tests/cjs/entry.test.cjs
new file mode 100644
index 0000000..d65442e
--- /dev/null
+++ b/tests/cjs/entry.test.cjs
@@ -0,0 +1,56 @@
+/**
+ *
+ * @Project: @cldmv/droidsock
+ * @Filename: /tests/cjs/entry.test.cjs
+ * @Date: 2026-10-03T00:00:00-07:00 (1791010800)
+ * @Author: Nate Corcoran
+ * @Email:
+ * -----
+ * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com)
+ * @Last modified time: 2026-10-03T10:41:45-07:00 (1791049305)
+ * -----
+ * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
+ *
+ */
+
+/**
+ * CommonJS entry tests. These run under Node's own test runner (`node --test`), not Vitest:
+ * Vitest loads files through its own module runner, so it cannot show whether a plain
+ * `require()` of the package works the way it does for a CommonJS consumer.
+ */
+"use strict";
+
+const { test } = require("node:test");
+const assert = require("node:assert/strict");
+const { spawnSync } = require("node:child_process");
+const path = require("node:path");
+
+const repoRoot = path.resolve(__dirname, "../..");
+
+test("require() returns the same droidsock object as import", async () => {
+ const droidsock = require("../../index.cjs");
+ const esm = await import("../../index.mjs");
+
+ // droidsock() opens a real ADB connection when called, so only identity/type is
+ // checked here - no sockets are opened in this test.
+ assert.equal(typeof droidsock, "function");
+ assert.equal(droidsock, esm.default);
+ assert.equal(droidsock.createDroidSock, esm.createDroidSock);
+ assert.equal(droidsock.DroidSock, esm.DroidSock);
+ assert.equal(droidsock.ADB, esm.ADB);
+ assert.equal(droidsock.AndroidDebugBridge, esm.AndroidDebugBridge);
+});
+
+test("require() fails with a clear message where Node.js has no require(esm)", () => {
+ // --no-experimental-require-module turns require(esm) off, which is what Node.js
+ // versions before 20.19 / 22.12 look like to the entry.
+ const res = spawnSync(process.execPath, ["--no-experimental-require-module", "-e", "require('./index.cjs')"], {
+ cwd: repoRoot,
+ encoding: "utf8"
+ });
+
+ assert.notEqual(res.status, 0);
+ assert.match(res.stderr, /ERR_REQUIRE_ESM/);
+ assert.match(res.stderr, /require\(\) needs Node\.js \^20\.19\.0 or >=22\.12\.0/);
+ assert.match(res.stderr, /import\(\)/);
+});