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 +21 -0
- package/README.md +80 -0
- package/package.json +18 -0
- package/src/commands/apk-pull.js +63 -0
- package/src/commands/backup.js +90 -0
- package/src/commands/devices.js +67 -0
- package/src/commands/install.js +69 -0
- package/src/commands/logcat.js +29 -0
- package/src/commands/packages.js +39 -0
- package/src/commands/pull.js +98 -0
- package/src/commands/push.js +89 -0
- package/src/commands/restore.js +103 -0
- package/src/commands/screenshot.js +37 -0
- package/src/commands/select.js +76 -0
- package/src/config.js +20 -0
- package/src/index.js +92 -0
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
|
+
}
|