adbex 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JSleim
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 ADDED
@@ -0,0 +1,80 @@
1
+ # adbex
2
+
3
+ A friendlier `adb` wrapper. Adds glob expansion, device selection, backups, screenshots, and more — and passes anything it doesn't recognize straight through to `adb`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g adbex
9
+ ```
10
+
11
+ Requires `adb` to be installed and on your PATH (part of [Android Platform Tools](https://developer.android.com/tools/releases/platform-tools)).
12
+
13
+ ## Commands
14
+
15
+ | Command | Description |
16
+ |---|---|
17
+ | `adbex pull <source...> [dest]` | Pull files; globs like `*.png` are expanded on the device |
18
+ | `adbex push <source...> <dest>` | Push files; globs like `*.mp4` are expanded locally |
19
+ | `adbex apk-pull <package>` | Pull all APKs for an installed package |
20
+ | `adbex screenshot [dest]` | Capture the screen and save it locally |
21
+ | `adbex backup <package> [--data]` | Back up an APK and optionally its sdcard data |
22
+ | `adbex restore <dir> [--data]` | Reinstall APKs and optionally push data back |
23
+ | `adbex install <file\|glob...>` | Install one or more APKs; supports wildcards like `*.apk` |
24
+ | `adbex devices` | List connected devices with friendly info |
25
+ | `adbex select` | Pick a default device (saved to `~/.adbex.json`) |
26
+ | `adbex packages [filter]` | List packages; filter by name, `-3` for third-party, `-s` for system |
27
+ | `adbex logcat [package]` | Stream logcat for a package; auto-resolves its PID |
28
+
29
+ Any command adbex doesn't recognize is passed directly to `adb`:
30
+
31
+ ```bash
32
+ adbex shell # same as: adb shell
33
+ adbex reboot bootloader # same as: adb reboot bootloader
34
+ adbex forward tcp:8080 ... # and so on
35
+ ```
36
+
37
+ ## Flags
38
+
39
+ | Flag | Description |
40
+ |---|---|
41
+ | `-d <serial>` | Target a specific device, overrides the saved default |
42
+ | `-v, --verbose` | Show what adbex is doing under the hood |
43
+ | `-h, --help` | Print help |
44
+
45
+ ## Device selection
46
+
47
+ If you work with multiple devices, pick one as the default so you don't have to type `-d` every time:
48
+
49
+ ```bash
50
+ adbex select
51
+ ```
52
+
53
+ Your choice is saved to `~/.adbex.json`. Override it at any time with `-d <serial>`.
54
+
55
+ ## Examples
56
+
57
+ ```bash
58
+ # Pull all screenshots from the device
59
+ adbex pull /sdcard/DCIM/Screenshots/*.png ./screenshots
60
+
61
+ # Install every APK in the current directory
62
+ adbex install *.apk
63
+
64
+ # Back up an app and its data, then restore it to a new device
65
+ adbex backup com.example.app --data
66
+ adbex restore ./backup-com.example.app --data
67
+
68
+ # Pull the APK for an installed app
69
+ adbex apk-pull com.example.app
70
+
71
+ # Watch logs for a specific app
72
+ adbex logcat com.example.app
73
+
74
+ # Use a specific device for one command
75
+ adbex -d emulator-5554 screenshot
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "adbex",
3
+ "version": "1.0.0",
4
+ "author": "JSleim",
5
+ "description": "A friendlier ADB wrapper - glob pulls, APK extraction, screenshots, backups and more",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "adbex": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js"
12
+ },
13
+ "keywords": ["adb", "android", "cli", "file-transfer", "apk"],
14
+ "license": "MIT",
15
+ "engines": {
16
+ "node": ">=14"
17
+ }
18
+ }
@@ -0,0 +1,63 @@
1
+ const { execFileSync, spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function apkPull(args, { verbose } = {}) {
6
+ const pkg = args[0];
7
+ const dest = args[1] || '.';
8
+
9
+ if (!pkg) {
10
+ console.error('Usage: adbex apk-pull <package> [dest]');
11
+ console.error('Example: adbex apk-pull com.example.app ./apks');
12
+ process.exit(1);
13
+ }
14
+
15
+ if (verbose) console.error(`[adbex] adb shell pm path ${pkg}`);
16
+
17
+ let pmOutput;
18
+ try {
19
+ pmOutput = execFileSync('adb', ['shell', 'pm', 'path', pkg], {
20
+ encoding: 'utf8',
21
+ });
22
+ } catch (err) {
23
+ console.error(`Failed to find package "${pkg}". Is it installed?`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const paths = pmOutput
28
+ .trim()
29
+ .split(/\r?\n/)
30
+ .map(line => line.replace(/^package:/, '').trim())
31
+ .filter(Boolean);
32
+
33
+ if (!paths.length) {
34
+ console.error(`No APKs found for "${pkg}".`);
35
+ process.exit(1);
36
+ }
37
+
38
+ if (dest !== '.') {
39
+ fs.mkdirSync(dest, { recursive: true });
40
+ }
41
+
42
+ console.log(`Found ${paths.length} APK(s) for ${pkg}`);
43
+
44
+ let failed = 0;
45
+ for (const apkPath of paths) {
46
+ const fileName = path.basename(apkPath);
47
+ console.log(`Pulling ${fileName}...`);
48
+ if (verbose) console.error(`[adbex] adb pull "${apkPath}" "${dest}"`);
49
+
50
+ const result = spawnSync('adb', ['pull', apkPath, dest], { stdio: 'inherit' });
51
+ if (result.status !== 0) {
52
+ console.error(`Failed: ${apkPath}`);
53
+ failed++;
54
+ }
55
+ }
56
+
57
+ if (failed > 0) {
58
+ console.error(`\n${failed} APK(s) failed.`);
59
+ process.exit(1);
60
+ } else {
61
+ console.log(`\nDone. ${paths.length} APK(s) saved to ${dest}`);
62
+ }
63
+ };
@@ -0,0 +1,90 @@
1
+ const { execFileSync, spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function backup(args, { verbose } = {}) {
6
+ const flags = args.filter(a => a.startsWith('-'));
7
+ const positional = args.filter(a => !a.startsWith('-'));
8
+
9
+ const pkg = positional[0];
10
+ const dest = positional[1] || '.';
11
+ const includeData = flags.includes('--data') || flags.includes('--full');
12
+
13
+ if (!pkg) {
14
+ console.error('Usage: adbex backup <package> [dest] [--data]');
15
+ console.error('Example: adbex backup com.example.app ./backups --data');
16
+ process.exit(1);
17
+ }
18
+
19
+ const resolvedDest = require('path').resolve(dest);
20
+ const backupDir = path.join(resolvedDest, pkg);
21
+ fs.mkdirSync(backupDir, { recursive: true });
22
+
23
+ const steps = includeData ? 2 : 1;
24
+ let step = 0;
25
+ console.log(`Backing up ${pkg} → ${backupDir}`);
26
+
27
+ console.log(`\n[${++step}/${steps}] Pulling APK(s)...`);
28
+ let pmOutput;
29
+ try {
30
+ if (verbose) console.error(`[adbex] adb shell pm path ${pkg}`);
31
+ pmOutput = execFileSync('adb', ['shell', 'pm', 'path', pkg], { encoding: 'utf8' });
32
+ } catch {
33
+ console.error(`Package "${pkg}" not found on device.`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const apkPaths = pmOutput
38
+ .trim()
39
+ .split(/\r?\n/)
40
+ .map(l => l.replace(/^package:/, '').trim())
41
+ .filter(Boolean);
42
+
43
+ let apkFailed = 0;
44
+ for (const apkPath of apkPaths) {
45
+ if (verbose) console.error(`[adbex] adb pull "${apkPath}" "${backupDir}"`);
46
+ const result = spawnSync('adb', ['pull', apkPath, backupDir], { stdio: 'inherit' });
47
+ if (result.status !== 0) apkFailed++;
48
+ }
49
+
50
+ if (apkFailed) {
51
+ console.error(`Warning: ${apkFailed} APK(s) failed to pull.`);
52
+ } else {
53
+ console.log(`APK(s) saved (${apkPaths.length} file(s))`);
54
+ }
55
+
56
+ if (includeData) {
57
+ console.log(`\n[${++step}/${steps}] Pulling sdcard data...`);
58
+ const remoteDat = `/sdcard/Android/data/${pkg}`;
59
+ const dataDir = path.join(backupDir, 'data');
60
+ fs.mkdirSync(dataDir, { recursive: true });
61
+
62
+ try {
63
+ if (verbose) console.error(`[adbex] adb shell ls ${remoteDat}`);
64
+ execFileSync('adb', ['shell', 'ls', remoteDat], { encoding: 'utf8' });
65
+ } catch {
66
+ console.warn(`No sdcard data found at ${remoteDat} — app may store data internally only.`);
67
+ summarize(backupDir, pkg);
68
+ return;
69
+ }
70
+
71
+ if (verbose) console.error(`[adbex] adb pull "${remoteDat}" "${dataDir}"`);
72
+ const result = spawnSync('adb', ['pull', remoteDat, dataDir], { stdio: 'inherit' });
73
+
74
+ if (result.status !== 0) {
75
+ console.error('Warning: data pull completed with errors.');
76
+ } else {
77
+ console.log('Data saved.');
78
+ }
79
+ } else {
80
+ console.log('\nTip: use --data to also backup /sdcard/Android/data');
81
+ }
82
+
83
+ summarize(backupDir, pkg);
84
+ };
85
+
86
+ function summarize(backupDir, pkg) {
87
+ console.log(`\nBackup complete → ${backupDir}`);
88
+ console.log(`Restore with: adbex restore "${backupDir}"`);
89
+ console.log(`With data: adbex restore "${backupDir}" --data`);
90
+ }
@@ -0,0 +1,67 @@
1
+ const { execFileSync } = require('child_process');
2
+
3
+ module.exports = function devices(args, { verbose } = {}) {
4
+ let devicesOutput;
5
+ try {
6
+ devicesOutput = execFileSync('adb', ['devices'], { encoding: 'utf8' });
7
+ } catch {
8
+ console.error('Failed to run adb devices. Is adb in your PATH?');
9
+ process.exit(1);
10
+ }
11
+
12
+ const lines = devicesOutput.trim().split(/\r?\n/).slice(1);
13
+ const serials = lines
14
+ .map(l => l.trim())
15
+ .filter(l => l && !l.startsWith('*'))
16
+ .map(l => {
17
+ const [serial, state] = l.split(/\s+/);
18
+ return { serial, state };
19
+ });
20
+
21
+ if (!serials.length) {
22
+ console.log('No devices connected.');
23
+ return;
24
+ }
25
+
26
+ console.log(`${serials.length} device(s) connected:\n`);
27
+
28
+ for (const { serial, state } of serials) {
29
+ if (state !== 'device') {
30
+ console.log(` ${serial} [${state}]`);
31
+ continue;
32
+ }
33
+
34
+ try {
35
+ const prop = (prop) => {
36
+ if (verbose) console.error(`[adbex] adb -s ${serial} shell getprop ${prop}`);
37
+ return execFileSync('adb', ['-s', serial, 'shell', 'getprop', prop], {
38
+ encoding: 'utf8',
39
+ }).trim();
40
+ };
41
+
42
+ const model = prop('ro.product.model');
43
+ const brand = prop('ro.product.brand');
44
+ const androidVer = prop('ro.build.version.release');
45
+ const sdkVer = prop('ro.build.version.sdk');
46
+
47
+ let battery = '?';
48
+ try {
49
+ const battOut = execFileSync(
50
+ 'adb', ['-s', serial, 'shell', 'dumpsys', 'battery'],
51
+ { encoding: 'utf8' }
52
+ );
53
+ const match = battOut.match(/level:\s*(\d+)/);
54
+ if (match) battery = `${match[1]}%`;
55
+ } catch (_) {}
56
+
57
+ console.log(` ${serial}`);
58
+ console.log(` Device: ${brand} ${model}`);
59
+ console.log(` Android: ${androidVer} (SDK ${sdkVer})`);
60
+ console.log(` Battery: ${battery}`);
61
+ console.log();
62
+ } catch (err) {
63
+ console.log(` ${serial} (could not read device info)`);
64
+ if (verbose) console.error(err.message);
65
+ }
66
+ }
67
+ };
@@ -0,0 +1,69 @@
1
+ const { spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function install(args, { verbose } = {}) {
6
+ if (!args.length) {
7
+ console.error('Usage: adbex install <file|glob> [file|glob...]');
8
+ console.error('Example: adbex install app.apk');
9
+ console.error('Example: adbex install .\\backup\\*.apk');
10
+ process.exit(1);
11
+ }
12
+
13
+ const apks = [];
14
+
15
+ for (const arg of args) {
16
+ if (hasGlob(arg)) {
17
+ const dir = path.dirname(arg);
18
+ const pattern = path.basename(arg);
19
+ const regex = globToRegex(pattern);
20
+
21
+ if (!fs.existsSync(dir)) {
22
+ console.error(`Directory not found: ${dir}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ const matches = fs.readdirSync(dir)
27
+ .filter(f => regex.test(f) && f.endsWith('.apk'))
28
+ .map(f => path.join(dir, f));
29
+
30
+ if (!matches.length) {
31
+ console.error(`No APKs matched: ${arg}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ if (verbose) console.error(`[adbex] expanded "${arg}" → ${matches.join(', ')}`);
36
+ apks.push(...matches);
37
+ } else {
38
+ if (!fs.existsSync(arg)) {
39
+ console.error(`File not found: ${arg}`);
40
+ process.exit(1);
41
+ }
42
+ apks.push(arg);
43
+ }
44
+ }
45
+
46
+ if (!apks.length) {
47
+ console.error('No APKs to install.');
48
+ process.exit(1);
49
+ }
50
+
51
+ const cmd = apks.length === 1 ? 'install' : 'install-multiple';
52
+ console.log(`Installing ${apks.length} APK(s) via ${cmd}...`);
53
+ if (verbose) console.error(`[adbex] adb ${cmd} ${apks.join(' ')}`);
54
+
55
+ const result = spawnSync('adb', [cmd, ...apks], { stdio: 'inherit' });
56
+ process.exit(result.status ?? 1);
57
+ };
58
+
59
+ function hasGlob(s) {
60
+ return /[*?[]/.test(s);
61
+ }
62
+
63
+ function globToRegex(pattern) {
64
+ const escaped = pattern
65
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
66
+ .replace(/\*/g, '.*')
67
+ .replace(/\?/g, '.');
68
+ return new RegExp(`^${escaped}$`, 'i');
69
+ }
@@ -0,0 +1,29 @@
1
+ const { execFileSync, spawnSync } = require('child_process');
2
+
3
+ module.exports = function logcat(args, { verbose } = {}) {
4
+ const pkg = args[0];
5
+
6
+ if (!pkg || pkg.startsWith('-')) {
7
+ const result = spawnSync('adb', ['logcat', ...args], { stdio: 'inherit' });
8
+ process.exit(result.status ?? 1);
9
+ }
10
+
11
+ const rest = args.slice(1);
12
+
13
+ let pid;
14
+ try {
15
+ pid = execFileSync('adb', ['shell', 'pidof', pkg], { encoding: 'utf8' }).trim();
16
+ } catch {
17
+ console.error(`Could not get PID for package "${pkg}"`);
18
+ process.exit(1);
19
+ }
20
+
21
+ if (!pid) {
22
+ console.error(`Package "${pkg}" is not running.`);
23
+ process.exit(1);
24
+ }
25
+
26
+ if (verbose) console.error(`[adbex] adb logcat --pid=${pid} ${rest.join(' ')}`);
27
+ const result = spawnSync('adb', ['logcat', '--pid=' + pid, ...rest], { stdio: 'inherit' });
28
+ process.exit(result.status ?? 1);
29
+ };
@@ -0,0 +1,39 @@
1
+ const { execFileSync } = require('child_process');
2
+
3
+ module.exports = function packages(args, { verbose } = {}) {
4
+ const flags = args.filter(a => a.startsWith('-'));
5
+ const filter = args.filter(a => !a.startsWith('-'))[0];
6
+
7
+ const pmArgs = ['shell', 'pm', 'list', 'packages'];
8
+ if (flags.includes('-3')) pmArgs.push('-3');
9
+ if (flags.includes('-s')) pmArgs.push('-s');
10
+ if (flags.includes('-e')) pmArgs.push('-e');
11
+
12
+ if (verbose) console.error(`[adbex] adb ${pmArgs.join(' ')}`);
13
+
14
+ let output;
15
+ try {
16
+ output = execFileSync('adb', pmArgs, { encoding: 'utf8' });
17
+ } catch {
18
+ console.error('Failed to list packages. Is a device connected?');
19
+ process.exit(1);
20
+ }
21
+
22
+ let packages = output
23
+ .trim()
24
+ .split(/\r?\n/)
25
+ .map(l => l.replace(/^package:/, '').trim())
26
+ .filter(Boolean);
27
+
28
+ if (filter) {
29
+ packages = packages.filter(p => p.toLowerCase().includes(filter.toLowerCase()));
30
+ }
31
+
32
+ if (!packages.length) {
33
+ console.log(filter ? `No packages matching "${filter}".` : 'No packages found.');
34
+ return;
35
+ }
36
+
37
+ packages.sort().forEach(p => console.log(p));
38
+ console.log(`\n${packages.length} package(s)${filter ? ` matching "${filter}"` : ''}`);
39
+ };
@@ -0,0 +1,98 @@
1
+ const { execFileSync, spawnSync } = require('child_process');
2
+ const { mkdirSync } = require('fs');
3
+
4
+ function ensureDestDir(dest) {
5
+ try {
6
+ mkdirSync(dest, { recursive: true });
7
+ } catch {}
8
+ }
9
+
10
+ module.exports = function pull(args, { verbose } = {}) {
11
+ if (args.length === 0) {
12
+ console.error('Usage: adbex pull <source...> [dest]');
13
+ process.exit(1);
14
+ }
15
+
16
+ const hasAnyGlob = args.some(hasGlob);
17
+
18
+ if (!hasAnyGlob) {
19
+ if (args.length <= 2) {
20
+ if (args.length === 2) ensureDestDir(args[1]);
21
+ if (verbose) console.error(`[adbex] adb pull ${args.join(' ')}`);
22
+ const result = spawnSync('adb', ['pull', ...args], { stdio: 'inherit' });
23
+ process.exit(result.status ?? 1);
24
+ } else {
25
+ const dest = args[args.length - 1];
26
+ const sources = args.slice(0, -1);
27
+ ensureDestDir(dest);
28
+ for (const source of sources) {
29
+ if (verbose) console.error(`[adbex] adb pull "${source}" "${dest}"`);
30
+ const result = spawnSync('adb', ['pull', source, dest], { stdio: 'inherit' });
31
+ if (result.status !== 0) process.exit(result.status);
32
+ }
33
+ return;
34
+ }
35
+ }
36
+
37
+ const dest = args.length >= 2 ? args[args.length - 1] : '.';
38
+ const sources = args.length >= 2 ? args.slice(0, -1) : args;
39
+
40
+ ensureDestDir(dest);
41
+
42
+ const expanded = [];
43
+
44
+ for (const source of sources) {
45
+ if (hasGlob(source)) {
46
+ if (verbose) console.error(`[adbex] expanding: adb shell ls ${source}`);
47
+ try {
48
+ const out = execFileSync('adb', ['shell', 'ls', source], {
49
+ encoding: 'utf8',
50
+ maxBuffer: 10 * 1024 * 1024,
51
+ });
52
+ const matches = out
53
+ .trim()
54
+ .split(/\r?\n/)
55
+ .map(s => s.trim())
56
+ .filter(s => s && !s.startsWith('ls:'));
57
+
58
+ if (!matches.length) {
59
+ console.warn(`Warning: no matches for "${source}"`);
60
+ } else {
61
+ if (verbose) console.error(`[adbex] matched: ${matches.join(', ')}`);
62
+ expanded.push(...matches);
63
+ }
64
+ } catch (err) {
65
+ console.error(`Error expanding "${source}": ${err.message}`);
66
+ process.exit(1);
67
+ }
68
+ } else {
69
+ expanded.push(source);
70
+ }
71
+ }
72
+
73
+ if (!expanded.length) {
74
+ console.error('No files to pull.');
75
+ process.exit(1);
76
+ }
77
+
78
+ console.log(`Pulling ${expanded.length} file(s) → ${dest}`);
79
+
80
+ let failed = 0;
81
+ for (const file of expanded) {
82
+ if (verbose) console.error(`[adbex] adb pull "${file}" "${dest}"`);
83
+ const result = spawnSync('adb', ['pull', file, dest], { stdio: 'inherit' });
84
+ if (result.status !== 0) {
85
+ console.error(`Failed: ${file}`);
86
+ failed++;
87
+ }
88
+ }
89
+
90
+ if (failed > 0) {
91
+ console.error(`\n${failed} file(s) failed.`);
92
+ process.exit(1);
93
+ }
94
+ };
95
+
96
+ function hasGlob(s) {
97
+ return /[*?[]/.test(s);
98
+ }
@@ -0,0 +1,89 @@
1
+ const { spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function push(args, { verbose } = {}) {
6
+ if (args.length < 2) {
7
+ console.error('Usage: adbex push <source...> <dest>');
8
+ console.error('Example: adbex push *.mp4 /sdcard/Movies/');
9
+ console.error('Example: adbex push ./photos/*.jpg /sdcard/DCIM/');
10
+ process.exit(1);
11
+ }
12
+
13
+ const dest = args[args.length - 1];
14
+ const sources = args.slice(0, -1);
15
+ const hasAnyGlob = sources.some(hasGlob);
16
+
17
+ if (!hasAnyGlob) {
18
+ if (verbose) console.error(`[adbex] adb push ${args.join(' ')}`);
19
+ const result = spawnSync('adb', ['push', ...args], { stdio: 'inherit' });
20
+ process.exit(result.status ?? 1);
21
+ }
22
+
23
+ const expanded = [];
24
+
25
+ for (const source of sources) {
26
+ if (hasGlob(source)) {
27
+ const dir = path.dirname(source);
28
+ const pattern = path.basename(source);
29
+ const regex = globToRegex(pattern);
30
+ const resolvedDir = dir === '.' ? process.cwd() : path.resolve(dir);
31
+
32
+ if (!fs.existsSync(resolvedDir)) {
33
+ console.error(`Directory not found: ${resolvedDir}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const matches = fs.readdirSync(resolvedDir)
38
+ .filter(f => regex.test(f))
39
+ .map(f => path.join(resolvedDir, f));
40
+
41
+ if (!matches.length) {
42
+ console.warn(`Warning: no matches for "${source}"`);
43
+ } else {
44
+ if (verbose) console.error(`[adbex] expanded "${source}" → ${matches.join(', ')}`);
45
+ expanded.push(...matches);
46
+ }
47
+ } else {
48
+ if (!fs.existsSync(source)) {
49
+ console.error(`File not found: ${source}`);
50
+ process.exit(1);
51
+ }
52
+ expanded.push(source);
53
+ }
54
+ }
55
+
56
+ if (!expanded.length) {
57
+ console.error('No files to push.');
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log(`Pushing ${expanded.length} file(s) → ${dest}`);
62
+
63
+ let failed = 0;
64
+ for (const file of expanded) {
65
+ if (verbose) console.error(`[adbex] adb push "${file}" "${dest}"`);
66
+ const result = spawnSync('adb', ['push', file, dest], { stdio: 'inherit' });
67
+ if (result.status !== 0) {
68
+ console.error(`Failed: ${file}`);
69
+ failed++;
70
+ }
71
+ }
72
+
73
+ if (failed > 0) {
74
+ console.error(`\n${failed} file(s) failed.`);
75
+ process.exit(1);
76
+ }
77
+ };
78
+
79
+ function hasGlob(s) {
80
+ return /[*?[]/.test(s);
81
+ }
82
+
83
+ function globToRegex(pattern) {
84
+ const escaped = pattern
85
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
86
+ .replace(/\*/g, '.*')
87
+ .replace(/\?/g, '.');
88
+ return new RegExp(`^${escaped}$`, 'i');
89
+ }
@@ -0,0 +1,103 @@
1
+ const { spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function restore(args, { verbose } = {}) {
6
+ const dataFlagIndex = args.indexOf('--data');
7
+ const hasData = dataFlagIndex !== -1;
8
+
9
+ let explicitDataDir = null;
10
+ const cleanArgs = [...args];
11
+ if (hasData) {
12
+ cleanArgs.splice(dataFlagIndex, 1);
13
+ const next = cleanArgs[dataFlagIndex];
14
+ if (next && !next.startsWith('-')) {
15
+ explicitDataDir = next;
16
+ cleanArgs.splice(dataFlagIndex, 1);
17
+ }
18
+ }
19
+
20
+ const dir = cleanArgs[0];
21
+
22
+ if (!dir) {
23
+ console.error('Usage: adbex restore <dir> [--data [data-dir]]');
24
+ console.error('Example: adbex restore .\\com.example.app --data');
25
+ console.error('Example: adbex restore .\\com.example.app --data .\\mydata\\');
26
+ process.exit(1);
27
+ }
28
+
29
+ const resolvedDir = path.resolve(dir);
30
+
31
+ if (!fs.existsSync(resolvedDir)) {
32
+ console.error(`Directory not found: ${resolvedDir}`);
33
+ process.exit(1);
34
+ }
35
+
36
+ const apks = fs.readdirSync(resolvedDir)
37
+ .filter(f => f.endsWith('.apk'))
38
+ .map(f => path.join(resolvedDir, f));
39
+
40
+ if (!apks.length) {
41
+ console.error(`No APKs found in ${resolvedDir}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const steps = hasData ? 2 : 1;
46
+ let step = 0;
47
+
48
+ console.log(`Restoring from ${resolvedDir}\n`);
49
+
50
+ console.log(`[${++step}/${steps}] Installing ${apks.length} APK(s)...`);
51
+
52
+ const installCmd = apks.length === 1 ? 'install' : 'install-multiple';
53
+ if (verbose) console.error(`[adbex] adb ${installCmd} ${apks.join(' ')}`);
54
+
55
+ const installResult = spawnSync('adb', [installCmd, ...apks], { stdio: 'inherit' });
56
+ if (installResult.status !== 0) {
57
+ console.error('Install failed.');
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log('Installed.');
62
+
63
+ if (hasData) {
64
+ console.log(`\n[${++step}/${steps}] Pushing data...`);
65
+
66
+ const dataDir = explicitDataDir
67
+ ? path.resolve(explicitDataDir)
68
+ : path.join(resolvedDir, 'data');
69
+
70
+ if (!fs.existsSync(dataDir)) {
71
+ console.error(`Data folder not found: ${dataDir}`);
72
+ console.error('Tip: specify it explicitly with --data <path>');
73
+ process.exit(1);
74
+ }
75
+
76
+ const entries = fs.readdirSync(dataDir);
77
+ if (!entries.length) {
78
+ console.error(`Data folder is empty: ${dataDir}`);
79
+ process.exit(1);
80
+ }
81
+
82
+ const remoteDest = '/sdcard/Android/data/';
83
+ let pushFailed = false;
84
+ for (const entry of entries) {
85
+ const entryPath = path.join(dataDir, entry);
86
+ if (verbose) console.error(`[adbex] adb push "${entryPath}" "${remoteDest}"`);
87
+ const pushResult = spawnSync('adb', ['push', entryPath, remoteDest], { stdio: 'inherit' });
88
+ if (pushResult.status !== 0) pushFailed = true;
89
+ }
90
+
91
+ if (pushFailed) {
92
+ console.error(`
93
+ Data push completed with errors. Possible reasons:
94
+ - App has restricted its data folder (common on Android 11+)
95
+ - Insufficient storage on device
96
+ - File path too long for the device filesystem`);
97
+ } else {
98
+ console.log('Data restored.');
99
+ }
100
+ }
101
+
102
+ console.log('\nRestore complete.');
103
+ };
@@ -0,0 +1,37 @@
1
+ const { execFileSync, spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function screenshot(args, { verbose } = {}) {
6
+ const dest = args[0] || '.';
7
+
8
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
9
+ const fileName = `screenshot-${timestamp}.png`;
10
+ const remotePath = `/sdcard/${fileName}`;
11
+
12
+ let localPath;
13
+ if (dest === '.' || (fs.existsSync(dest) && fs.statSync(dest).isDirectory())) {
14
+ localPath = path.join(dest, fileName);
15
+ } else {
16
+ localPath = dest.endsWith('.png') ? dest : `${dest}.png`;
17
+ }
18
+
19
+ console.log('Capturing screenshot...');
20
+
21
+ try {
22
+ if (verbose) console.error(`[adbex] adb shell screencap -p ${remotePath}`);
23
+ execFileSync('adb', ['shell', 'screencap', '-p', remotePath]);
24
+
25
+ if (verbose) console.error(`[adbex] adb pull ${remotePath} ${localPath}`);
26
+ spawnSync('adb', ['pull', remotePath, localPath], { stdio: 'inherit' });
27
+
28
+ if (verbose) console.error(`[adbex] adb shell rm ${remotePath}`);
29
+ execFileSync('adb', ['shell', 'rm', remotePath]);
30
+
31
+ console.log(`Saved to ${localPath}`);
32
+ } catch (err) {
33
+ console.error(`Screenshot failed: ${err.message}`);
34
+ try { execFileSync('adb', ['shell', 'rm', remotePath]); } catch (_) {}
35
+ process.exit(1);
36
+ }
37
+ };
@@ -0,0 +1,76 @@
1
+ const { execFileSync } = require('child_process');
2
+ const readline = require('readline');
3
+
4
+ module.exports = function select(args, { verbose } = {}) {
5
+ let devicesOutput;
6
+ try {
7
+ devicesOutput = execFileSync('adb', ['devices'], { encoding: 'utf8' });
8
+ } catch {
9
+ console.error('Failed to run adb devices. Is adb in your PATH?');
10
+ process.exit(1);
11
+ }
12
+
13
+ const lines = devicesOutput.trim().split(/\r?\n/).slice(1);
14
+ const serials = lines
15
+ .map(l => l.trim())
16
+ .filter(l => l && !l.startsWith('*'))
17
+ .map(l => {
18
+ const [serial, state] = l.split(/\s+/);
19
+ return { serial, state };
20
+ })
21
+ .filter(d => d.state === 'device');
22
+
23
+ if (!serials.length) {
24
+ console.error('No devices connected.');
25
+ process.exit(1);
26
+ }
27
+
28
+ if (serials.length === 1) {
29
+ const { serial } = serials[0];
30
+ console.log(`Only one device: ${serial}`);
31
+ saveSelection(serial);
32
+ return;
33
+ }
34
+
35
+ console.log(`${serials.length} devices available. Choose one:\n`);
36
+
37
+ for (let i = 0; i < serials.length; i++) {
38
+ const { serial } = serials[i];
39
+ try {
40
+ const prop = (p) => {
41
+ if (verbose) console.error(`[adbex] adb -s ${serial} shell getprop ${p}`);
42
+ return execFileSync('adb', ['-s', serial, 'shell', 'getprop', p], { encoding: 'utf8' }).trim();
43
+ };
44
+ const model = prop('ro.product.model');
45
+ const brand = prop('ro.product.brand');
46
+ const ver = prop('ro.build.version.release');
47
+ console.log(` ${i + 1}) ${serial}`);
48
+ console.log(` ${brand} ${model} — Android ${ver}`);
49
+ } catch {
50
+ console.log(` ${i + 1}) ${serial}`);
51
+ console.log(` (could not read device info) `);
52
+ }
53
+ console.log();
54
+ }
55
+
56
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
57
+
58
+ rl.question('Enter number (1): ', (input) => {
59
+ rl.close();
60
+ const idx = parseInt(input, 10) - 1;
61
+
62
+ if (isNaN(idx) || idx < 0 || idx >= serials.length) {
63
+ console.error('Invalid selection.');
64
+ process.exit(1);
65
+ }
66
+
67
+ const { serial } = serials[idx];
68
+ saveSelection(serial);
69
+ });
70
+ };
71
+
72
+ function saveSelection(serial) {
73
+ const { load, save } = require('../config');
74
+ save({ ...load(), selectedDevice: serial });
75
+ console.log(`Default device set to: ${serial}`);
76
+ }
package/src/config.js ADDED
@@ -0,0 +1,20 @@
1
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
2
+ const { homedir } = require('os');
3
+ const { join } = require('path');
4
+
5
+ const CONFIG_PATH = join(homedir(), '.adbex.json');
6
+
7
+ function load() {
8
+ if (!existsSync(CONFIG_PATH)) return {};
9
+ try {
10
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
11
+ } catch {
12
+ return {};
13
+ }
14
+ }
15
+
16
+ function save(data) {
17
+ writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2) + '\n');
18
+ }
19
+
20
+ module.exports = { load, save };
package/src/index.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const pull = require('./commands/pull');
5
+ const apkPull = require('./commands/apk-pull');
6
+ const screenshot = require('./commands/screenshot');
7
+ const backup = require('./commands/backup');
8
+ const devices = require('./commands/devices');
9
+ const packages = require('./commands/packages');
10
+ const select = require('./commands/select');
11
+ const logcat = require('./commands/logcat');
12
+ const restore = require('./commands/restore');
13
+ const install = require('./commands/install');
14
+ const push = require('./commands/push');
15
+
16
+ const COMMANDS = {
17
+ 'pull': pull,
18
+ 'push': push,
19
+ 'apk-pull': apkPull,
20
+ 'screenshot': screenshot,
21
+ 'backup': backup,
22
+ 'restore': restore,
23
+ 'install': install,
24
+ 'devices': devices,
25
+ 'packages': packages,
26
+ 'select': select,
27
+ 'logcat': logcat,
28
+ };
29
+
30
+ const { load: loadConfig } = require('./config');
31
+ const { selectedDevice } = loadConfig();
32
+ if (selectedDevice) {
33
+ process.env.ANDROID_SERIAL = selectedDevice;
34
+ }
35
+
36
+ let args = process.argv.slice(2);
37
+
38
+ const dIdx = args.indexOf('-d');
39
+ if (dIdx !== -1 && dIdx + 1 < args.length) {
40
+ process.env.ANDROID_SERIAL = args[dIdx + 1];
41
+ args.splice(dIdx, 2);
42
+ }
43
+
44
+ const cmd = args[0];
45
+ const verbose = args.includes('--verbose') || args.includes('-v');
46
+
47
+ const cleanArgs = args.filter(a => a !== '--verbose' && a !== '-v');
48
+
49
+ if (!cmd) {
50
+ printHelp();
51
+ process.exit(0);
52
+ }
53
+
54
+ if (cmd === '--help' || cmd === '-h') {
55
+ printHelp();
56
+ process.exit(0);
57
+ }
58
+
59
+ if (COMMANDS[cmd]) {
60
+ COMMANDS[cmd](cleanArgs.slice(1), { verbose });
61
+ } else {
62
+ if (verbose) console.error(`[adbex] passing through to adb: adb ${args.join(' ')}`);
63
+ const result = spawnSync('adb', args, { stdio: 'inherit' });
64
+ process.exit(result.status ?? 1);
65
+ }
66
+
67
+ function printHelp() {
68
+ console.log(`
69
+ adbex — a friendlier adb wrapper
70
+
71
+ ADBEX COMMANDS:
72
+ pull <source...> [dest] Pull files, globs like *.png are auto-expanded
73
+ push <source...> <dest> Push files, globs like *.mp4 are auto-expanded
74
+ apk-pull <package> Pull APK(s) for an installed package
75
+ screenshot [dest] Capture screen and save locally
76
+ backup <package> [--data] Backup APK and optionally sdcard data
77
+ restore <dir> [--data] Reinstall APKs and optionally push data back
78
+ install <file|glob...> Install APK(s), supports wildcards like *.apk
79
+ devices List devices with friendly info
80
+ select Select default device (saves to ~/.adbex.json)
81
+ packages [filter] [-3] [-s] List installed packages, optionally filtered
82
+ logcat [package] Logcat for package (auto-resolves PID)
83
+
84
+ FLAGS:
85
+ -v, --verbose Show what adbex is doing under the hood
86
+ -d <serial> Target a specific device (overrides default)
87
+ -h, --help Show this help
88
+
89
+ ANY OTHER COMMAND is passed straight through to adb:
90
+ adbex shell, adbex logcat, adbex reboot, etc.
91
+ `);
92
+ }