gf-packages-cli 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/.claude/settings.local.json +23 -0
- package/bin/cli.js +23 -0
- package/lib/common/appCommon.js +36 -0
- package/lib/tools/adb.js +65 -0
- package/package.json +18 -0
- package/src/deletePackage.js +43 -0
- package/src/exportPackage.js +274 -0
- package/src/index.js +45 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(powershell.exe -Command \"& 'C:\\\\Users\\\\HP\\\\AppData\\\\Roaming\\\\npm\\\\my-cli.ps1'\")",
|
|
5
|
+
"Bash(node -e \"const i = require\\('inquirer'\\); console.log\\(Object.keys\\(i.default\\)\\)\")",
|
|
6
|
+
"Bash(node -e \"const i = require\\('inquirer'\\); console.log\\('keys:', Object.keys\\(i\\)\\); console.log\\('default:', i.default\\); console.log\\('typeof default:', typeof i.default\\)\")",
|
|
7
|
+
"Bash(node -e \"const o = require\\('ora'\\); console.log\\('keys:', Object.keys\\(o\\)\\); console.log\\('typeof ora:', typeof o\\); console.log\\('isFunction:', typeof o === 'function'\\)\")",
|
|
8
|
+
"Bash(node -e \"const c = require\\('chalk'\\); console.log\\('keys:', Object.keys\\(c\\).slice\\(0,10\\)\\); console.log\\('typeof default:', typeof c.default\\); console.log\\('is chalk instance?', typeof c === 'function'\\)\")",
|
|
9
|
+
"Bash(node -e \"const i = require\\('inquirer'\\); console.log\\(i.version || 'no version'\\); console.log\\(Object.keys\\(i\\)\\)\")",
|
|
10
|
+
"Bash(npm ls:*)",
|
|
11
|
+
"Bash(node -e \"const d=require\\('fs'\\).readFileSync\\('/dev/stdin','utf8'\\); const p=JSON.parse\\(d\\); console.log\\(p.packages['node_modules/inquirer'].version\\)\")",
|
|
12
|
+
"Bash(node -e \"const d=require\\('fs'\\).readFileSync\\('/dev/stdin','utf8'\\); const p=JSON.parse\\(d\\); console.log\\('version:', p.version\\); console.log\\('type:', p.type\\)\")",
|
|
13
|
+
"Bash(node -e \"const p=require\\('d:/GfWorkbench/downloadApk/node_modules/ora/package.json'\\); console.log\\('type:', p.type, '| main:', p.main\\)\")",
|
|
14
|
+
"Bash(npm install:*)",
|
|
15
|
+
"Bash(node -e \"const p=require\\('d:/GfWorkbench/downloadApk/node_modules/jszip/package.json'\\); console.log\\('version:', p.version, '| type:', p.type, '| main:', p.main\\)\")",
|
|
16
|
+
"Bash(node -c \"lib/exportPackage.js\")",
|
|
17
|
+
"Bash(adb shell:*)",
|
|
18
|
+
"Bash(npm search:*)",
|
|
19
|
+
"Bash(npm info:*)",
|
|
20
|
+
"Bash(node -e ':*)"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const pkg = require('../package.json');
|
|
5
|
+
const showMenu = require('../src/index.js');
|
|
6
|
+
const exportCommand = require('../src/exportPackage.js');
|
|
7
|
+
const deleteCommand = require('../src/deletePackage.js');
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.version(pkg.version, '-v, --version', '查看版本')
|
|
11
|
+
.helpOption('-h, --help', '查看帮助');
|
|
12
|
+
|
|
13
|
+
// 注册 export 子命令
|
|
14
|
+
program.addCommand(exportCommand);
|
|
15
|
+
// 注册 delete 子命令
|
|
16
|
+
program.addCommand(deleteCommand);
|
|
17
|
+
// 无子命令时,显示菜单
|
|
18
|
+
program
|
|
19
|
+
.action(() => {
|
|
20
|
+
showMenu();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const chalk = require('chalk').default;
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const { runAdbSync } = require('../tools/adb.js');
|
|
4
|
+
|
|
5
|
+
// ===================== 业务逻辑 =====================
|
|
6
|
+
|
|
7
|
+
async function getThirdPartyPackages(deviceId) {
|
|
8
|
+
console.log(chalk.gray('[1] 读取手机第三方应用列表...'));
|
|
9
|
+
const raw = await runAdbSync(deviceId, 'shell pm list packages -3');
|
|
10
|
+
const pkgList = raw.split(/\r?\n/)
|
|
11
|
+
.map(line => line.trim())
|
|
12
|
+
.filter(line => line.startsWith('package:'))
|
|
13
|
+
.map(line => line.replace('package:', ''));
|
|
14
|
+
return [...new Set(pkgList)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async function selectPackages(pkgList,message) {
|
|
19
|
+
const { selectedPkgs } = await inquirer.prompt([{
|
|
20
|
+
type: 'checkbox',
|
|
21
|
+
name: 'selectedPkgs',
|
|
22
|
+
message: message,
|
|
23
|
+
choices: pkgList.map(pkg => ({ name: pkg, value: pkg })),
|
|
24
|
+
pageSize: 15
|
|
25
|
+
}]);
|
|
26
|
+
if (selectedPkgs.length === 0) {
|
|
27
|
+
console.log(chalk.yellow('未选择任何应用,程序退出'));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
return selectedPkgs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
getThirdPartyPackages,
|
|
35
|
+
selectPackages
|
|
36
|
+
}
|
package/lib/tools/adb.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const chalk = require('chalk').default;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 获取已连接的Android设备列表
|
|
7
|
+
* @returns {string[]} 设备序列号数组
|
|
8
|
+
*/
|
|
9
|
+
async function getDeviceList() {
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync('adb devices', { encoding: 'utf-8', stdio: 'pipe' });
|
|
12
|
+
const lines = output.split(/\r?\n/);
|
|
13
|
+
return lines
|
|
14
|
+
.slice(1) // 跳过 "List of devices attached"
|
|
15
|
+
.map(line => line.trim())
|
|
16
|
+
.filter(line => line && !line.startsWith('*')) // 过滤空行和daemon日志
|
|
17
|
+
.filter(line => /\tdevice$/.test(line)) // 只保留已授权的设备
|
|
18
|
+
.map(line => line.split(/\t/)[0]);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new Error(`adb 执行失败:${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// ===================== 交互式设备选择 =====================
|
|
26
|
+
async function selectDevice(devices) {
|
|
27
|
+
if (devices.length === 0) {
|
|
28
|
+
console.log(chalk.red('未检测到ADB连接设备,请检查手机USB调试'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (devices.length === 1) {
|
|
32
|
+
console.log(chalk.green(`已选中设备: ${devices[0]}`));
|
|
33
|
+
return devices[0];
|
|
34
|
+
}
|
|
35
|
+
const { device } = await inquirer.prompt([{
|
|
36
|
+
type: 'list',
|
|
37
|
+
name: 'device',
|
|
38
|
+
message: '请选择目标设备:',
|
|
39
|
+
choices: devices
|
|
40
|
+
}]);
|
|
41
|
+
console.log(chalk.green(`已选中设备: ${device}`));
|
|
42
|
+
return device;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 同步执行ADB命令
|
|
47
|
+
* @param {string|null} deviceId - 设备序列号,为空时自动选择唯一设备
|
|
48
|
+
* @param {string} cmd - 要执行的ADB命令(不含adb前缀)
|
|
49
|
+
* @returns {string} 命令执行的标准输出
|
|
50
|
+
* @throws {Error} 命令执行失败时抛出错误,包含完整的命令和错误信息
|
|
51
|
+
*/
|
|
52
|
+
async function runAdbSync(deviceId, cmd) {
|
|
53
|
+
// 根据是否指定设备ID构建完整的adb命令
|
|
54
|
+
const adbCmd = deviceId ? `adb -s ${deviceId} ${cmd}` : `adb ${cmd}`;
|
|
55
|
+
try {
|
|
56
|
+
// 同步执行命令,返回UTF-8编码的输出
|
|
57
|
+
return execSync(adbCmd, { encoding: 'utf-8', stdio: 'pipe' });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// 优先获取标准错误输出,否则使用错误对象的message
|
|
60
|
+
const errMsg = err.stderr ? err.stderr.toString().trim() : err.message;
|
|
61
|
+
throw new Error(`命令执行失败\n指令: ${adbCmd}\n错误: ${errMsg}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { getDeviceList, selectDevice ,runAdbSync};
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gf-packages-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"gf-packages-cli": "./bin/cli.js"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"adbkit-apkreader": "^3.2.0",
|
|
9
|
+
"adm-zip": "^0.5.18",
|
|
10
|
+
"chalk": "^5.6.2",
|
|
11
|
+
"commander": "^15.0.0",
|
|
12
|
+
"ejs": "^6.0.1",
|
|
13
|
+
"fs-extra": "^11.3.6",
|
|
14
|
+
"inquirer": "^8.2.7",
|
|
15
|
+
"jszip": "^3.10.1",
|
|
16
|
+
"ora": "^9.4.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk').default;
|
|
3
|
+
const { getDeviceList, selectDevice, runAdbSync } = require('../lib/tools/adb.js');
|
|
4
|
+
const { getThirdPartyPackages, selectPackages } = require('../lib/common/appCommon.js');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async function runDelete(options = {}) {
|
|
9
|
+
const { device, output, all, packages } = options;
|
|
10
|
+
|
|
11
|
+
// 1. 获取所有设备列表
|
|
12
|
+
const allDevices = await getDeviceList();
|
|
13
|
+
const targetDevice = options.device || await selectDevice(allDevices);
|
|
14
|
+
|
|
15
|
+
const allPkgs = packages || await getThirdPartyPackages(targetDevice);
|
|
16
|
+
const selectedPkgs = all
|
|
17
|
+
? allPkgs
|
|
18
|
+
: packages
|
|
19
|
+
? allPkgs
|
|
20
|
+
: await selectPackages(allPkgs, '勾选需要卸载的应用(空格选中,回车确认)');
|
|
21
|
+
|
|
22
|
+
console.log(chalk.green(`选中 ${selectedPkgs.length} 个应用,开始批量卸载\n`));
|
|
23
|
+
// 实现卸载应用的逻辑
|
|
24
|
+
for (const pkg of selectedPkgs) {
|
|
25
|
+
await runAdbSync(targetDevice, `shell pm uninstall ${pkg}`);
|
|
26
|
+
console.log(chalk.green(`已卸载应用: ${pkg}`));
|
|
27
|
+
}
|
|
28
|
+
console.log(chalk.green('所有应用卸载完成'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const deleteCommand = new Command('delete')
|
|
33
|
+
.description('从ADB设备删除应用及文件')
|
|
34
|
+
.option('-d, --device <id>', '设备序列号(单设备时自动识别)')
|
|
35
|
+
.option('-o, --output <dir>', '输出目录')
|
|
36
|
+
.option('-a, --all', '删除所有应用(跳过选择)')
|
|
37
|
+
.option('-p, --packages <pkgs...>', '指定应用名列表(空格分隔,如: -p com.xx.xx com.yy.yy)')
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
await runDelete(options);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
module.exports = deleteCommand;
|
|
43
|
+
module.exports.run = runDelete;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const chalk = require('chalk').default;
|
|
4
|
+
const { execSync, spawn } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const AdmZip = require('adm-zip');
|
|
9
|
+
const { getDeviceList, selectDevice, runAdbSync } = require('../lib/tools/adb.js');
|
|
10
|
+
const { getThirdPartyPackages, selectPackages } = require('../lib/common/appCommon.js');
|
|
11
|
+
const REMOTE_APK_SUFFIX = '.apk';
|
|
12
|
+
|
|
13
|
+
// ===================== ADB 工具函数 =====================
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async function getPackageApkPaths(deviceId, pkg) {
|
|
20
|
+
const raw = await runAdbSync(deviceId, `shell pm path ${pkg}`);
|
|
21
|
+
return raw.split(/\r?\n/)
|
|
22
|
+
.map(line => line.trim().replace('package:', ''))
|
|
23
|
+
.filter(p => p && p.endsWith(REMOTE_APK_SUFFIX));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getAppVersion(deviceId, pkg) {
|
|
27
|
+
const dump = await runAdbSync(deviceId, `shell pm dump ${pkg}`);
|
|
28
|
+
const match = dump.match(/versionName=([\w.\-+]+)/);
|
|
29
|
+
if (!match) throw new Error(`无法读取 ${pkg} 的版本号`);
|
|
30
|
+
return match[1].trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getRemoteFileSize(deviceId, remotePath) {
|
|
34
|
+
try {
|
|
35
|
+
const output = await runAdbSync(deviceId, `shell ls -l "${remotePath}"`);
|
|
36
|
+
const cols = output.trim().split(/\s+/);
|
|
37
|
+
const size = parseInt(cols[4], 10);
|
|
38
|
+
return isNaN(size) ? 0 : size;
|
|
39
|
+
} catch {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function pullApk(deviceId, remotePath, localSavePath, onProgress) {
|
|
45
|
+
const dir = path.dirname(localSavePath);
|
|
46
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
// 先获取远程文件大小
|
|
49
|
+
const totalSize = await getRemoteFileSize(deviceId, remotePath);
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
// 轮询本地文件大小算百分比(fs.statSync 极轻量,不影响传输速度)
|
|
53
|
+
let lastPercent = -1;
|
|
54
|
+
const timer = setInterval(() => {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(localSavePath)) {
|
|
57
|
+
const current = fs.statSync(localSavePath).size;
|
|
58
|
+
const percent = Math.min(Math.floor((current / totalSize) * 100), 99);
|
|
59
|
+
if (percent !== lastPercent) {
|
|
60
|
+
lastPercent = percent;
|
|
61
|
+
onProgress(percent);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch { }
|
|
65
|
+
}, 200);
|
|
66
|
+
|
|
67
|
+
const args = deviceId ? ['-s', deviceId, 'pull', remotePath, localSavePath] : ['pull', remotePath, localSavePath];
|
|
68
|
+
const proc = spawn('adb', args);
|
|
69
|
+
let errMsg = '';
|
|
70
|
+
|
|
71
|
+
proc.stderr.on('data', chunk => { errMsg += chunk.toString(); });
|
|
72
|
+
|
|
73
|
+
proc.on('close', (code) => {
|
|
74
|
+
clearInterval(timer);
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
onProgress(100);
|
|
77
|
+
resolve();
|
|
78
|
+
} else {
|
|
79
|
+
reject(new Error(errMsg.trim() || `adb pull 退出码: ${code}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
proc.on('error', (err) => {
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
reject(err);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildApks(tmpDir, targetApksPath) {
|
|
91
|
+
const zip = new AdmZip();
|
|
92
|
+
const apkFiles = fs.readdirSync(tmpDir)
|
|
93
|
+
.filter(f => f.endsWith(REMOTE_APK_SUFFIX))
|
|
94
|
+
.map(f => path.join(tmpDir, f));
|
|
95
|
+
if (apkFiles.length === 0) return false;
|
|
96
|
+
apkFiles.forEach(file => zip.addLocalFile(file));
|
|
97
|
+
zip.writeZip(targetApksPath);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ===================== 单个包导出 =====================
|
|
102
|
+
|
|
103
|
+
async function exportSinglePackage(deviceId, pkg, tmpBase, outputRoot) {
|
|
104
|
+
const tmpDir = path.join(tmpBase, pkg);
|
|
105
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
106
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
console.log(chalk.cyan(`\n----------------------------------------`));
|
|
109
|
+
console.log(chalk.white(`正在处理应用: ${pkg}`));
|
|
110
|
+
|
|
111
|
+
const version = getAppVersion(deviceId, pkg);
|
|
112
|
+
console.log(chalk.gray(`当前版本号: ${version}`));
|
|
113
|
+
|
|
114
|
+
const remoteApkPaths = await getPackageApkPaths(deviceId, pkg);
|
|
115
|
+
if (remoteApkPaths.length === 0) {
|
|
116
|
+
console.log(chalk.red(`❌ 未找到任何APK分包,跳过`));
|
|
117
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const remotePath of remoteApkPaths) {
|
|
122
|
+
const fileName = path.basename(remotePath);
|
|
123
|
+
const localPath = path.join(tmpDir, fileName);
|
|
124
|
+
|
|
125
|
+
// 旋转动画 + 百分比
|
|
126
|
+
const spinChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
127
|
+
let spinIdx = 0;
|
|
128
|
+
let curPercent = 0;
|
|
129
|
+
|
|
130
|
+
const displayTimer = setInterval(() => {
|
|
131
|
+
spinIdx = (spinIdx + 1) % spinChars.length;
|
|
132
|
+
process.stdout.write(`\r${chalk.gray(` ${spinChars[spinIdx]} 正在拉取 ${fileName} ... ${curPercent}%`)}`);
|
|
133
|
+
}, 100);
|
|
134
|
+
|
|
135
|
+
await pullApk(deviceId, remotePath, localPath, (p) => { curPercent = p; });
|
|
136
|
+
clearInterval(displayTimer);
|
|
137
|
+
process.stdout.write(`\r${chalk.green(` ✔ 拉取完成 ${fileName} ... 100%`)}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const apksFileName = `${pkg}_${version}.apks`;
|
|
141
|
+
const targetApksPath = path.join(outputRoot, apksFileName);
|
|
142
|
+
const buildSuccess = buildApks(tmpDir, targetApksPath);
|
|
143
|
+
|
|
144
|
+
if (buildSuccess) {
|
|
145
|
+
console.log(chalk.green(`✅ 导出完成: ${apksFileName}`));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(chalk.red(`❌ 打包失败,无有效APK文件`));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
// ===================== 配置持久化 =====================
|
|
161
|
+
|
|
162
|
+
const CONFIG_PATH = path.join(os.homedir(), '.my-cli-config.json');
|
|
163
|
+
|
|
164
|
+
function loadConfig() {
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
167
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
168
|
+
}
|
|
169
|
+
} catch { }
|
|
170
|
+
return {};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function saveConfig(config) {
|
|
174
|
+
try {
|
|
175
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
176
|
+
} catch { }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ===================== 交互式输出目录 =====================
|
|
180
|
+
|
|
181
|
+
async function askOutputDirectory() {
|
|
182
|
+
const config = loadConfig();
|
|
183
|
+
if (config.outputDir) {
|
|
184
|
+
const { reuse } = await inquirer.prompt([{
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'reuse',
|
|
187
|
+
message: `是否使用上次的导出目录?`,
|
|
188
|
+
default: true
|
|
189
|
+
}]);
|
|
190
|
+
if (reuse) {
|
|
191
|
+
console.log(chalk.gray(`使用上次目录: ${config.outputDir}`));
|
|
192
|
+
return config.outputDir;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const { dir } = await inquirer.prompt([{
|
|
196
|
+
type: 'input',
|
|
197
|
+
name: 'dir',
|
|
198
|
+
message: '请输入导出目录路径:',
|
|
199
|
+
default: config.outputDir || path.resolve('./output_apks'),
|
|
200
|
+
validate: input => input.trim() ? true : '路径不能为空'
|
|
201
|
+
}]);
|
|
202
|
+
const resolved = path.resolve(dir.trim());
|
|
203
|
+
saveConfig({ outputDir: resolved });
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ===================== 核心导出逻辑(供命令行和菜单共同使用) =====================
|
|
208
|
+
|
|
209
|
+
async function runExport(options = {}) {
|
|
210
|
+
const { device, output, all, packages } = options;
|
|
211
|
+
const tmpBase = path.resolve('./tmp_export');
|
|
212
|
+
const outputRoot = output ? path.resolve(output) : await askOutputDirectory();
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
if (!fs.existsSync(outputRoot)) fs.mkdirSync(outputRoot, { recursive: true });
|
|
216
|
+
if (!fs.existsSync(tmpBase)) fs.mkdirSync(tmpBase, { recursive: true });
|
|
217
|
+
|
|
218
|
+
const allDevices = await getDeviceList();
|
|
219
|
+
const targetDevice = device || await selectDevice(allDevices);
|
|
220
|
+
|
|
221
|
+
const allPkgs = packages || await getThirdPartyPackages(targetDevice);
|
|
222
|
+
const selectedPkgs = all
|
|
223
|
+
? allPkgs
|
|
224
|
+
: packages
|
|
225
|
+
? allPkgs
|
|
226
|
+
: await selectPackages(allPkgs, '勾选需要导出APKS的应用(空格选中,回车确认)');
|
|
227
|
+
|
|
228
|
+
console.log(chalk.green(`选中 ${selectedPkgs.length} 个应用,开始批量导出\n`));
|
|
229
|
+
|
|
230
|
+
for (const pkg of selectedPkgs) {
|
|
231
|
+
await exportSinglePackage(targetDevice, pkg, tmpBase, outputRoot);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 导出完成后清理临时目录
|
|
235
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
236
|
+
|
|
237
|
+
// 询问是否卸载已导出的应用
|
|
238
|
+
const { shouldUninstall } = await inquirer.prompt([{
|
|
239
|
+
type: 'confirm',
|
|
240
|
+
name: 'shouldUninstall',
|
|
241
|
+
message: '是否卸载已导出的应用?',
|
|
242
|
+
default: false
|
|
243
|
+
}]);
|
|
244
|
+
|
|
245
|
+
if (shouldUninstall) {
|
|
246
|
+
for (const pkg of selectedPkgs) {
|
|
247
|
+
await runAdbSync(targetDevice, `shell pm uninstall ${pkg}`);
|
|
248
|
+
console.log(chalk.green(`已卸载应用: ${pkg}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(chalk.bold.cyan(`\n===== 全部导出完成 =====`));
|
|
253
|
+
console.log(chalk.blue(`文件输出目录: ${outputRoot}`));
|
|
254
|
+
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.log(chalk.red(`\n程序异常: ${err.message}`));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ===================== Commander 命令定义 =====================
|
|
262
|
+
|
|
263
|
+
const exportCommand = new Command('export')
|
|
264
|
+
.description('从ADB设备导出第三方应用APKS')
|
|
265
|
+
.option('-d, --device <id>', '设备序列号(单设备时自动识别)')
|
|
266
|
+
.option('-o, --output <dir>', '输出目录')
|
|
267
|
+
.option('-a, --all', '导出所有第三方应用(跳过选择)')
|
|
268
|
+
.option('-p, --packages <pkgs...>', '指定包名列表(空格分隔,如: -p com.xx.xx com.yy.yy)')
|
|
269
|
+
.action(async (options) => {
|
|
270
|
+
await runExport(options);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
module.exports = exportCommand;
|
|
274
|
+
module.exports.run = runExport;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk').default;
|
|
3
|
+
const { run: runExport } = require('./exportPackage');
|
|
4
|
+
const { run: runDelete } = require('./deletePackage');
|
|
5
|
+
async function showMenu() {
|
|
6
|
+
console.log('\n📦 APP包工具\n');
|
|
7
|
+
|
|
8
|
+
const { action } = await inquirer.prompt([
|
|
9
|
+
{
|
|
10
|
+
type: 'list',
|
|
11
|
+
name: 'action',
|
|
12
|
+
message: '请选择包功能:',
|
|
13
|
+
choices: [
|
|
14
|
+
{
|
|
15
|
+
name: '📤 导出指定包',
|
|
16
|
+
description: '导出指定包到本地目录',
|
|
17
|
+
value: 'export'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: '🚫 删除设备指定APP及文件',
|
|
21
|
+
description: '删除设备指定APP及文件',
|
|
22
|
+
value: 'delete'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: '❌ 退出',
|
|
26
|
+
value: 'exit'
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
switch (action) {
|
|
33
|
+
case 'export':
|
|
34
|
+
await runExport();
|
|
35
|
+
break;
|
|
36
|
+
case 'delete':
|
|
37
|
+
await runDelete();
|
|
38
|
+
break;
|
|
39
|
+
case 'exit':
|
|
40
|
+
console.log(chalk.gray('已退出'));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = showMenu;
|