damon-devtools 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. damon_devtools-0.3.0/PKG-INFO +126 -0
  2. damon_devtools-0.3.0/README.md +115 -0
  3. damon_devtools-0.3.0/damon_devtools.egg-info/PKG-INFO +126 -0
  4. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/__main__.py +12 -2
  5. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/launcher.py +2 -1
  6. damon_devtools-0.3.0/devtools/tools/apkpull/core.py +303 -0
  7. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/apkpull/ui.py +27 -14
  8. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/pyproject.toml +1 -1
  9. damon_devtools-0.1.0/PKG-INFO +0 -46
  10. damon_devtools-0.1.0/README.md +0 -35
  11. damon_devtools-0.1.0/damon_devtools.egg-info/PKG-INFO +0 -46
  12. damon_devtools-0.1.0/devtools/tools/apkpull/core.py +0 -123
  13. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/damon_devtools.egg-info/SOURCES.txt +0 -0
  14. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/damon_devtools.egg-info/dependency_links.txt +0 -0
  15. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/damon_devtools.egg-info/entry_points.txt +0 -0
  16. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/damon_devtools.egg-info/requires.txt +0 -0
  17. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/damon_devtools.egg-info/top_level.txt +0 -0
  18. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/__init__.py +0 -0
  19. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/__init__.py +0 -0
  20. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/apkpull/__init__.py +0 -0
  21. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/apkpull/__main__.py +0 -0
  22. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/apkpull/models.py +0 -0
  23. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/portkill/__init__.py +0 -0
  24. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/portkill/__main__.py +0 -0
  25. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/portkill/core.py +0 -0
  26. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/portkill/models.py +0 -0
  27. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/portkill/ui.py +0 -0
  28. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/removebg/__init__.py +0 -0
  29. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/removebg/__main__.py +0 -0
  30. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/removebg/cli.py +0 -0
  31. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/tools/removebg/core.py +0 -0
  32. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/devtools/types.py +0 -0
  33. {damon_devtools-0.1.0 → damon_devtools-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: damon-devtools
3
+ Version: 0.3.0
4
+ Summary: Developer CLI tools — portkill, removebg, apkpull
5
+ Author: damon
6
+ License: MIT
7
+ Keywords: cli,devtools,android,apk,adb
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: psutil>=5.9
11
+
12
+ # devtools
13
+
14
+ Developer CLI tools: portkill, removebg, apkpull.
15
+
16
+ ---
17
+
18
+ ## Tools
19
+
20
+ ### portkill
21
+
22
+ Kill Node.js processes by port or scan all node processes.
23
+
24
+ ```bash
25
+ python -m devtools portkill
26
+ python -m devtools portkill --listen-only
27
+ ```
28
+
29
+ ### apkpull
30
+
31
+ Pull APK files from Android device via ADB.
32
+
33
+ Features:
34
+ - List and search third-party apps
35
+ - Fast app name lookup via aapt2
36
+ - Auto-download aapt2 if not found locally
37
+ - Multi-device support
38
+ - Label caching for faster subsequent runs
39
+
40
+ ```bash
41
+ # Interactive mode
42
+ python -m devtools apkpull
43
+
44
+ # Specify target directory
45
+ python -m devtools apkpull /path/to/output
46
+
47
+ # Search apps by keyword
48
+ python -m devtools apkpull -s wechat
49
+ ```
50
+
51
+ ### removebg
52
+
53
+ Remove image backgrounds using the remove.bg API.
54
+
55
+ ```bash
56
+ python -m devtools removebg input.jpg
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install damon-devtools
65
+ ```
66
+
67
+ Requires: Python >= 3.10
68
+
69
+ ---
70
+
71
+ # devtools
72
+
73
+ 开发者命令行工具集:portkill、removebg、apkpull。
74
+
75
+ ---
76
+
77
+ ## 工具
78
+
79
+ ### portkill
80
+
81
+ 根据端口查找并终止 Node.js 进程,或扫描所有 node 进程。
82
+
83
+ ```bash
84
+ python -m devtools portkill
85
+ python -m devtools portkill --listen-only
86
+ ```
87
+
88
+ ### apkpull
89
+
90
+ 通过 ADB 从 Android 设备拉取 APK 安装包。
91
+
92
+ 功能特点:
93
+ - 列出和搜索第三方应用
94
+ - 通过 aapt2 快速获取应用名
95
+ - 本地未找到 aapt2 时自动下载
96
+ - 多设备支持
97
+ - 应用名缓存,加速后续运行
98
+
99
+ ```bash
100
+ # 交互模式
101
+ python -m devtools apkpull
102
+
103
+ # 指定目标目录
104
+ python -m devtools apkpull /path/to/output
105
+
106
+ # 搜索应用
107
+ python -m devtools apkpull -s wechat
108
+ ```
109
+
110
+ ### removebg
111
+
112
+ 使用 remove.bg API 去除图片背景。
113
+
114
+ ```bash
115
+ python -m devtools removebg input.jpg
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 安装
121
+
122
+ ```bash
123
+ pip install damon-devtools
124
+ ```
125
+
126
+ 要求:Python >= 3.10
@@ -0,0 +1,115 @@
1
+ # devtools
2
+
3
+ Developer CLI tools: portkill, removebg, apkpull.
4
+
5
+ ---
6
+
7
+ ## Tools
8
+
9
+ ### portkill
10
+
11
+ Kill Node.js processes by port or scan all node processes.
12
+
13
+ ```bash
14
+ python -m devtools portkill
15
+ python -m devtools portkill --listen-only
16
+ ```
17
+
18
+ ### apkpull
19
+
20
+ Pull APK files from Android device via ADB.
21
+
22
+ Features:
23
+ - List and search third-party apps
24
+ - Fast app name lookup via aapt2
25
+ - Auto-download aapt2 if not found locally
26
+ - Multi-device support
27
+ - Label caching for faster subsequent runs
28
+
29
+ ```bash
30
+ # Interactive mode
31
+ python -m devtools apkpull
32
+
33
+ # Specify target directory
34
+ python -m devtools apkpull /path/to/output
35
+
36
+ # Search apps by keyword
37
+ python -m devtools apkpull -s wechat
38
+ ```
39
+
40
+ ### removebg
41
+
42
+ Remove image backgrounds using the remove.bg API.
43
+
44
+ ```bash
45
+ python -m devtools removebg input.jpg
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install damon-devtools
54
+ ```
55
+
56
+ Requires: Python >= 3.10
57
+
58
+ ---
59
+
60
+ # devtools
61
+
62
+ 开发者命令行工具集:portkill、removebg、apkpull。
63
+
64
+ ---
65
+
66
+ ## 工具
67
+
68
+ ### portkill
69
+
70
+ 根据端口查找并终止 Node.js 进程,或扫描所有 node 进程。
71
+
72
+ ```bash
73
+ python -m devtools portkill
74
+ python -m devtools portkill --listen-only
75
+ ```
76
+
77
+ ### apkpull
78
+
79
+ 通过 ADB 从 Android 设备拉取 APK 安装包。
80
+
81
+ 功能特点:
82
+ - 列出和搜索第三方应用
83
+ - 通过 aapt2 快速获取应用名
84
+ - 本地未找到 aapt2 时自动下载
85
+ - 多设备支持
86
+ - 应用名缓存,加速后续运行
87
+
88
+ ```bash
89
+ # 交互模式
90
+ python -m devtools apkpull
91
+
92
+ # 指定目标目录
93
+ python -m devtools apkpull /path/to/output
94
+
95
+ # 搜索应用
96
+ python -m devtools apkpull -s wechat
97
+ ```
98
+
99
+ ### removebg
100
+
101
+ 使用 remove.bg API 去除图片背景。
102
+
103
+ ```bash
104
+ python -m devtools removebg input.jpg
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 安装
110
+
111
+ ```bash
112
+ pip install damon-devtools
113
+ ```
114
+
115
+ 要求:Python >= 3.10
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: damon-devtools
3
+ Version: 0.3.0
4
+ Summary: Developer CLI tools — portkill, removebg, apkpull
5
+ Author: damon
6
+ License: MIT
7
+ Keywords: cli,devtools,android,apk,adb
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: psutil>=5.9
11
+
12
+ # devtools
13
+
14
+ Developer CLI tools: portkill, removebg, apkpull.
15
+
16
+ ---
17
+
18
+ ## Tools
19
+
20
+ ### portkill
21
+
22
+ Kill Node.js processes by port or scan all node processes.
23
+
24
+ ```bash
25
+ python -m devtools portkill
26
+ python -m devtools portkill --listen-only
27
+ ```
28
+
29
+ ### apkpull
30
+
31
+ Pull APK files from Android device via ADB.
32
+
33
+ Features:
34
+ - List and search third-party apps
35
+ - Fast app name lookup via aapt2
36
+ - Auto-download aapt2 if not found locally
37
+ - Multi-device support
38
+ - Label caching for faster subsequent runs
39
+
40
+ ```bash
41
+ # Interactive mode
42
+ python -m devtools apkpull
43
+
44
+ # Specify target directory
45
+ python -m devtools apkpull /path/to/output
46
+
47
+ # Search apps by keyword
48
+ python -m devtools apkpull -s wechat
49
+ ```
50
+
51
+ ### removebg
52
+
53
+ Remove image backgrounds using the remove.bg API.
54
+
55
+ ```bash
56
+ python -m devtools removebg input.jpg
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install damon-devtools
65
+ ```
66
+
67
+ Requires: Python >= 3.10
68
+
69
+ ---
70
+
71
+ # devtools
72
+
73
+ 开发者命令行工具集:portkill、removebg、apkpull。
74
+
75
+ ---
76
+
77
+ ## 工具
78
+
79
+ ### portkill
80
+
81
+ 根据端口查找并终止 Node.js 进程,或扫描所有 node 进程。
82
+
83
+ ```bash
84
+ python -m devtools portkill
85
+ python -m devtools portkill --listen-only
86
+ ```
87
+
88
+ ### apkpull
89
+
90
+ 通过 ADB 从 Android 设备拉取 APK 安装包。
91
+
92
+ 功能特点:
93
+ - 列出和搜索第三方应用
94
+ - 通过 aapt2 快速获取应用名
95
+ - 本地未找到 aapt2 时自动下载
96
+ - 多设备支持
97
+ - 应用名缓存,加速后续运行
98
+
99
+ ```bash
100
+ # 交互模式
101
+ python -m devtools apkpull
102
+
103
+ # 指定目标目录
104
+ python -m devtools apkpull /path/to/output
105
+
106
+ # 搜索应用
107
+ python -m devtools apkpull -s wechat
108
+ ```
109
+
110
+ ### removebg
111
+
112
+ 使用 remove.bg API 去除图片背景。
113
+
114
+ ```bash
115
+ python -m devtools removebg input.jpg
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 安装
121
+
122
+ ```bash
123
+ pip install damon-devtools
124
+ ```
125
+
126
+ 要求:Python >= 3.10
@@ -1,7 +1,11 @@
1
1
  import sys
2
+ import argparse
2
3
  from importlib import import_module
4
+ from importlib.metadata import version as get_version
3
5
  from devtools.tools import get_tool
4
6
 
7
+ VERSION = get_version("damon-devtools")
8
+
5
9
 
6
10
  def run_tool(tool_name: str, args: list[str]):
7
11
  tool = get_tool(tool_name)
@@ -41,8 +45,14 @@ def run_tool(tool_name: str, args: list[str]):
41
45
 
42
46
 
43
47
  def main():
44
- if len(sys.argv) > 1:
45
- run_tool(sys.argv[1], sys.argv[2:])
48
+ parser = argparse.ArgumentParser(prog="devtools", description="Developer CLI tools")
49
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {VERSION}")
50
+ parser.add_argument("tool", nargs="?", help="Tool name to run")
51
+ parser.add_argument("args", nargs="*", help="Arguments for the tool")
52
+ parsed = parser.parse_args()
53
+
54
+ if parsed.tool:
55
+ run_tool(parsed.tool, parsed.args)
46
56
  else:
47
57
  from devtools.launcher import render_launcher
48
58
  result = render_launcher()
@@ -3,6 +3,7 @@ if sys.platform == "win32":
3
3
  sys.stdout.reconfigure(encoding="utf-8")
4
4
 
5
5
  from devtools.tools import TOOLS
6
+ from devtools.__main__ import VERSION
6
7
 
7
8
 
8
9
  def render_launcher():
@@ -16,7 +17,7 @@ def render_launcher():
16
17
 
17
18
  print()
18
19
  print("=" * 50)
19
- print(" devtools — 开发者工具箱")
20
+ print(f" devtools {VERSION} — 开发者工具箱")
20
21
  print("=" * 50)
21
22
  print()
22
23
  for i, tool in enumerate(TOOLS):
@@ -0,0 +1,303 @@
1
+ import os
2
+ import subprocess
3
+ import re
4
+ import tempfile
5
+ import urllib.request
6
+ import json
7
+
8
+ # Local aapt2 binary directory
9
+ AAPT2_LOCAL_DIR = os.path.join(
10
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
11
+ "deps", "android-tools", "build"
12
+ )
13
+
14
+ # Remote aapt2 download base URL (android-11.0.0_r33)
15
+ AAPT2_REMOTE_URL = "https://github.com/JonForShort/android-tools/raw/refs/heads/master/build/android-11.0.0_r33/aapt2/{arch}/bin/aapt2"
16
+
17
+ # Device-side aapt2 path
18
+ AAPT2_DEVICE_PATH = "/data/local/tmp/aapt2"
19
+
20
+ # App name cache
21
+ LABEL_CACHE_FILE = os.path.expanduser("~/.devtools/cache/apkpull/label_cache.json")
22
+
23
+ def load_label_cache() -> dict[str, str]:
24
+ """Load cached app labels from file"""
25
+ if os.path.isfile(LABEL_CACHE_FILE):
26
+ try:
27
+ with open(LABEL_CACHE_FILE, "r", encoding="utf-8") as f:
28
+ return json.load(f)
29
+ except Exception:
30
+ return {}
31
+ return {}
32
+
33
+ def save_label_cache(cache: dict[str, str]) -> None:
34
+ """Save app labels cache to file"""
35
+ try:
36
+ os.makedirs(os.path.dirname(LABEL_CACHE_FILE), exist_ok=True)
37
+ with open(LABEL_CACHE_FILE, "w", encoding="utf-8") as f:
38
+ json.dump(cache, f, ensure_ascii=False, indent=2)
39
+ except Exception:
40
+ pass
41
+
42
+ # Global cache
43
+ _label_cache: dict[str, str] | None = None
44
+
45
+ def get_label_cache() -> dict[str, str]:
46
+ """Get or initialize the global label cache"""
47
+ global _label_cache
48
+ if _label_cache is None:
49
+ _label_cache = load_label_cache()
50
+ return _label_cache
51
+
52
+ def check_adb() -> bool:
53
+ """检查 ADB 是否可用"""
54
+ try:
55
+ subprocess.run(["adb", "version"], capture_output=True, check=True)
56
+ return True
57
+ except (subprocess.CalledProcessError, FileNotFoundError):
58
+ return False
59
+
60
+ def get_device_serial() -> str | None:
61
+ """获取第一个已连接的设备序列号,多设备时返回第一个"""
62
+ try:
63
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
64
+ lines = result.stdout.strip().split("\n")
65
+ for line in lines[1:]:
66
+ if line.strip() and not line.startswith("*"):
67
+ parts = line.split()
68
+ if len(parts) >= 2 and parts[1] == "device":
69
+ return parts[0]
70
+ return None
71
+ except (subprocess.CalledProcessError, FileNotFoundError):
72
+ return None
73
+
74
+ def check_device() -> bool:
75
+ """检查设备是否连接"""
76
+ return get_device_serial() is not None
77
+
78
+ def adb_cmd(args: list[str], serial: str | None = None) -> list[str]:
79
+ """构建带 -s 参数的 adb 命令"""
80
+ if serial:
81
+ return ["adb", "-s", serial] + args
82
+ return ["adb"] + args
83
+
84
+ def check_aapt2_on_device() -> bool:
85
+ """Check if /data/local/tmp/aapt2 exists on device"""
86
+ serial = get_device_serial()
87
+ try:
88
+ result = subprocess.run(
89
+ adb_cmd(["shell", "test", "-f", AAPT2_DEVICE_PATH], serial),
90
+ capture_output=True, check=False
91
+ )
92
+ return result.returncode == 0
93
+ except (subprocess.CalledProcessError, FileNotFoundError):
94
+ return False
95
+
96
+ def detect_device_arch() -> str | None:
97
+ """Get device CPU architecture via getprop"""
98
+ serial = get_device_serial()
99
+ try:
100
+ result = subprocess.run(
101
+ adb_cmd(["shell", "getprop", "ro.product.cpu.abi"], serial),
102
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
103
+ )
104
+ arch = result.stdout.strip()
105
+ return arch if arch else None
106
+ except (subprocess.CalledProcessError, FileNotFoundError):
107
+ return None
108
+
109
+ # Local aapt2 dir name -> device arch values
110
+ ARCH_MAP = {
111
+ "arm64-v8a": ["arm64-v8a", "aarch64", "arm64"],
112
+ "armeabi-v7a": ["armeabi-v7a", "armeabi", "arm"],
113
+ "x86": ["x86", "i386", "i686"],
114
+ "x86_64": ["x86_64", "x64", "amd64"],
115
+ }
116
+
117
+ def find_local_aapt2(arch: str) -> str | None:
118
+ """Find local aapt2 binary matching device arch"""
119
+ if not os.path.isdir(AAPT2_LOCAL_DIR):
120
+ return None
121
+ for version in sorted(os.listdir(AAPT2_LOCAL_DIR), reverse=True):
122
+ version_path = os.path.join(AAPT2_LOCAL_DIR, version, "aapt2")
123
+ if not os.path.isdir(version_path):
124
+ continue
125
+ for arch_dir, aliases in ARCH_MAP.items():
126
+ if arch in aliases:
127
+ aapt2_path = os.path.join(version_path, arch_dir, "bin", "aapt2")
128
+ if os.path.isfile(aapt2_path):
129
+ return aapt2_path
130
+ return None
131
+
132
+ def download_aapt2(arch: str) -> str | None:
133
+ """Download aapt2 from GitHub and cache locally. Returns local path or None on failure."""
134
+ # Map device arch to download arch
135
+ arch_to_url_arch = {
136
+ "arm64-v8a": "arm64-v8a",
137
+ "aarch64": "arm64-v8a",
138
+ "arm64": "arm64-v8a",
139
+ "armeabi-v7a": "armeabi-v7a",
140
+ "armeabi": "armeabi-v7a",
141
+ "arm": "armeabi-v7a",
142
+ "x86": "x86",
143
+ "i386": "x86",
144
+ "i686": "x86",
145
+ "x86_64": "x86_64",
146
+ "x64": "x86_64",
147
+ "amd64": "x86_64",
148
+ }
149
+ url_arch = arch_to_url_arch.get(arch, arch)
150
+ url = AAPT2_REMOTE_URL.format(arch=url_arch)
151
+
152
+ # Cache path: AAPT2_LOCAL_DIR/android-11.0.0_r33/aapt2/{arch}/bin/aapt2
153
+ cache_dir = os.path.join(AAPT2_LOCAL_DIR, "android-11.0.0_r33", "aapt2", url_arch, "bin")
154
+ cache_path = os.path.join(cache_dir, "aapt2")
155
+
156
+ if os.path.isfile(cache_path):
157
+ return cache_path
158
+
159
+ try:
160
+ os.makedirs(cache_dir, exist_ok=True)
161
+ print(f"正在下载 aapt2 ({arch})...")
162
+ urllib.request.urlretrieve(url, cache_path)
163
+ # Make executable
164
+ os.chmod(cache_path, 0o755)
165
+ return cache_path
166
+ except Exception as e:
167
+ print(f"下载失败: {e}")
168
+ return None
169
+
170
+ def ensure_aapt2_on_device() -> bool:
171
+ """Push local aapt2 to device if missing; return success"""
172
+ if check_aapt2_on_device():
173
+ return True
174
+
175
+ serial = get_device_serial()
176
+ arch = detect_device_arch()
177
+ if not arch:
178
+ print("警告: 无法获取设备架构,使用 arm64-v8a")
179
+ arch = "arm64-v8a"
180
+
181
+ local_aapt2 = find_local_aapt2(arch)
182
+ if not local_aapt2:
183
+ print(f"本地未找到 aapt2 ({arch}),尝试下载...")
184
+ local_aapt2 = download_aapt2(arch)
185
+ if not local_aapt2:
186
+ print(f"警告: 下载失败,回退到 pm dump 方式")
187
+ return False
188
+
189
+ try:
190
+ print(f"正在推送 aapt2 ({arch}) 到设备...")
191
+ subprocess.run(
192
+ adb_cmd(["push", local_aapt2, AAPT2_DEVICE_PATH], serial),
193
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
194
+ )
195
+ subprocess.run(
196
+ adb_cmd(["shell", "chmod", "0755", AAPT2_DEVICE_PATH], serial),
197
+ capture_output=True, check=True
198
+ )
199
+ return True
200
+ except (subprocess.CalledProcessError, FileNotFoundError):
201
+ return False
202
+
203
+ def scan_packages() -> list[str]:
204
+ """执行 adb shell pm list packages -3,返回包名列表"""
205
+ serial = get_device_serial()
206
+ result = subprocess.run(
207
+ adb_cmd(["shell", "pm", "list", "packages", "-3"], serial),
208
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
209
+ )
210
+ pkgs = []
211
+ for line in result.stdout.strip().split("\n"):
212
+ line = line.strip()
213
+ if line.startswith("package:"):
214
+ pkgs.append(line[8:])
215
+ return pkgs
216
+
217
+ def get_apk_path(pkg: str) -> str | None:
218
+ """执行 pm path,返回 APK 物理路径"""
219
+ serial = get_device_serial()
220
+ try:
221
+ result = subprocess.run(
222
+ adb_cmd(["shell", "pm", "path", pkg], serial),
223
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
224
+ )
225
+ for line in result.stdout.strip().split("\n"):
226
+ if line.startswith("package:"):
227
+ return line[8:].strip()
228
+ return None
229
+ except (subprocess.CalledProcessError, FileNotFoundError):
230
+ return None
231
+
232
+ def get_app_name_pm_dump(pkg: str) -> str:
233
+ """通过设备端 pm dump 提取 application label(失败时回退包名)"""
234
+ serial = get_device_serial()
235
+ try:
236
+ result = subprocess.run(
237
+ adb_cmd(["shell", "pm", "dump", pkg], serial),
238
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
239
+ )
240
+ if result.returncode != 0:
241
+ return pkg
242
+ for line in result.stdout.splitlines():
243
+ # application-label:'Facebook'
244
+ # application-label-zh-CN:'微信'
245
+ m = re.search(r"application-label(?:-[^:]+)?:'(.+?)'", line)
246
+ if m:
247
+ return m.group(1)
248
+ return pkg
249
+ except Exception:
250
+ return pkg
251
+
252
+ def get_app_name_aapt2(pkg: str) -> str | None:
253
+ """Use device-side aapt2 dump badging to get app label. Returns None on failure."""
254
+ apk_path = get_apk_path(pkg)
255
+ if not apk_path:
256
+ return None
257
+ serial = get_device_serial()
258
+ try:
259
+ cmd = f'/data/local/tmp/aapt2 dump badging "{apk_path}"'
260
+ result = subprocess.run(
261
+ adb_cmd(["shell", cmd], serial),
262
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
263
+ )
264
+ if result.returncode != 0:
265
+ return None
266
+ for line in result.stdout.splitlines():
267
+ m = re.search(r"^application-label:'(.+?)'", line)
268
+ if m:
269
+ return m.group(1)
270
+ return None
271
+ except (subprocess.CalledProcessError, FileNotFoundError):
272
+ return None
273
+
274
+ def get_app_name(pkg: str) -> str:
275
+ """Get app label: check cache first, then fetch and cache"""
276
+ cache = get_label_cache()
277
+ if pkg in cache:
278
+ return cache[pkg]
279
+
280
+ label = get_app_name_aapt2(pkg)
281
+ if not label:
282
+ label = get_app_name_pm_dump(pkg)
283
+
284
+ cache[pkg] = label
285
+ save_label_cache(cache)
286
+ return label
287
+
288
+ def sanitize_filename(name: str) -> str:
289
+ """Sanitize filename for cross-platform compatibility."""
290
+ sanitized = re.sub(r'[\\/:*?"<>|]+', "_", name).strip().rstrip(".")
291
+ return sanitized or "unknown"
292
+
293
+ def pull_apk(apk_path: str, dest_path: str) -> bool:
294
+ """执行 adb pull,拉取 APK 到目标文件路径"""
295
+ serial = get_device_serial()
296
+ try:
297
+ subprocess.run(
298
+ adb_cmd(["pull", apk_path, dest_path], serial),
299
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
300
+ )
301
+ return True
302
+ except (subprocess.CalledProcessError, FileNotFoundError):
303
+ return False
@@ -3,12 +3,13 @@ if sys.platform == "win32":
3
3
  sys.stdout.reconfigure(encoding="utf-8")
4
4
 
5
5
  import os
6
+ import argparse
6
7
  import threading
7
8
  from devtools.tools.apkpull.models import PackageInfo
8
9
 
9
10
  def scan_packages_with_labels() -> list[PackageInfo]:
10
11
  """Scan packages and fetch labels in parallel threads (max 10 concurrent)"""
11
- from devtools.tools.apkpull.core import scan_packages, check_adb, check_device, get_app_name
12
+ from devtools.tools.apkpull.core import scan_packages, check_adb, check_device, get_app_name, ensure_aapt2_on_device
12
13
 
13
14
  if not check_adb():
14
15
  print("错误: 未找到 adb 命令,请确保 Android SDK platform-tools 已安装并加入 PATH")
@@ -17,6 +18,7 @@ def scan_packages_with_labels() -> list[PackageInfo]:
17
18
  print("错误: 未检测到设备,请确保手机已通过 USB 连接并开启了 USB 调试模式")
18
19
  return []
19
20
 
21
+ ensure_aapt2_on_device()
20
22
  print("正在获取应用列表...")
21
23
  pkgs = scan_packages()
22
24
  if not pkgs:
@@ -53,12 +55,12 @@ def scan_packages_with_labels() -> list[PackageInfo]:
53
55
 
54
56
  def show_packages(packages: list[PackageInfo], short: bool = True) -> None:
55
57
  print()
56
- print(f"{'#':<5} {'应用名':<30} {'包名'}")
58
+ print(f"{'#':<5} {'包名':<50} {'应用名'}")
57
59
  print("-" * 90)
58
60
  for i, p in enumerate(packages):
59
- label = p.label[:28] + ".." if short and len(p.label) > 30 else p.label
60
- pkg = p.pkg[:50] if short and len(p.pkg) > 50 else p.pkg
61
- print(f"{i+1:<5} {label:<30} {pkg}")
61
+ pkg = p.pkg[:48] + ".." if short and len(p.pkg) > 50 else p.pkg
62
+ label = p.label[:38] + ".." if short and len(p.label) > 40 else p.label
63
+ print(f"{i+1:<5} {pkg:<50} {label}")
62
64
 
63
65
  def select_packages(packages: list[PackageInfo], user_input: str) -> list[PackageInfo]:
64
66
  """Parse user input: keyword search or numeric selection (1,3-5, 1,3,5)"""
@@ -108,17 +110,26 @@ def run(args: list[str] | None = None):
108
110
  if args is None:
109
111
  args = []
110
112
 
111
- # Determine destination directory
112
- if args:
113
- dest_dir = args[0]
114
- else:
115
- dest_dir = os.getcwd()
113
+ parser = argparse.ArgumentParser(prog="apkpull", description="Pull APK from Android device")
114
+ parser.add_argument("dest_dir", nargs="?", default=os.getcwd(), help="目标目录 (默认当前目录)")
115
+ parser.add_argument("-s", "--search", metavar="KEYWORD", help="模糊搜索应用名/包名并退出")
116
+
117
+ parsed = parser.parse_args(args)
116
118
 
117
119
  packages = scan_packages_with_labels()
118
120
  if not packages:
119
121
  return
120
122
 
121
- show_packages(packages)
123
+ # Fuzzy search mode: pre-filter packages and enter selection loop
124
+ if parsed.search:
125
+ keyword = parsed.search.lower()
126
+ packages = [p for p in packages if keyword in p.label.lower() or keyword in p.pkg.lower()]
127
+ if not packages:
128
+ print(f"未找到包含 '{parsed.search}' 的应用")
129
+ return
130
+ show_packages(packages)
131
+ else:
132
+ show_packages(packages)
122
133
 
123
134
  while True:
124
135
  print()
@@ -142,12 +153,12 @@ def run(args: list[str] | None = None):
142
153
 
143
154
  show_packages(selected, short=False)
144
155
 
145
- if not confirm_pull(selected, dest_dir):
156
+ if not confirm_pull(selected, parsed.dest_dir):
146
157
  print("取消拉取")
147
158
  continue
148
159
 
149
160
  # Pull APKs
150
- from devtools.tools.apkpull.core import get_apk_path, pull_apk
161
+ from devtools.tools.apkpull.core import get_apk_path, pull_apk, sanitize_filename
151
162
  succeeded: list[str] = []
152
163
  failed: list[tuple] = []
153
164
 
@@ -156,7 +167,9 @@ def run(args: list[str] | None = None):
156
167
  if not apk_path:
157
168
  failed.append((p.label, "未找到 APK 路径"))
158
169
  continue
159
- if pull_apk(apk_path, dest_dir):
170
+ file_base = p.label if p.label and p.label != p.pkg else p.pkg
171
+ dest_apk = os.path.join(parsed.dest_dir, f"{sanitize_filename(file_base)}.apk")
172
+ if pull_apk(apk_path, dest_apk):
160
173
  succeeded.append(p.label)
161
174
  else:
162
175
  failed.append((p.label, "拉取失败"))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "damon-devtools"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "Developer CLI tools — portkill, removebg, apkpull"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,46 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: damon-devtools
3
- Version: 0.1.0
4
- Summary: Developer CLI tools — portkill, removebg, apkpull
5
- Author: damon
6
- License: MIT
7
- Keywords: cli,devtools,android,apk,adb
8
- Requires-Python: >=3.10
9
- Description-Content-Type: text/markdown
10
- Requires-Dist: psutil>=5.9
11
-
12
- # devtools
13
-
14
- Developer CLI tools: portkill, removebg, apkpull.
15
-
16
- ## Tools
17
-
18
- ### portkill
19
- Kill Node.js processes by port or scan all node processes.
20
-
21
- ```bash
22
- python -m devtools portkill
23
- python -m devtools portkill --listen-only
24
- ```
25
-
26
- ### apkpull
27
- Pull APK files from Android device via ADB.
28
-
29
- ```bash
30
- python -m devtools apkpull [target_dir]
31
- ```
32
-
33
- ### removebg
34
- Remove image backgrounds using the remove.bg API.
35
-
36
- ```bash
37
- python -m devtools removebg input.jpg
38
- ```
39
-
40
- ## Install
41
-
42
- ```bash
43
- pip install damon-devtools
44
- ```
45
-
46
- Requires: Python >=3.10
@@ -1,35 +0,0 @@
1
- # devtools
2
-
3
- Developer CLI tools: portkill, removebg, apkpull.
4
-
5
- ## Tools
6
-
7
- ### portkill
8
- Kill Node.js processes by port or scan all node processes.
9
-
10
- ```bash
11
- python -m devtools portkill
12
- python -m devtools portkill --listen-only
13
- ```
14
-
15
- ### apkpull
16
- Pull APK files from Android device via ADB.
17
-
18
- ```bash
19
- python -m devtools apkpull [target_dir]
20
- ```
21
-
22
- ### removebg
23
- Remove image backgrounds using the remove.bg API.
24
-
25
- ```bash
26
- python -m devtools removebg input.jpg
27
- ```
28
-
29
- ## Install
30
-
31
- ```bash
32
- pip install damon-devtools
33
- ```
34
-
35
- Requires: Python >=3.10
@@ -1,46 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: damon-devtools
3
- Version: 0.1.0
4
- Summary: Developer CLI tools — portkill, removebg, apkpull
5
- Author: damon
6
- License: MIT
7
- Keywords: cli,devtools,android,apk,adb
8
- Requires-Python: >=3.10
9
- Description-Content-Type: text/markdown
10
- Requires-Dist: psutil>=5.9
11
-
12
- # devtools
13
-
14
- Developer CLI tools: portkill, removebg, apkpull.
15
-
16
- ## Tools
17
-
18
- ### portkill
19
- Kill Node.js processes by port or scan all node processes.
20
-
21
- ```bash
22
- python -m devtools portkill
23
- python -m devtools portkill --listen-only
24
- ```
25
-
26
- ### apkpull
27
- Pull APK files from Android device via ADB.
28
-
29
- ```bash
30
- python -m devtools apkpull [target_dir]
31
- ```
32
-
33
- ### removebg
34
- Remove image backgrounds using the remove.bg API.
35
-
36
- ```bash
37
- python -m devtools removebg input.jpg
38
- ```
39
-
40
- ## Install
41
-
42
- ```bash
43
- pip install damon-devtools
44
- ```
45
-
46
- Requires: Python >=3.10
@@ -1,123 +0,0 @@
1
- import subprocess
2
- import re
3
- import os
4
- import tempfile
5
-
6
- def _find_aapt() -> str | None:
7
- """Find aapt.exe — checks PATH first, then Android SDK build-tools"""
8
- # 1. Check PATH
9
- import shutil as _shutil
10
- path_aapt = _shutil.which("aapt")
11
- if path_aapt and os.path.isfile(path_aapt):
12
- return path_aapt
13
- path_aapt2 = _shutil.which("aapt2")
14
- if path_aapt2 and os.path.isfile(path_aapt2):
15
- return path_aapt2
16
- # 2. Check Android SDK build-tools
17
- sdk_base = os.environ.get("LOCALAPPDATA", "") + "/Android/Sdk"
18
- build_tools = os.path.join(sdk_base, "build-tools")
19
- if os.path.isdir(build_tools):
20
- versions = sorted(os.listdir(build_tools), reverse=True)
21
- for v in versions:
22
- for binary in ("aapt.exe", "aapt2.exe"):
23
- aapt = os.path.join(build_tools, v, binary)
24
- if os.path.isfile(aapt):
25
- return aapt
26
- return None
27
-
28
- AAPT_PATH = _find_aapt()
29
-
30
- def check_adb() -> bool:
31
- """检查 ADB 是否可用"""
32
- try:
33
- subprocess.run(["adb", "version"], capture_output=True, check=True)
34
- return True
35
- except (subprocess.CalledProcessError, FileNotFoundError):
36
- return False
37
-
38
- def check_device() -> bool:
39
- """检查设备是否连接"""
40
- try:
41
- result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
42
- lines = result.stdout.strip().split("\n")
43
- for line in lines[1:]:
44
- if line.strip() and not line.startswith("*"):
45
- parts = line.split()
46
- if len(parts) >= 2 and parts[1] == "device":
47
- return True
48
- return False
49
- except (subprocess.CalledProcessError, FileNotFoundError):
50
- return False
51
-
52
- def scan_packages() -> list[str]:
53
- """执行 adb shell pm list packages -3,返回包名列表"""
54
- result = subprocess.run(
55
- ["adb", "shell", "pm", "list", "packages", "-3"],
56
- capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
57
- )
58
- pkgs = []
59
- for line in result.stdout.strip().split("\n"):
60
- line = line.strip()
61
- if line.startswith("package:"):
62
- pkgs.append(line[8:])
63
- return pkgs
64
-
65
- def get_apk_path(pkg: str) -> str | None:
66
- """执行 pm path,返回 APK 物理路径"""
67
- try:
68
- result = subprocess.run(
69
- ["adb", "shell", "pm", "path", pkg],
70
- capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
71
- )
72
- for line in result.stdout.strip().split("\n"):
73
- if line.startswith("package:"):
74
- return line[8:].strip()
75
- return None
76
- except (subprocess.CalledProcessError, FileNotFoundError):
77
- return None
78
-
79
- def get_app_name(pkg: str) -> str:
80
- """通过 aapt 提取 APK 的 application label"""
81
- if not AAPT_PATH:
82
- return pkg
83
- apk_path = get_apk_path(pkg)
84
- if not apk_path:
85
- return pkg
86
- tmp = os.path.join(tempfile.gettempdir(), f"apkpull_{os.getpid()}_{pkg.replace('.', '_')}.apk")
87
- try:
88
- # pull APK to temp file
89
- pull_ok = subprocess.run(
90
- ["adb", "pull", apk_path, tmp],
91
- capture_output=True, encoding="utf-8", errors="replace"
92
- ).returncode == 0
93
- if not pull_ok or not os.path.exists(tmp):
94
- return pkg
95
- # extract label via aapt
96
- result = subprocess.run(
97
- [AAPT_PATH, "dump", "badging", tmp],
98
- capture_output=True, encoding="utf-8", errors="replace"
99
- )
100
- for line in result.stdout.split("\n"):
101
- m = re.search(r"application-label:'(.+?)'", line)
102
- if m:
103
- return m.group(1)
104
- return pkg
105
- except Exception:
106
- return pkg
107
- finally:
108
- if os.path.exists(tmp):
109
- try:
110
- os.remove(tmp)
111
- except OSError:
112
- pass
113
-
114
- def pull_apk(apk_path: str, dest_dir: str) -> bool:
115
- """执行 adb pull,拉取 APK 到目标目录"""
116
- try:
117
- subprocess.run(
118
- ["adb", "pull", apk_path, dest_dir],
119
- capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
120
- )
121
- return True
122
- except (subprocess.CalledProcessError, FileNotFoundError):
123
- return False
File without changes