adb-unlock 0.0.0-stage → 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/dist/cli.js +480 -0
  4. package/package.json +24 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MusiCode1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # adb-unlock
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Inject a **known** pattern / PIN / password into a **remote** Android device over
4
+ **ADB or SSH**, dismissing the lock screen without touching it.
5
+
6
+ Pattern coordinates are derived **live from the accessibility tree**
7
+ (`uiautomator dump`), not hardcoded — so it works across resolutions and OEM skins
8
+ (stock `lockPatternView`, OneUI / OxygenOS `colorLockPatternView`), and even when the
9
+ touchscreen is dead (`input` injects above the hardware layer).
10
+
11
+ > For **your own** devices. Wrong attempts count toward the lockout; when root is
12
+ > available the tool runs a `locksettings verify` pre-check and stops if the stored
13
+ > value is wrong. See also [prior art](./docs/prior-art.md) — this idea exists in
14
+ > other forms; the differentiators here are dynamic coordinates + SSH transport.
15
+
16
+ 🔢 **Landing page** with a pattern → digit-sequence converter: `web/` (SvelteKit).
17
+
18
+ ## Install / run
19
+
20
+ ```bash
21
+ npx adb-unlock -s <serial> --pattern
22
+ ```
23
+
24
+ Everything is passed via flags (no positional); the default transport is **adb**.
25
+ Requires `adb` and/or `ssh` on PATH. No install needed via `npx`.
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ # ADB (default), explicit serial like `adb -s`; a bare --pattern prompts hidden
31
+ npx adb-unlock -s 10.0.0.5:5555 --pattern
32
+
33
+ # ANDROID_SERIAL env (adb's native var) is honored
34
+ ANDROID_SERIAL=10.0.0.5:5555 npx adb-unlock --pattern 583
35
+
36
+ # SSH transport (e.g. Termux over a VPN) — selected by --ssh
37
+ npx adb-unlock --ssh my-tablet-alias --pin
38
+
39
+ # Registry entry from devices.json (also default --bw-item)
40
+ npx adb-unlock --device my-tablet
41
+
42
+ # List connected adb devices
43
+ npx adb-unlock --list
44
+
45
+ # Recon only / remove the lock (needs root)
46
+ npx adb-unlock -s SERIAL --pattern 583 --dry-run
47
+ npx adb-unlock -s SERIAL --remove-lock
48
+ ```
49
+
50
+ ### Pattern numbering
51
+
52
+ Dots are numbered **1-9 by position**, left-to-right, top-to-bottom
53
+ (1 = top-left, 9 = bottom-right) — the same order Android's grid uses. This is **not**
54
+ the order you drew them. The landing page converter turns a drawn pattern into this
55
+ sequence for you.
56
+
57
+ ### Device selection
58
+
59
+ Everything is a flag — there is no positional argument.
60
+
61
+ - **ADB (default):** `-s` / `--serial`, or `$ANDROID_SERIAL`. With multiple devices
62
+ connected and no serial, the tool lists them and asks you to choose (same as `adb`).
63
+ - **SSH:** `--ssh <alias>` selects the ssh transport.
64
+ - **Registry:** `--device <name>` looks the device up in `devices.json` (and is the
65
+ default `--bw-item`).
66
+
67
+ ## Credential sources (in priority order)
68
+
69
+ 1. Direct flag with a value: `--pattern 583` / `--pin 4721` / `--password hunter2`.
70
+ 2. Bare flag → hidden prompt: `--pattern` (nothing printed, nothing in shell history).
71
+ 3. Bitwarden (optional): an item per device with hidden fields
72
+ `screen-lock-pattern` / `screen-lock-pin` / `screen-lock-password`
73
+ (needs the `bw-mcp-cli` wrapper and an unlocked vault).
74
+
75
+ ## Device registry (optional) — `devices.json`
76
+
77
+ Non-secret: transport + connection + shell wrapper. See `devices.json.example`.
78
+ Path from `--devices`, or `$ADB_UNLOCK_DEVICES`, or `./devices.json`.
79
+
80
+ ## Transports
81
+
82
+ | transport | how |
83
+ |---|---|
84
+ | `ssh` | `ssh <alias> "<wrap> sh"` |
85
+ | `adb` | `adb -s <serial> shell "<wrap> sh"` (auto-runs `adb connect` for `ip:port` serials) |
86
+ | `adb` via a jump host | `ssh <host> "[pct exec <ct> --] adb -s <serial> shell <wrap> sh"` |
87
+
88
+ `<wrap>` = `su -c` (root), `$HOME/rish -c` (Shizuku), or none — chosen by `--via`
89
+ (default `auto`). Root is needed only for the `verify` pre-check and `--remove-lock`.
90
+
91
+ ## Safety
92
+
93
+ - Wrong pattern/PIN attempts count toward the 5-try lockout — root `locksettings verify`
94
+ pre-check stops before injecting a wrong value (`--no-verify` to skip).
95
+ - Never injects during an active call (unless `--force`).
96
+ - Confirms success via `isKeyguardShowing=false`, not assumption.
97
+ - The credential is never printed or written to disk; `logcat` is cleared afterward.
98
+
99
+ ## Architecture
100
+
101
+ Functional core / imperative shell: `src/core/` is pure (grid math, script builders,
102
+ output parsing, transport argv) with unit tests; `src/shell/` does IO (`child_process`
103
+ for ssh/adb, Bitwarden, hidden prompt); `src/cli.ts` orchestrates.
104
+
105
+ ```bash
106
+ npm test # vitest on core/
107
+ npm run build # esbuild -> dist/cli.js
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+ import { setTimeout as sleep } from "node:timers/promises";
6
+ import { readFileSync, existsSync } from "node:fs";
7
+
8
+ // src/core/bouncer.ts
9
+ var PATTERN_IDS = ["lockPatternView", "colorLockPatternView", "oplusos_kgd_security_pattern_view"];
10
+ function squarish(b) {
11
+ const w = b[2] - b[0], h = b[3] - b[1];
12
+ return w > 0 && h > 0 && Math.abs(w - h) <= 0.25 * Math.max(w, h);
13
+ }
14
+ function patternGridFromDump(xml) {
15
+ const cands = [];
16
+ for (const m of xml.matchAll(/<node\b[^>]*>/g)) {
17
+ const s = m[0];
18
+ const rid = (/resource-id="([^"]*)"/.exec(s)?.[1] ?? "").toLowerCase();
19
+ const desc = /content-desc="([^"]*)"/.exec(s)?.[1] ?? "";
20
+ const isGrid = rid.endsWith("lockpatternview");
21
+ const hit = isGrid || PATTERN_IDS.some((p) => rid.endsWith(p.toLowerCase())) || desc.toLowerCase().includes("pattern") || desc.includes("\u05EA\u05D1\u05E0\u05D9\u05EA");
22
+ if (!hit) continue;
23
+ const b = /bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(s);
24
+ if (b) cands.push({ grid: isGrid, b: [Number(b[1]), Number(b[2]), Number(b[3]), Number(b[4])] });
25
+ }
26
+ return cands.find((c) => c.grid && squarish(c.b))?.b ?? cands.find((c) => squarish(c.b))?.b ?? null;
27
+ }
28
+ function patternPoints(b) {
29
+ const [L, T, R, B] = b;
30
+ const w = (R - L) / 3, h = (B - T) / 3;
31
+ const pts = {};
32
+ for (let d = 1; d <= 9; d++) {
33
+ const i = d - 1, c = i % 3, r = Math.floor(i / 3);
34
+ pts[d] = [Math.trunc(L + (c + 0.5) * w), Math.trunc(T + (r + 0.5) * h)];
35
+ }
36
+ return pts;
37
+ }
38
+ function parsePatternDigits(cred) {
39
+ return [...cred].filter((c) => c >= "1" && c <= "9").map(Number);
40
+ }
41
+ function buildPatternGesture(points, digits) {
42
+ const lines = [];
43
+ digits.forEach((d, k) => {
44
+ const [x, y] = points[d];
45
+ lines.push(`input motionevent ${k === 0 ? "DOWN" : "MOVE"} ${x} ${y}`);
46
+ lines.push("sleep 0.08");
47
+ });
48
+ const [lx, ly] = points[digits[digits.length - 1]];
49
+ lines.push(`input motionevent UP ${lx} ${ly}`);
50
+ return lines;
51
+ }
52
+ function buildTextEntry(text) {
53
+ const esc = text.replace(/ /g, "%s");
54
+ return [`input text '${esc}'`, "sleep 0.3", "input keyevent KEYCODE_ENTER"];
55
+ }
56
+ var PROBE = {
57
+ pattern: "lockpatternview",
58
+ text: "passwordentry|pinentry|numpadkey|keyguard_pin|keyguard_password"
59
+ };
60
+ function raiseLoopScript(size, kind = "pattern") {
61
+ const x = size ? Math.round(size.w / 2) : 600;
62
+ const y1 = size ? Math.round(size.h * 0.8) : 1600;
63
+ const y2 = size ? Math.round(size.h * 0.2) : 400;
64
+ const check = `uiautomator dump /sdcard/.au.xml >/dev/null 2>&1; grep -qiE '${PROBE[kind]}' /sdcard/.au.xml && break`;
65
+ return [
66
+ "for i in 1 2 3 4; do",
67
+ " input keyevent KEYCODE_WAKEUP; sleep 0.4",
68
+ " wm dismiss-keyguard; sleep 1.0",
69
+ ` ${check}`,
70
+ ` input swipe ${x} ${y1} ${x} ${y2} 150; sleep 0.8`,
71
+ ` ${check}`,
72
+ " sleep 0.3",
73
+ "done",
74
+ ""
75
+ ].join("\n");
76
+ }
77
+
78
+ // src/core/recon.ts
79
+ function reconScript() {
80
+ return [
81
+ "echo KG=$(dumpsys window 2>/dev/null | grep -m1 -o 'isKeyguardShowing=[a-z]*')",
82
+ "echo CALL=$(dumpsys telephony.registry 2>/dev/null | grep -m1 -o 'mCallState=[0-9]*')",
83
+ "echo SIZE=$(wm size 2>/dev/null | grep -m1 -o '[0-9]*x[0-9]*' | head -1)",
84
+ "echo TOP=$(dumpsys activity activities 2>/dev/null | grep -m1 -o 'topResumedActivity=[^ ]*')"
85
+ ].join("\n") + "\n";
86
+ }
87
+ function parseRecon(out) {
88
+ const kg = /isKeyguardShowing=([a-z]*)/.exec(out)?.[1] ?? "";
89
+ const call = /mCallState=([0-9]*)/.exec(out)?.[1] ?? "0";
90
+ const size = /SIZE=([0-9x]*)/.exec(out)?.[1] ?? "";
91
+ const top = /TOP=(\S*)/.exec(out)?.[1] ?? "";
92
+ return { keyguard: kg === "true", call: !(call === "" || call === "0"), size, top };
93
+ }
94
+ function parseSize(size) {
95
+ const m = /^(\d+)x(\d+)$/.exec(size);
96
+ return m ? { w: Number(m[1]), h: Number(m[2]) } : null;
97
+ }
98
+
99
+ // src/core/credential.ts
100
+ var CRED_FIELDS = {
101
+ pattern: "screen-lock-pattern",
102
+ pin: "screen-lock-pin",
103
+ password: "screen-lock-password"
104
+ };
105
+ function resolveCredentialType(fields, requested) {
106
+ if (requested) {
107
+ const v = fields[CRED_FIELDS[requested]];
108
+ if (!v) throw new Error(`item has no field '${CRED_FIELDS[requested]}'`);
109
+ return { type: requested, value: v };
110
+ }
111
+ for (const t of ["pattern", "pin", "password"]) {
112
+ const v = fields[CRED_FIELDS[t]];
113
+ if (v) return { type: t, value: v };
114
+ }
115
+ throw new Error("no screen-lock field (screen-lock-pattern/-pin/-password) in item");
116
+ }
117
+
118
+ // src/core/schema.ts
119
+ var TRANSPORTS = ["ssh", "adb"];
120
+ var VIAS = ["auto", "root", "rish", "none"];
121
+ function parseRegistry(raw) {
122
+ const data = JSON.parse(raw);
123
+ if (typeof data !== "object" || data === null) throw new Error("devices.json is not an object");
124
+ for (const [k, v] of Object.entries(data)) {
125
+ if (v.transport && !TRANSPORTS.includes(v.transport)) throw new Error(`${k}: invalid transport`);
126
+ if (v.via && !VIAS.includes(v.via)) throw new Error(`${k}: invalid via`);
127
+ }
128
+ return data;
129
+ }
130
+
131
+ // src/shell/exec.ts
132
+ import { spawnSync } from "node:child_process";
133
+
134
+ // src/core/transport.ts
135
+ function chooseWrap(d, via) {
136
+ switch (via) {
137
+ case "root":
138
+ return "su -c";
139
+ case "rish":
140
+ return "$HOME/rish -c";
141
+ case "none":
142
+ return "";
143
+ default:
144
+ return d.root ? "su -c" : d.rish ? "$HOME/rish -c" : "";
145
+ }
146
+ }
147
+ function remoteShell(wrap) {
148
+ return wrap ? `${wrap} sh` : "sh";
149
+ }
150
+ function buildInvocation(name, profile, wrap, opts = {}) {
151
+ const transport = opts.transport ?? profile.transport ?? "ssh";
152
+ const remote = remoteShell(wrap);
153
+ if (transport === "adb") {
154
+ const serial = opts.serial ?? profile.adb?.serial;
155
+ const target = serial ? ["-s", serial] : [];
156
+ if (profile.adb?.sshHost) {
157
+ const pct = profile.adb.ct ? `pct exec ${profile.adb.ct} -- ` : "";
158
+ const s = serial ? `-s ${serial} ` : "";
159
+ return ["ssh", "-o", "ConnectTimeout=12", profile.adb.sshHost, `${pct}adb ${s}shell ${remote}`];
160
+ }
161
+ return ["adb", ...target, "shell", remote];
162
+ }
163
+ const alias = profile.ssh ?? name;
164
+ return ["ssh", "-o", "ConnectTimeout=12", alias, remote];
165
+ }
166
+ function buildAdbConnect(profile, opts = {}) {
167
+ const transport = opts.transport ?? profile.transport ?? "ssh";
168
+ if (transport !== "adb") return null;
169
+ const serial = opts.serial ?? profile.adb?.serial;
170
+ if (!serial || !serial.includes(":")) return null;
171
+ if (profile.adb?.sshHost) {
172
+ const pct = profile.adb.ct ? `pct exec ${profile.adb.ct} -- ` : "";
173
+ return ["ssh", "-o", "ConnectTimeout=12", profile.adb.sshHost, `${pct}adb connect ${serial}`];
174
+ }
175
+ return ["adb", "connect", serial];
176
+ }
177
+
178
+ // src/shell/exec.ts
179
+ var Device = class {
180
+ constructor(name, profile, opts = {}) {
181
+ this.name = name;
182
+ this.profile = profile;
183
+ this.opts = opts;
184
+ }
185
+ name;
186
+ profile;
187
+ opts;
188
+ wrap = "";
189
+ _root = false;
190
+ get root() {
191
+ return this._root;
192
+ }
193
+ spawn(argv, script) {
194
+ const r = spawnSync(argv[0], argv.slice(1), {
195
+ input: script,
196
+ encoding: "utf8",
197
+ maxBuffer: 16 * 1024 * 1024
198
+ });
199
+ return (r.stdout ?? "") + (r.stderr ?? "");
200
+ }
201
+ runWith(wrap, script) {
202
+ return this.spawn(buildInvocation(this.name, this.profile, wrap, this.opts), script);
203
+ }
204
+ sh(script, o) {
205
+ return this.runWith(o?.asRoot ? "su -c" : this.wrap, script);
206
+ }
207
+ detect() {
208
+ const conn = buildAdbConnect(this.profile, this.opts);
209
+ if (conn) this.spawn(conn);
210
+ const out = this.runWith(
211
+ "",
212
+ "su -c id 2>/dev/null\ntest -x $HOME/rish && echo RISH=yes || echo RISH=no\n"
213
+ );
214
+ this._root = /uid=0/.test(out);
215
+ const rish = /RISH=yes/.test(out);
216
+ const via = this.profile.via ?? "auto";
217
+ if (via === "root" && !this._root) throw new Error(`${this.name}: via=root but no root access`);
218
+ this.wrap = chooseWrap({ root: this._root, rish }, via);
219
+ }
220
+ cleanLogcat() {
221
+ try {
222
+ this.sh("logcat -c\n", { asRoot: this.root });
223
+ } catch {
224
+ }
225
+ }
226
+ };
227
+ function adbDevices() {
228
+ const r = spawnSync("adb", ["devices", "-l"], { encoding: "utf8" });
229
+ return (r.stdout ?? "") + (r.stderr ?? "");
230
+ }
231
+ function adbSerials() {
232
+ const out = adbDevices();
233
+ const res = [];
234
+ for (const line of out.split("\n").slice(1)) {
235
+ const m = /^(\S+)\s+(device|unauthorized|offline)\b/.exec(line.trim());
236
+ if (m) res.push({ serial: m[1], state: m[2] });
237
+ }
238
+ return res;
239
+ }
240
+
241
+ // src/shell/bitwarden.ts
242
+ import { spawnSync as spawnSync2 } from "node:child_process";
243
+ function bwUnlocked() {
244
+ try {
245
+ const r = spawnSync2("bw-mcp-cli", ["status"], { encoding: "utf8" });
246
+ return JSON.parse(r.stdout ?? "{}").status === "unlocked";
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ function bwItemFields(name) {
252
+ const r = spawnSync2("bw-mcp-cli", ["get", "item", name], { encoding: "utf8" });
253
+ if (r.status !== 0 || !r.stdout) throw new Error(`bitwarden item '${name}' not found`);
254
+ const item = JSON.parse(r.stdout);
255
+ const fields = {};
256
+ for (const f of item.fields ?? []) fields[f.name] = f.value;
257
+ return fields;
258
+ }
259
+
260
+ // src/shell/prompt.ts
261
+ async function promptHidden(label) {
262
+ process.stderr.write(label);
263
+ const stdin = process.stdin;
264
+ stdin.setRawMode?.(true);
265
+ stdin.resume();
266
+ let buf = "";
267
+ for await (const chunk of stdin) {
268
+ for (const ch of chunk.toString("utf8")) {
269
+ if (ch === "\n" || ch === "\r") {
270
+ stdin.setRawMode?.(false);
271
+ stdin.pause();
272
+ process.stderr.write("\n");
273
+ return buf;
274
+ } else if (ch === "\x7F" || ch === "\b") {
275
+ buf = buf.slice(0, -1);
276
+ } else if (ch === "") {
277
+ process.exit(130);
278
+ } else {
279
+ buf += ch;
280
+ }
281
+ }
282
+ }
283
+ stdin.setRawMode?.(false);
284
+ return buf;
285
+ }
286
+
287
+ // src/cli.ts
288
+ var ASK = "\0ASK";
289
+ function log(m) {
290
+ process.stderr.write(m + "\n");
291
+ }
292
+ function die(m) {
293
+ log("\u2717 " + m);
294
+ process.exit(1);
295
+ }
296
+ function preprocess(argv) {
297
+ const creds = /* @__PURE__ */ new Set(["--pattern", "--pin", "--password"]);
298
+ const out = [];
299
+ for (let i = 0; i < argv.length; i++) {
300
+ const a = argv[i];
301
+ if (creds.has(a)) {
302
+ const next = argv[i + 1];
303
+ out.push(next === void 0 || next.startsWith("-") ? `${a}=${ASK}` : a);
304
+ } else out.push(a);
305
+ }
306
+ return out;
307
+ }
308
+ function loadRegistry(path) {
309
+ const p = path || process.env.ADB_UNLOCK_DEVICES || "devices.json";
310
+ return existsSync(p) ? parseRegistry(readFileSync(p, "utf8")) : {};
311
+ }
312
+ async function resolveCredential(values, label, bwItem) {
313
+ for (const t of ["pattern", "pin", "password"]) {
314
+ const v = values[t];
315
+ if (v !== void 0) {
316
+ const val = v === ASK ? await promptHidden(`${t} for ${label} (hidden): `) : v;
317
+ if (!val) die("empty credential");
318
+ return { type: t, value: val, source: "direct" };
319
+ }
320
+ }
321
+ if (!bwItem) {
322
+ die("no credential given. pass --pattern/--pin/--password, or --bw-item/--device for Bitwarden.");
323
+ }
324
+ if (!bwUnlocked()) die("bitwarden vault is locked. unlock it, or pass a credential directly.");
325
+ const fields = bwItemFields(bwItem);
326
+ const r = resolveCredentialType(fields, values.type);
327
+ return { ...r, source: "bitwarden" };
328
+ }
329
+ function verifyCredential(dev, cred) {
330
+ const out = dev.sh(`locksettings verify --old '${cred}'
331
+ `, { asRoot: true });
332
+ dev.cleanLogcat();
333
+ return out.includes("verified successfully");
334
+ }
335
+ function removeLock(dev, cred) {
336
+ const out = dev.sh(`locksettings clear --old '${cred}'
337
+ `, { asRoot: true });
338
+ dev.cleanLogcat();
339
+ const chk = dev.sh(
340
+ "locksettings get-disabled 2>/dev/null; dumpsys lock_settings 2>/dev/null | grep -m1 CredentialType\n",
341
+ { asRoot: true }
342
+ );
343
+ return chk.includes("NONE") || /success/i.test(out);
344
+ }
345
+ async function injectPattern(dev, cred, dry, size) {
346
+ const raise = raiseLoopScript(size ?? void 0, "pattern");
347
+ const xml = dev.sh(raise + "cat /sdcard/.au.xml; rm -f /sdcard/.au.xml\n");
348
+ const bounds = patternGridFromDump(xml);
349
+ if (!bounds) die("no pattern grid in the accessibility tree (did the bouncer show? maybe the lock is not a pattern)");
350
+ const digits = parsePatternDigits(cred);
351
+ if (digits.length < 4) die("pattern shorter than 4 dots \u2014 probably a wrong value");
352
+ log(` grid: bounds=[${bounds.join(",")}] pattern-dots=${digits.length}`);
353
+ if (dry) return;
354
+ dev.sh(raise + "rm -f /sdcard/.au.xml\n" + buildPatternGesture(patternPoints(bounds), digits).join("\n") + "\n");
355
+ await sleep(1e3);
356
+ }
357
+ async function injectText(dev, cred, dry, size) {
358
+ const raise = raiseLoopScript(size ?? void 0, "text");
359
+ if (dry) {
360
+ dev.sh(raise + "rm -f /sdcard/.au.xml\n");
361
+ log(" (dry-run: no text entered)");
362
+ return;
363
+ }
364
+ dev.sh(raise + "rm -f /sdcard/.au.xml\n" + buildTextEntry(cred).join("\n") + "\n");
365
+ await sleep(1e3);
366
+ }
367
+ var HELP = `adb-unlock \u2014 inject a known pattern/PIN/password into a remote Android device
368
+
369
+ Usage: adb-unlock [options] (everything via flags; default transport: adb)
370
+
371
+ Credential (one of; a bare flag prompts hidden):
372
+ --pattern [VALUE] pattern as position digits 1-9 (keypad order, 1=top-left)
373
+ --pin [VALUE] numeric PIN
374
+ --password [VALUE] text password
375
+ --type TYPE when reading from Bitwarden: pattern|pin|password
376
+
377
+ Device / transport:
378
+ --transport adb|ssh default adb (ssh is implied by --ssh)
379
+ -s, --serial SERIAL adb device serial (also env ANDROID_SERIAL)
380
+ --ssh ALIAS ssh host alias (selects ssh transport)
381
+ -d, --device NAME registry key in devices.json; also default --bw-item
382
+ --via auto|root|rish|none shell wrapper on device (default auto)
383
+ --list list connected adb devices and exit
384
+
385
+ Other:
386
+ --bw-item NAME Bitwarden item name (default: --device value)
387
+ --devices PATH device registry JSON (default ./devices.json or $ADB_UNLOCK_DEVICES)
388
+ --no-verify skip the root locksettings-verify pre-check
389
+ --force inject even during an active call
390
+ --dry-run recon only, no injection
391
+ --remove-lock remove the lock entirely (needs root)
392
+ -h, --help
393
+
394
+ Requires 'adb' and/or 'ssh' on PATH. Credential never printed; logcat cleared after.`;
395
+ async function main() {
396
+ const { values } = parseArgs({
397
+ args: preprocess(process.argv.slice(2)),
398
+ allowPositionals: false,
399
+ options: {
400
+ type: { type: "string" },
401
+ ssh: { type: "string" },
402
+ via: { type: "string" },
403
+ transport: { type: "string" },
404
+ serial: { type: "string", short: "s" },
405
+ device: { type: "string", short: "d" },
406
+ "bw-item": { type: "string" },
407
+ devices: { type: "string" },
408
+ pattern: { type: "string" },
409
+ pin: { type: "string" },
410
+ password: { type: "string" },
411
+ "no-verify": { type: "boolean" },
412
+ "remove-lock": { type: "boolean" },
413
+ force: { type: "boolean" },
414
+ "dry-run": { type: "boolean" },
415
+ list: { type: "boolean" },
416
+ help: { type: "boolean", short: "h" }
417
+ }
418
+ });
419
+ if (values.help) {
420
+ console.log(HELP);
421
+ return;
422
+ }
423
+ if (values.list) {
424
+ process.stdout.write(adbDevices());
425
+ return;
426
+ }
427
+ const deviceKey = values.device;
428
+ const registry = loadRegistry(values.devices);
429
+ const profile = { ...deviceKey ? registry[deviceKey] ?? {} : {} };
430
+ if (values.ssh) profile.ssh = values.ssh;
431
+ if (values.via) profile.via = values.via;
432
+ const serial = values.serial ?? process.env.ANDROID_SERIAL ?? profile.adb?.serial;
433
+ const effTransport = values.transport ?? profile.transport ?? (serial ? "adb" : profile.ssh ? "ssh" : "adb");
434
+ if (effTransport === "ssh" && !profile.ssh) {
435
+ die("ssh transport needs an alias \u2014 pass --ssh <alias> (or --device with ssh in devices.json).");
436
+ }
437
+ if (effTransport === "adb" && !serial) {
438
+ const devs = adbSerials().filter((d) => d.state === "device");
439
+ if (devs.length > 1) {
440
+ die("multiple adb devices connected \u2014 choose one with -s/--serial (or $ANDROID_SERIAL):\n " + devs.map((d) => d.serial).join("\n "));
441
+ }
442
+ }
443
+ const label = deviceKey ?? serial ?? profile.ssh ?? "(adb)";
444
+ const bwItem = values["bw-item"] ?? profile.bw_item ?? deviceKey;
445
+ const { type, value: cred, source } = await resolveCredential(values, label, bwItem);
446
+ log(`device: ${label} \xB7 transport=${effTransport} \xB7 type=${type} \xB7 source=${source}`);
447
+ const dev = new Device(label, profile, { transport: effTransport, serial });
448
+ dev.detect();
449
+ log(` root=${dev.root ? "yes" : "no"} wrap=${dev.wrap || "none"}`);
450
+ const st = parseRecon(dev.sh(reconScript()));
451
+ log(` keyguard=${st.keyguard ? "shown" : "dismissed"} call=${st.call ? "active" : "none"} size=${st.size} top=${st.top}`);
452
+ if (st.call && !values.force) die("active call on device \u2014 not injecting blind. end the call or use --force.");
453
+ if (!st.keyguard && !values["remove-lock"]) {
454
+ log("\u2713 device already unlocked (keyguard dismissed).");
455
+ return;
456
+ }
457
+ if (dev.root && !values["no-verify"]) {
458
+ if (verifyCredential(dev, cred)) log(" \u2713 credential verified via locksettings");
459
+ else die(`credential (${source}) failed verify on device \u2014 stopping before injection (lockout risk).`);
460
+ }
461
+ if (values["remove-lock"] && !dev.root) die("--remove-lock needs root, none available.");
462
+ if (st.keyguard) {
463
+ const size = parseSize(st.size);
464
+ if (type === "pattern") await injectPattern(dev, cred, !!values["dry-run"], size);
465
+ else await injectText(dev, cred, !!values["dry-run"], size);
466
+ dev.cleanLogcat();
467
+ if (values["dry-run"]) {
468
+ log("\u2713 dry-run done (nothing injected).");
469
+ return;
470
+ }
471
+ const st2 = parseRecon(dev.sh(reconScript()));
472
+ if (st2.keyguard) die("injection did not dismiss the keyguard. check the lock type and value.");
473
+ log(`\u2713 unlocked \u2014 keyguard dismissed, top=${st2.top}`);
474
+ }
475
+ if (values["remove-lock"]) {
476
+ if (removeLock(dev, cred)) log("\u2713 lock removed (locksettings clear).");
477
+ else die("remove-lock not confirmed \u2014 check manually.");
478
+ }
479
+ }
480
+ main().catch((e) => die(e instanceof Error ? e.message : String(e)));
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "adb-unlock",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Inject a known pattern / PIN / password into a remote Android device (ADB or SSH) to dismiss the lock screen without touching it — coordinates derived live from the accessibility tree.",
5
+ "type": "module",
6
+ "bin": { "adb-unlock": "dist/cli.js" },
7
+ "files": ["dist"],
8
+ "engines": { "node": ">=18" },
9
+ "scripts": {
10
+ "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --banner:js='#!/usr/bin/env node'",
11
+ "test": "vitest run",
12
+ "typecheck": "tsc --noEmit",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": ["android", "adb", "unlock", "pattern", "pin", "lockscreen", "uiautomator", "cli", "scrcpy"],
16
+ "license": "MIT",
17
+ "repository": { "type": "git", "url": "git+https://github.com/MusiCode1/adb-unlock.git" },
18
+ "homepage": "https://github.com/MusiCode1/adb-unlock#readme",
19
+ "bugs": { "url": "https://github.com/MusiCode1/adb-unlock/issues" },
20
+ "devDependencies": {
21
+ "@types/node": "latest",
22
+ "esbuild": "latest",
23
+ "typescript": "latest",
24
+ "vitest": "latest"
25
+ }
26
+ }