@jspg-ai/coding-bb 0.0.1
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/README.md +41 -0
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +104 -0
- package/cbb/dev-standards/rules/cbb-coding-rule.md +50 -0
- package/cbb/dev-standards/rules/cbb-priority.md +56 -0
- package/cbb/dev-standards/skills/architecture-specs/SKILL.md +129 -0
- package/cbb/dev-standards/skills/coding-specs/SKILL.md +376 -0
- package/cbb/dev-standards/skills/coding-specs/references/concurrency.md +53 -0
- package/cbb/dev-standards/skills/coding-specs/references/config-center.md +16 -0
- package/cbb/dev-standards/skills/coding-specs/references/distributed.md +42 -0
- package/cbb/dev-standards/skills/coding-specs/references/es-coding.md +60 -0
- package/cbb/dev-standards/skills/coding-specs/references/scheduled-task.md +25 -0
- package/cbb/dev-standards/skills/coding-specs/references/security.md +11 -0
- package/cbb/dev-standards/skills/coding-specs/references/unit-testing.md +43 -0
- package/cbb/dev-standards/skills/es-design-specs/SKILL.md +104 -0
- package/cbb/dev-standards/skills/mysql-design-specs/SKILL.md +89 -0
- package/cbb/lib/install/claude-code.js +20 -0
- package/cbb/lib/install/cleanup.js +64 -0
- package/cbb/lib/install/init.js +1875 -0
- package/cbb/lib/install/qoder.js +20 -0
- package/cbb/lib/install/workspaces.js +232 -0
- package/cbb/lib/openspec/index.js +554 -0
- package/cbb/lib/superpowers/index.js +265 -0
- package/cbb/lib/utils/check-update.js +147 -0
- package/cbb/lib/utils/checkbox.js +383 -0
- package/cbb/lib/utils/gitignore.js +69 -0
- package/cbb/lib/utils/output.js +64 -0
- package/cbb/lib/utils/settings.js +119 -0
- package/cbb/lib/utils/version.js +135 -0
- package/cbb/lib/wiki/api-client.js +358 -0
- package/cbb/lib/wiki/cli.js +427 -0
- package/cbb/lib/wiki/convert.js +464 -0
- package/cbb/lib/wiki/index.js +218 -0
- package/cbb/lib/wiki/mermaid-guard.js +103 -0
- package/cbb/lib/wiki/split.js +131 -0
- package/cbb/tools/cbb-decompile-jar/SKILL.md +220 -0
- package/cbb/tools/cbb-decompile-jar/scripts/decompile.py +863 -0
- package/cbb/tools/cbb-design-to-wiki/SKILL.md +180 -0
- package/cbb/tools/cbb-mvn-guide/SKILL.md +281 -0
- package/cbb/tools/cbb-mvn-guide/scripts/mvn_jdk_manager.py +378 -0
- package/cbb/tools/cbb-mvn-guide/scripts/mvn_pom_jdk_reader.py +197 -0
- package/cbb/tools/cbb-plantuml-authoring/SKILL.md +448 -0
- package/cbb/tools/cbb-plantuml-authoring/references/drawing-templates.md +479 -0
- package/cbb/tools/cbb-wiki-ops/SKILL.md +134 -0
- package/cbb/tools/cbb-wiki-ops/scripts/check-auth.js +44 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/_shared/scripts/push-core.js +136 -0
- package/cbb/worktrees/_shared/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/commands/close.md +64 -0
- package/cbb/worktrees/commands/extend.md +65 -0
- package/cbb/worktrees/commands/init.md +55 -0
- package/cbb/worktrees/commands/push.md +42 -0
- package/cbb/worktrees/skills/openspec-close-worktree/SKILL.md +449 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-env.js +76 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-unarchived.js +48 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/delete-branches.js +136 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/discover-apps.js +76 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/remove-worktrees.js +124 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/safety-check.js +174 -0
- package/cbb/worktrees/skills/openspec-close-worktree/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/SKILL.md +390 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-env.js +95 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-repos.js +98 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js +135 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/install-ai.js +150 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/list-available-apps.js +88 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/push-core.js +136 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/openspec-extend-worktree/scripts/sync-repos.js +93 -0
- package/cbb/worktrees/skills/openspec-init-worktree/SKILL.md +539 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/auto-open.js +136 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env-deep.js +87 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env.js +90 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-repos.js +171 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-branches.js +103 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-worktrees.js +154 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/generate-app-options.js +93 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/install-ai.js +177 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/parse-config.js +67 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-branches.js +101 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-core.js +136 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/sync-repos.js +95 -0
- package/cbb/worktrees/skills/openspec-init-worktree/scripts/update-gitignore.js +134 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/SKILL.md +307 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/commit-worktrees.js +269 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-target-worktree.js +96 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-workspace-root.js +67 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-core.js +136 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-worktrees.js +159 -0
- package/cbb/worktrees/skills/openspec-push-worktrees/scripts/silence-popup.js +23 -0
- package/config/cbb.yaml +6 -0
- package/config/config.sample.json +16 -0
- package/config/openspec/config.yaml +28 -0
- package/config/openspec/schemas/spec-driven/schema.yaml +247 -0
- package/config/openspec/schemas/spec-driven/templates/design.md +475 -0
- package/config/openspec/schemas/spec-driven/templates/proposal.md +58 -0
- package/config/openspec/schemas/spec-driven/templates/spec.md +8 -0
- package/config/openspec/schemas/spec-driven/templates/tasks.md +9 -0
- package/config/workspaces.json +15 -0
- package/openspec/.version +6 -0
- package/openspec/commands/apply.md +182 -0
- package/openspec/commands/archive.md +223 -0
- package/openspec/commands/bulk-archive.md +334 -0
- package/openspec/commands/continue.md +112 -0
- package/openspec/commands/explore.md +206 -0
- package/openspec/commands/ff.md +111 -0
- package/openspec/commands/new.md +70 -0
- package/openspec/commands/onboard.md +555 -0
- package/openspec/commands/propose.md +157 -0
- package/openspec/commands/sync.md +256 -0
- package/openspec/commands/update.md +85 -0
- package/openspec/commands/verify.md +169 -0
- package/openspec/skills/openspec-apply-change/SKILL.md +187 -0
- package/openspec/skills/openspec-archive-change/SKILL.md +181 -0
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +338 -0
- package/openspec/skills/openspec-continue-change/SKILL.md +117 -0
- package/openspec/skills/openspec-explore/SKILL.md +342 -0
- package/openspec/skills/openspec-ff-change/SKILL.md +116 -0
- package/openspec/skills/openspec-new-change/SKILL.md +76 -0
- package/openspec/skills/openspec-onboard/SKILL.md +560 -0
- package/openspec/skills/openspec-propose/SKILL.md +162 -0
- package/openspec/skills/openspec-sync-specs/SKILL.md +261 -0
- package/openspec/skills/openspec-update-change/SKILL.md +90 -0
- package/openspec/skills/openspec-verify-change/SKILL.md +174 -0
- package/package.json +46 -0
- package/superpowers/.version +6 -0
- package/superpowers/skills/brainstorming/SKILL.md +250 -0
- package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
- package/superpowers/skills/brainstorming/scripts/helper.js +167 -0
- package/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
- package/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
- package/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/superpowers/skills/brainstorming/visual-companion.md +299 -0
- package/superpowers/skills/dispatching-parallel-agents/SKILL.md +167 -0
- package/superpowers/skills/executing-plans/SKILL.md +64 -0
- package/superpowers/skills/finishing-a-development-branch/SKILL.md +225 -0
- package/superpowers/skills/receiving-code-review/SKILL.md +205 -0
- package/superpowers/skills/requesting-code-review/SKILL.md +95 -0
- package/superpowers/skills/requesting-code-review/code-reviewer.md +181 -0
- package/superpowers/skills/subagent-driven-development/SKILL.md +568 -0
- package/superpowers/skills/subagent-driven-development/implementer-prompt.md +154 -0
- package/superpowers/skills/subagent-driven-development/re-review-prompt.md +115 -0
- package/superpowers/skills/subagent-driven-development/scripts/review-package +46 -0
- package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +40 -0
- package/superpowers/skills/subagent-driven-development/scripts/task-brief +41 -0
- package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
- package/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/superpowers/skills/systematic-debugging/SKILL.md +283 -0
- package/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/superpowers/skills/systematic-debugging/find-polluter.sh +72 -0
- package/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/superpowers/skills/systematic-debugging/test-academic.md +14 -0
- package/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/superpowers/skills/test-driven-development/SKILL.md +320 -0
- package/superpowers/skills/test-driven-development/writing-good-tests.md +198 -0
- package/superpowers/skills/using-git-worktrees/SKILL.md +167 -0
- package/superpowers/skills/using-superpowers/SKILL.md +63 -0
- package/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
- package/superpowers/skills/using-superpowers/references/codex-tools.md +108 -0
- package/superpowers/skills/using-superpowers/references/gemini-tools.md +63 -0
- package/superpowers/skills/using-superpowers/references/hermes-tools.md +56 -0
- package/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
- package/superpowers/skills/verification-before-completion/SKILL.md +120 -0
- package/superpowers/skills/writing-plans/SKILL.md +171 -0
- package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/superpowers/skills/writing-skills/SKILL.md +679 -0
- package/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
- package/superpowers/skills/writing-skills/render-graphs.js +169 -0
- package/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""JAR 反编译辅助脚本(全平台通用)
|
|
3
|
+
用法:
|
|
4
|
+
decompile.py check - 环境预检(JDK + Maven + 仓库)
|
|
5
|
+
decompile.py repo - 输出本地 Maven 仓库路径
|
|
6
|
+
decompile.py resolve <FQN> [-p projectDir] - 类全限定名 → JAR 路径 + Maven 坐标
|
|
7
|
+
decompile.py find <G> <A> <V> - Maven 坐标 → JAR 路径
|
|
8
|
+
decompile.py list <jar_path> [keyword] - 列出 JAR 中的类(可按关键字过滤)
|
|
9
|
+
decompile.py show <jar_path> <fully_qualified> - 查看类的完整信息(字段、方法)
|
|
10
|
+
decompile.py api <jar_path> <fully_qualified> - 查看 RPC 接口的方法签名
|
|
11
|
+
decompile.py enum <jar_path> <fully_qualified> - 查看枚举类的所有值
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# 抑制字节码缓存:本脚本作为工具直接调用、无复用价值,避免在 scripts/ 下生成 __pycache__/
|
|
15
|
+
import sys
|
|
16
|
+
if not getattr(sys, 'dont_write_bytecode', False):
|
|
17
|
+
sys.dont_write_bytecode = True
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import glob
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import xml.etree.ElementTree as ET
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ============================================================
|
|
29
|
+
# XML 解析
|
|
30
|
+
# ============================================================
|
|
31
|
+
def _strip_ns(tag):
|
|
32
|
+
"""去掉 XML 命名空间前缀,如 {http://...}settings → settings"""
|
|
33
|
+
if tag.startswith('{'):
|
|
34
|
+
return tag.split('}', 1)[1]
|
|
35
|
+
return tag
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _find_ns(root):
|
|
39
|
+
"""从根节点提取命名空间"""
|
|
40
|
+
if '}' in root.tag:
|
|
41
|
+
return {'ns': root.tag.split('}')[0][1:]}
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_xml_element(xml_file, element_path):
|
|
46
|
+
"""解析 XML 文件,提取指定路径的元素文本值(天然忽略注释)
|
|
47
|
+
element_path 用 / 分隔,如 "settings/localRepository" 或 "project/packaging"
|
|
48
|
+
同名兄弟元素(如多个 module)会全部返回
|
|
49
|
+
"""
|
|
50
|
+
tree = ET.parse(xml_file)
|
|
51
|
+
root = tree.getroot()
|
|
52
|
+
ns = _find_ns(root)
|
|
53
|
+
parts = element_path.split('/')
|
|
54
|
+
|
|
55
|
+
# 先定位到父节点,再 findall 最后一级(支持同名兄弟)
|
|
56
|
+
parent = root
|
|
57
|
+
for p in parts[1:-1]:
|
|
58
|
+
found = parent.find('ns:' + p, ns) if ns else parent.find(p)
|
|
59
|
+
if found is None:
|
|
60
|
+
found = parent.find(p)
|
|
61
|
+
if found is None:
|
|
62
|
+
return []
|
|
63
|
+
parent = found
|
|
64
|
+
|
|
65
|
+
child_tag = parts[-1]
|
|
66
|
+
siblings = parent.findall('ns:' + child_tag, ns) if ns else parent.findall(child_tag)
|
|
67
|
+
results = []
|
|
68
|
+
for s in siblings:
|
|
69
|
+
if s.text and s.text.strip():
|
|
70
|
+
results.append(s.text.strip())
|
|
71
|
+
return results
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_xml_single(xml_file, element_path):
|
|
75
|
+
"""解析 XML 文件,返回第一个匹配元素的文本值,无则返回 None"""
|
|
76
|
+
results = parse_xml_element(xml_file, element_path)
|
|
77
|
+
return results[0] if results else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ============================================================
|
|
81
|
+
# 定位 JDK 工具
|
|
82
|
+
# 查找顺序:环境变量 → PATH → 系统工具(Mac) → 常见路径 → 提示用户
|
|
83
|
+
# ============================================================
|
|
84
|
+
def find_jdk():
|
|
85
|
+
# 1. JAVA_HOME
|
|
86
|
+
java_home = os.environ.get('JAVA_HOME', '')
|
|
87
|
+
if java_home:
|
|
88
|
+
javap = os.path.join(java_home, 'bin', 'javap' + ('.exe' if sys.platform == 'win32' else ''))
|
|
89
|
+
jar_cmd = os.path.join(java_home, 'bin', 'jar' + ('.exe' if sys.platform == 'win32' else ''))
|
|
90
|
+
if os.path.isfile(javap):
|
|
91
|
+
return {'javap': javap, 'jar': jar_cmd, 'source': f'JAVA_HOME={java_home}'}
|
|
92
|
+
|
|
93
|
+
# 2. PATH
|
|
94
|
+
javap_name = 'javap.exe' if sys.platform == 'win32' else 'javap'
|
|
95
|
+
javap_in_path = shutil.which(javap_name)
|
|
96
|
+
if javap_in_path:
|
|
97
|
+
bin_dir = os.path.dirname(javap_in_path)
|
|
98
|
+
jar_name = 'jar.exe' if sys.platform == 'win32' else 'jar'
|
|
99
|
+
jar_cmd = os.path.join(bin_dir, jar_name)
|
|
100
|
+
if os.path.isfile(jar_cmd):
|
|
101
|
+
return {'javap': javap_in_path, 'jar': jar_cmd, 'source': 'PATH'}
|
|
102
|
+
|
|
103
|
+
# 3. Mac 专用: /usr/libexec/java_home
|
|
104
|
+
if sys.platform == 'darwin':
|
|
105
|
+
try:
|
|
106
|
+
result = subprocess.run(['/usr/libexec/java_home'], capture_output=True, text=True)
|
|
107
|
+
if result.returncode == 0:
|
|
108
|
+
mac_home = result.stdout.strip()
|
|
109
|
+
javap = os.path.join(mac_home, 'bin', 'javap')
|
|
110
|
+
jar_cmd = os.path.join(mac_home, 'bin', 'jar')
|
|
111
|
+
if os.path.isfile(javap):
|
|
112
|
+
return {'javap': javap, 'jar': jar_cmd, 'source': '/usr/libexec/java_home'}
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
# 4. 扫描常见安装路径
|
|
117
|
+
if sys.platform == 'darwin':
|
|
118
|
+
scan_dirs = [
|
|
119
|
+
'/Library/Java/JavaVirtualMachines/*/Contents/Home/bin',
|
|
120
|
+
'/opt/homebrew/opt/openjdk*/bin',
|
|
121
|
+
'/usr/local/opt/openjdk*/bin',
|
|
122
|
+
]
|
|
123
|
+
elif sys.platform == 'win32':
|
|
124
|
+
scan_dirs = [
|
|
125
|
+
os.path.join(os.environ.get('ProgramFiles', ''), 'Java', 'jdk*', 'bin'),
|
|
126
|
+
os.path.join(os.environ.get('ProgramFiles', ''), 'Amazon Corretto', '*', 'bin'),
|
|
127
|
+
os.path.join(os.environ.get('ProgramFiles', ''), 'Eclipse Adoptium', '*', 'bin'),
|
|
128
|
+
os.path.join(os.environ.get('ProgramFiles', ''), 'Zulu', '*', 'bin'),
|
|
129
|
+
os.path.join(os.environ.get('ProgramFiles(x86)', ''), 'Java', 'jdk*', 'bin'),
|
|
130
|
+
'C:\\env\\jdk\\*\\bin',
|
|
131
|
+
'D:\\env\\jdk\\*\\bin',
|
|
132
|
+
'E:\\env\\jdk\\*\\bin',
|
|
133
|
+
]
|
|
134
|
+
else:
|
|
135
|
+
scan_dirs = [
|
|
136
|
+
'/usr/lib/jvm/java-*/bin',
|
|
137
|
+
'/usr/lib/jvm/jdk-*/bin',
|
|
138
|
+
'/usr/lib/jvm/adoptopenjdk-*/bin',
|
|
139
|
+
'/usr/lib/jvm/temurin-*/bin',
|
|
140
|
+
'/usr/local/java/jdk*/bin',
|
|
141
|
+
'/opt/jdk*/bin',
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
javap_name = 'javap.exe' if sys.platform == 'win32' else 'javap'
|
|
145
|
+
jar_name = 'jar.exe' if sys.platform == 'win32' else 'jar'
|
|
146
|
+
for pattern in scan_dirs:
|
|
147
|
+
for bin_dir in sorted(glob.glob(pattern), reverse=True):
|
|
148
|
+
javap = os.path.join(bin_dir, javap_name)
|
|
149
|
+
jar_cmd = os.path.join(bin_dir, jar_name)
|
|
150
|
+
if os.path.isfile(javap):
|
|
151
|
+
home_dir = os.path.dirname(bin_dir)
|
|
152
|
+
return {'javap': javap, 'jar': jar_cmd, 'source': f'Scanned: {home_dir}'}
|
|
153
|
+
|
|
154
|
+
# 5. 都没找到
|
|
155
|
+
print("ERROR: Cannot find JDK tools (javap, jar)", file=sys.stderr)
|
|
156
|
+
print(file=sys.stderr)
|
|
157
|
+
print("Please set JAVA_HOME environment variable, or add JDK bin directory to PATH.", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ============================================================
|
|
162
|
+
# 定位 Maven 安装路径
|
|
163
|
+
# 查找顺序:环境变量 → PATH → 常见路径 → 提示用户
|
|
164
|
+
# ============================================================
|
|
165
|
+
def find_maven():
|
|
166
|
+
# 1. 环境变量
|
|
167
|
+
m2_home = os.environ.get('MAVEN_HOME', '') or os.environ.get('M2_HOME', '')
|
|
168
|
+
if m2_home:
|
|
169
|
+
mvn_name = 'mvn.cmd' if sys.platform == 'win32' else 'mvn'
|
|
170
|
+
if os.path.isfile(os.path.join(m2_home, 'bin', mvn_name)):
|
|
171
|
+
return {'home': m2_home, 'source': 'MAVEN_HOME'}
|
|
172
|
+
|
|
173
|
+
# 2. PATH
|
|
174
|
+
mvn_name = 'mvn.cmd' if sys.platform == 'win32' else 'mvn'
|
|
175
|
+
mvn_in_path = shutil.which(mvn_name)
|
|
176
|
+
if not mvn_in_path and sys.platform == 'win32':
|
|
177
|
+
mvn_in_path = shutil.which('mvn')
|
|
178
|
+
if mvn_in_path:
|
|
179
|
+
maven_home = os.path.dirname(os.path.dirname(mvn_in_path))
|
|
180
|
+
if os.path.isfile(os.path.join(maven_home, 'conf', 'settings.xml')):
|
|
181
|
+
return {'home': maven_home, 'source': 'PATH'}
|
|
182
|
+
|
|
183
|
+
# 3. 扫描常见安装路径
|
|
184
|
+
if sys.platform == 'darwin':
|
|
185
|
+
scan_dirs = [
|
|
186
|
+
'/opt/homebrew/opt/maven/libexec',
|
|
187
|
+
'/usr/local/opt/maven/libexec',
|
|
188
|
+
'/usr/local/apache-maven-*',
|
|
189
|
+
'/opt/apache-maven-*',
|
|
190
|
+
]
|
|
191
|
+
elif sys.platform == 'win32':
|
|
192
|
+
scan_dirs = [
|
|
193
|
+
os.path.join(os.environ.get('ProgramFiles', ''), 'apache-maven-*'),
|
|
194
|
+
os.path.join(os.environ.get('ProgramFiles(x86)', ''), 'apache-maven-*'),
|
|
195
|
+
'C:\\env\\maven\\apache-maven-*',
|
|
196
|
+
'D:\\env\\maven\\apache-maven-*',
|
|
197
|
+
'E:\\env\\maven\\apache-maven-*',
|
|
198
|
+
'C:\\tools\\maven\\apache-maven-*',
|
|
199
|
+
]
|
|
200
|
+
else:
|
|
201
|
+
scan_dirs = [
|
|
202
|
+
'/usr/local/apache-maven-*',
|
|
203
|
+
'/opt/apache-maven-*',
|
|
204
|
+
'/usr/share/maven',
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
mvn_name = 'mvn.cmd' if sys.platform == 'win32' else 'mvn'
|
|
208
|
+
for pattern in scan_dirs:
|
|
209
|
+
for maven_dir in sorted(glob.glob(pattern), reverse=True):
|
|
210
|
+
if os.path.isfile(os.path.join(maven_dir, 'bin', mvn_name)):
|
|
211
|
+
return {'home': maven_dir, 'source': f'Scanned: {maven_dir}'}
|
|
212
|
+
|
|
213
|
+
# 4. 都没找到
|
|
214
|
+
print("ERROR: Cannot find Maven", file=sys.stderr)
|
|
215
|
+
print(file=sys.stderr)
|
|
216
|
+
print("Please set MAVEN_HOME environment variable, or add Maven bin directory to PATH.", file=sys.stderr)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ============================================================
|
|
221
|
+
# 读取 settings.xml 找到本地 Maven 仓库
|
|
222
|
+
# ============================================================
|
|
223
|
+
def find_maven_local_repo(maven_home):
|
|
224
|
+
home = os.path.expanduser('~')
|
|
225
|
+
user_settings = os.path.join(home, '.m2', 'settings.xml')
|
|
226
|
+
global_settings = os.path.join(maven_home, 'conf', 'settings.xml')
|
|
227
|
+
|
|
228
|
+
for settings_path in [user_settings, global_settings]:
|
|
229
|
+
if os.path.isfile(settings_path):
|
|
230
|
+
local_repo = parse_xml_single(settings_path, 'settings/localRepository')
|
|
231
|
+
if local_repo and os.path.isdir(local_repo):
|
|
232
|
+
return {'path': local_repo, 'source': f'settings.xml: {settings_path}'}
|
|
233
|
+
|
|
234
|
+
default_repo = os.path.join(home, '.m2', 'repository')
|
|
235
|
+
if os.path.isdir(default_repo):
|
|
236
|
+
return {'path': default_repo, 'source': 'Default (no localRepository in settings.xml)'}
|
|
237
|
+
|
|
238
|
+
print("ERROR: Maven local repository not found.", file=sys.stderr)
|
|
239
|
+
sys.exit(1)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ============================================================
|
|
243
|
+
# 从 FQN 提取包前缀列表(从最长到最短,最少2段)
|
|
244
|
+
# ============================================================
|
|
245
|
+
def get_package_prefixes(fqn):
|
|
246
|
+
segs = fqn.split('.')
|
|
247
|
+
prefixes = []
|
|
248
|
+
for i in range(len(segs) - 1, 0, -1):
|
|
249
|
+
prefixes.append('.'.join(segs[:i + 1]))
|
|
250
|
+
return prefixes
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ============================================================
|
|
254
|
+
# 自动下载 Maven artifact
|
|
255
|
+
# ============================================================
|
|
256
|
+
def download_artifact(group_id, artifact_id, version, maven_home):
|
|
257
|
+
java_home = os.environ.get('JAVA_HOME', '')
|
|
258
|
+
if not java_home:
|
|
259
|
+
print("ERROR: JAVA_HOME is not set. Cannot run Maven to download artifacts.", file=sys.stderr)
|
|
260
|
+
print(" Please set JAVA_HOME to your JDK directory.", file=sys.stderr)
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
mvn_name = 'mvn.cmd' if sys.platform == 'win32' else 'mvn'
|
|
264
|
+
mvn_exe = os.path.join(maven_home, 'bin', mvn_name)
|
|
265
|
+
artifact = f"{group_id}:{artifact_id}:{version}"
|
|
266
|
+
print(f"JAR not found locally, downloading {artifact} ...", file=sys.stderr)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
[mvn_exe, 'dependency:get', f'-Dartifact={artifact}'],
|
|
271
|
+
capture_output=True, text=True
|
|
272
|
+
)
|
|
273
|
+
if 'BUILD SUCCESS' in (result.stdout or ''):
|
|
274
|
+
print(f"Downloaded {artifact} successfully.", file=sys.stderr)
|
|
275
|
+
return True
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
print(f"Failed to download {artifact}.", file=sys.stderr)
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ============================================================
|
|
284
|
+
# 从 Maven 坐标定位 JAR 文件
|
|
285
|
+
# ============================================================
|
|
286
|
+
def find_jar_from_coordinates(group_id, artifact_id, version, local_repo, maven_home):
|
|
287
|
+
group_path = os.path.join(*group_id.split('.'))
|
|
288
|
+
jar_name = f"{artifact_id}-{version}.jar"
|
|
289
|
+
jar_path = os.path.join(local_repo, group_path, artifact_id, version, jar_name)
|
|
290
|
+
|
|
291
|
+
if os.path.isfile(jar_path):
|
|
292
|
+
return jar_path
|
|
293
|
+
|
|
294
|
+
if download_artifact(group_id, artifact_id, version, maven_home):
|
|
295
|
+
if os.path.isfile(jar_path):
|
|
296
|
+
return jar_path
|
|
297
|
+
|
|
298
|
+
print(f"ERROR: JAR not found at: {jar_path}. Auto-download failed.", file=sys.stderr)
|
|
299
|
+
sys.exit(2)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ============================================================
|
|
303
|
+
# 类全限定名解析到 JAR(两阶段)
|
|
304
|
+
# ============================================================
|
|
305
|
+
|
|
306
|
+
# ============================================================
|
|
307
|
+
# Resolve 缓存
|
|
308
|
+
# ============================================================
|
|
309
|
+
def _cache_path(project_dir):
|
|
310
|
+
if not project_dir:
|
|
311
|
+
return None
|
|
312
|
+
import hashlib
|
|
313
|
+
dir_hash = hashlib.md5(os.path.abspath(project_dir).encode()).hexdigest()[:12]
|
|
314
|
+
cache_dir = os.path.join(os.path.expanduser('~'), '.cache', 'cbb-decompile-jar')
|
|
315
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
316
|
+
return os.path.join(cache_dir, f'{dir_hash}.json')
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _cache_read(project_dir, class_name):
|
|
320
|
+
cp = _cache_path(project_dir)
|
|
321
|
+
if not cp or not os.path.isfile(cp):
|
|
322
|
+
return None
|
|
323
|
+
try:
|
|
324
|
+
with open(cp, 'r', encoding='utf-8') as f:
|
|
325
|
+
cache = json.load(f)
|
|
326
|
+
entry = cache.get(class_name)
|
|
327
|
+
if entry and os.path.isfile(entry.get('jar_path', '')):
|
|
328
|
+
return entry
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _cache_write(project_dir, class_name, result):
|
|
335
|
+
cp = _cache_path(project_dir)
|
|
336
|
+
if not cp:
|
|
337
|
+
return
|
|
338
|
+
try:
|
|
339
|
+
cache = {}
|
|
340
|
+
if os.path.isfile(cp):
|
|
341
|
+
with open(cp, 'r', encoding='utf-8') as f:
|
|
342
|
+
cache = json.load(f)
|
|
343
|
+
cache[class_name] = result
|
|
344
|
+
with open(cp, 'w', encoding='utf-8') as f:
|
|
345
|
+
json.dump(cache, f, ensure_ascii=False, indent=2)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def find_module_for_class(project_dir, class_name):
|
|
351
|
+
"""从类的包名前缀推断 groupId,在各子模块 pom.xml 中查找声明了该依赖的模块"""
|
|
352
|
+
pom_file = os.path.join(project_dir, 'pom.xml')
|
|
353
|
+
modules = parse_xml_element(pom_file, 'project/modules/module')
|
|
354
|
+
if not modules:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
segs = class_name.split('.')
|
|
358
|
+
prefixes = []
|
|
359
|
+
for i in range(min(len(segs) - 1, 5), 1, -1):
|
|
360
|
+
prefixes.append('.'.join(segs[:i]))
|
|
361
|
+
|
|
362
|
+
for module in modules:
|
|
363
|
+
module_pom = os.path.join(project_dir, module, 'pom.xml')
|
|
364
|
+
if not os.path.isfile(module_pom):
|
|
365
|
+
continue
|
|
366
|
+
try:
|
|
367
|
+
with open(module_pom, 'r', encoding='utf-8', errors='ignore') as f:
|
|
368
|
+
pom_content = f.read()
|
|
369
|
+
for prefix in prefixes:
|
|
370
|
+
if f'<groupId>{prefix}</groupId>' in pom_content:
|
|
371
|
+
return module
|
|
372
|
+
except Exception:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def resolve_class_to_jar(class_name, project_dir, maven_home, jdk_jar, local_repo):
|
|
379
|
+
# Check cache first
|
|
380
|
+
cached = _cache_read(project_dir, class_name)
|
|
381
|
+
if cached:
|
|
382
|
+
print(f"Cache hit for {class_name}", file=sys.stderr)
|
|
383
|
+
return cached
|
|
384
|
+
|
|
385
|
+
class_path = class_name.replace('.', '/')
|
|
386
|
+
class_file = f"{class_path}.class"
|
|
387
|
+
|
|
388
|
+
# 确定模块路径:从各子模块 pom 中精确定位声明了该依赖的模块
|
|
389
|
+
module_path = None
|
|
390
|
+
pom_file = os.path.join(project_dir, 'pom.xml') if project_dir else ''
|
|
391
|
+
if project_dir and os.path.isfile(pom_file):
|
|
392
|
+
packaging = parse_xml_single(pom_file, 'project/packaging')
|
|
393
|
+
if packaging == 'pom':
|
|
394
|
+
module_path = find_module_for_class(project_dir, class_name)
|
|
395
|
+
|
|
396
|
+
# Phase 1: dependency:tree
|
|
397
|
+
java_home = os.environ.get('JAVA_HOME', '')
|
|
398
|
+
if not java_home:
|
|
399
|
+
inferred_home = os.path.dirname(os.path.dirname(jdk_jar))
|
|
400
|
+
print("ERROR: JAVA_HOME is not set. Maven dependency:tree will fail.", file=sys.stderr)
|
|
401
|
+
print(" Please set JAVA_HOME to your JDK directory, e.g.: " + inferred_home, file=sys.stderr)
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
|
|
404
|
+
mvn_name = 'mvn.cmd' if sys.platform == 'win32' else 'mvn'
|
|
405
|
+
mvn_exe = os.path.join(maven_home, 'bin', mvn_name)
|
|
406
|
+
mvn_args = ['dependency:tree', '-DoutputType=text']
|
|
407
|
+
if module_path:
|
|
408
|
+
mvn_args += ['-pl', module_path]
|
|
409
|
+
if project_dir:
|
|
410
|
+
mvn_args += ['-f', project_dir]
|
|
411
|
+
|
|
412
|
+
print(f"Running mvn dependency:tree to locate dependency for {class_name} ...", file=sys.stderr)
|
|
413
|
+
|
|
414
|
+
candidates = []
|
|
415
|
+
try:
|
|
416
|
+
result = subprocess.run([mvn_exe] + mvn_args, capture_output=True, text=True, timeout=15)
|
|
417
|
+
if result.returncode != 0:
|
|
418
|
+
print(f"dependency:tree failed (exit code {result.returncode}), skipping to local repository scan.", file=sys.stderr)
|
|
419
|
+
if result.stderr:
|
|
420
|
+
for line in result.stderr.strip().splitlines()[:10]:
|
|
421
|
+
print(f" {line}", file=sys.stderr)
|
|
422
|
+
else:
|
|
423
|
+
prefixes = get_package_prefixes(class_name)
|
|
424
|
+
dep_regex = re.compile(r'(\S+):(\S+):\S+:(\S+):\S+')
|
|
425
|
+
seen = set()
|
|
426
|
+
for prefix in prefixes:
|
|
427
|
+
for line in result.stdout.splitlines():
|
|
428
|
+
if prefix in line:
|
|
429
|
+
m = dep_regex.search(line)
|
|
430
|
+
if m:
|
|
431
|
+
coord = f"{m.group(1)}:{m.group(2)}:{m.group(3)}"
|
|
432
|
+
if coord not in seen:
|
|
433
|
+
seen.add(coord)
|
|
434
|
+
candidates.append(coord)
|
|
435
|
+
if not candidates:
|
|
436
|
+
print("No matching dependencies found in dependency:tree, scanning local repository.", file=sys.stderr)
|
|
437
|
+
except subprocess.TimeoutExpired:
|
|
438
|
+
print("dependency:tree timed out (>15s), skipping to local repository scan.", file=sys.stderr)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"dependency:tree failed: {e}, skipping to local repository scan.", file=sys.stderr)
|
|
441
|
+
|
|
442
|
+
# 搜索 candidates
|
|
443
|
+
for coord in candidates:
|
|
444
|
+
parts = coord.split(':')
|
|
445
|
+
if len(parts) < 3:
|
|
446
|
+
continue
|
|
447
|
+
g, a, v = parts[0], parts[1], parts[2]
|
|
448
|
+
group_path = os.path.join(*g.split('.'))
|
|
449
|
+
jar_name = f"{a}-{v}.jar"
|
|
450
|
+
jar_path = os.path.join(local_repo, group_path, a, v, jar_name)
|
|
451
|
+
|
|
452
|
+
if not os.path.isfile(jar_path):
|
|
453
|
+
if not download_artifact(g, a, v, maven_home):
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
if os.path.isfile(jar_path):
|
|
457
|
+
try:
|
|
458
|
+
found = subprocess.run(
|
|
459
|
+
[jdk_jar, 'tf', jar_path],
|
|
460
|
+
capture_output=True, text=True
|
|
461
|
+
)
|
|
462
|
+
if class_file in (found.stdout or ''):
|
|
463
|
+
result = {'jar_path': jar_path, 'group_id': g, 'artifact_id': a, 'version': v}
|
|
464
|
+
_cache_write(project_dir, class_name, result)
|
|
465
|
+
return result
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
# Phase 2: scan local repository
|
|
470
|
+
print(f"Scanning local repository for {class_name} ...", file=sys.stderr)
|
|
471
|
+
prefixes = get_package_prefixes(class_name)
|
|
472
|
+
for prefix in prefixes:
|
|
473
|
+
group_dir = os.path.join(local_repo, *prefix.split('.'))
|
|
474
|
+
if not os.path.isdir(group_dir):
|
|
475
|
+
continue
|
|
476
|
+
for root, dirs, files in os.walk(group_dir):
|
|
477
|
+
for f in files:
|
|
478
|
+
if not f.endswith('.jar') or f.endswith('-sources.jar') or f.endswith('-javadoc.jar'):
|
|
479
|
+
continue
|
|
480
|
+
jar_path = os.path.join(root, f)
|
|
481
|
+
try:
|
|
482
|
+
result = subprocess.run(
|
|
483
|
+
[jdk_jar, 'tf', jar_path],
|
|
484
|
+
capture_output=True, text=True
|
|
485
|
+
)
|
|
486
|
+
if class_file in (result.stdout or ''):
|
|
487
|
+
ver_dir = os.path.basename(root)
|
|
488
|
+
art_dir = os.path.basename(os.path.dirname(root))
|
|
489
|
+
result_dict = {'jar_path': jar_path, 'group_id': prefix, 'artifact_id': art_dir, 'version': ver_dir}
|
|
490
|
+
_cache_write(project_dir, class_name, result_dict)
|
|
491
|
+
return result_dict
|
|
492
|
+
except Exception:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
print(f"ERROR: Class '{class_name}' not found in any project dependency JAR.", file=sys.stderr)
|
|
496
|
+
sys.exit(2)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ============================================================
|
|
500
|
+
# 解析 classpath
|
|
501
|
+
# ============================================================
|
|
502
|
+
def resolve_classpath(jar_paths):
|
|
503
|
+
separator = os.pathsep # ; on Windows, : on others
|
|
504
|
+
parts = re.split(r'[:;,]', jar_paths) if separator == ':' else re.split(r'[;,]', jar_paths)
|
|
505
|
+
cp_parts = []
|
|
506
|
+
for p in parts:
|
|
507
|
+
p = p.strip()
|
|
508
|
+
if not p:
|
|
509
|
+
continue
|
|
510
|
+
if os.path.isfile(p):
|
|
511
|
+
cp_parts.append(p)
|
|
512
|
+
else:
|
|
513
|
+
print(f"WARNING: JAR not found, skipping: {p}", file=sys.stderr)
|
|
514
|
+
if not cp_parts:
|
|
515
|
+
print(f"ERROR: No valid JAR paths in classpath: {jar_paths}", file=sys.stderr)
|
|
516
|
+
sys.exit(3)
|
|
517
|
+
return separator.join(cp_parts)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# ============================================================
|
|
521
|
+
# 打印 class-not-found 提示
|
|
522
|
+
# ============================================================
|
|
523
|
+
def print_class_not_found_hint(class_name):
|
|
524
|
+
print(file=sys.stderr)
|
|
525
|
+
print(f"Class '{class_name}' not found in the provided JAR(s). Possible reasons:", file=sys.stderr)
|
|
526
|
+
print(f" 1. The class is in another dependency JAR - use 'resolve {class_name}' to locate it", file=sys.stderr)
|
|
527
|
+
print(f" 2. The class name is incorrect - check for inner classes (use $ for inner class separator)", file=sys.stderr)
|
|
528
|
+
print(f" 3. Multiple JARs needed - pass additional JAR paths separated by {os.pathsep}", file=sys.stderr)
|
|
529
|
+
print(f" Example: decompile.py show \"jar1.jar{os.pathsep}jar2.jar\" {class_name}", file=sys.stderr)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# ============================================================
|
|
533
|
+
# 命令实现
|
|
534
|
+
# ============================================================
|
|
535
|
+
def cmd_check():
|
|
536
|
+
print("=== Environment Check ===")
|
|
537
|
+
|
|
538
|
+
# Python
|
|
539
|
+
py_exe = sys.executable or 'python'
|
|
540
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
541
|
+
print("Python: FOUND")
|
|
542
|
+
print(f" python: {py_exe}")
|
|
543
|
+
print(f" version: {py_ver}")
|
|
544
|
+
|
|
545
|
+
jdk = find_jdk()
|
|
546
|
+
print("JDK: FOUND")
|
|
547
|
+
print(f" javap: {jdk['javap']}")
|
|
548
|
+
print(f" jar: {jdk['jar']}")
|
|
549
|
+
print(f" source: {jdk['source']}")
|
|
550
|
+
|
|
551
|
+
# JAVA_HOME 检查(Maven 依赖此变量)
|
|
552
|
+
java_home = os.environ.get('JAVA_HOME', '')
|
|
553
|
+
if java_home:
|
|
554
|
+
print(f" JAVA_HOME: {java_home}")
|
|
555
|
+
else:
|
|
556
|
+
inferred_home = os.path.dirname(os.path.dirname(jdk['javap']))
|
|
557
|
+
print(" JAVA_HOME: NOT SET")
|
|
558
|
+
print()
|
|
559
|
+
print("ERROR: JAVA_HOME is not set. Maven commands (dependency:tree) will fail.")
|
|
560
|
+
print(" Please set JAVA_HOME to your JDK directory, e.g.: " + inferred_home)
|
|
561
|
+
sys.exit(1)
|
|
562
|
+
|
|
563
|
+
maven = find_maven()
|
|
564
|
+
print("Maven: FOUND")
|
|
565
|
+
print(f" home: {maven['home']}")
|
|
566
|
+
print(f" source: {maven['source']}")
|
|
567
|
+
print(f" settings: {os.path.join(maven['home'], 'conf', 'settings.xml')}")
|
|
568
|
+
|
|
569
|
+
repo = find_maven_local_repo(maven['home'])
|
|
570
|
+
print(f"LocalRepo: {repo['path']}")
|
|
571
|
+
print(f" source: {repo['source']}")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def cmd_repo():
|
|
575
|
+
maven = find_maven()
|
|
576
|
+
repo = find_maven_local_repo(maven['home'])
|
|
577
|
+
print(repo['path'])
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def cmd_resolve(class_name, project_dir):
|
|
581
|
+
jdk = find_jdk()
|
|
582
|
+
maven = find_maven()
|
|
583
|
+
repo = find_maven_local_repo(maven['home'])
|
|
584
|
+
result = resolve_class_to_jar(class_name, project_dir, maven['home'], jdk['jar'], repo['path'])
|
|
585
|
+
print(f"JAR: {result['jar_path']}")
|
|
586
|
+
print(f"Coordinates: {result['group_id']}:{result['artifact_id']}:{result['version']}")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def cmd_find(group_id, artifact_id, version):
|
|
590
|
+
maven = find_maven()
|
|
591
|
+
repo = find_maven_local_repo(maven['home'])
|
|
592
|
+
jar_path = find_jar_from_coordinates(group_id, artifact_id, version, repo['path'], maven['home'])
|
|
593
|
+
print(jar_path)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def cmd_list(jar_path, keyword=None):
|
|
597
|
+
jdk = find_jdk()
|
|
598
|
+
try:
|
|
599
|
+
result = subprocess.run([jdk['jar'], 'tf', jar_path], capture_output=True, text=True)
|
|
600
|
+
for line in (result.stdout or '').splitlines():
|
|
601
|
+
if line.endswith('.class'):
|
|
602
|
+
if not keyword or keyword.lower() in line.lower():
|
|
603
|
+
print(line)
|
|
604
|
+
except Exception as e:
|
|
605
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
606
|
+
sys.exit(1)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def cmd_show(jar_paths, class_name, verbose=False):
|
|
610
|
+
jdk = find_jdk()
|
|
611
|
+
cp = resolve_classpath(jar_paths)
|
|
612
|
+
try:
|
|
613
|
+
javap_args = [jdk['javap'], '-p']
|
|
614
|
+
if verbose:
|
|
615
|
+
javap_args += ['-c', '-verbose']
|
|
616
|
+
javap_args += ['-cp', cp, class_name]
|
|
617
|
+
result = subprocess.run(javap_args, capture_output=True, text=True, errors='replace')
|
|
618
|
+
output = result.stdout or ''
|
|
619
|
+
stderr = result.stderr or ''
|
|
620
|
+
if 'class not found' in stderr.lower() or (result.returncode != 0 and 'class not found' in output.lower()):
|
|
621
|
+
print_class_not_found_hint(class_name)
|
|
622
|
+
print(output)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
625
|
+
sys.exit(1)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def cmd_api(jar_paths, class_name):
|
|
629
|
+
jdk = find_jdk()
|
|
630
|
+
cp = resolve_classpath(jar_paths)
|
|
631
|
+
try:
|
|
632
|
+
result = subprocess.run([jdk['javap'], '-p', '-cp', cp, class_name], capture_output=True, text=True, errors='replace')
|
|
633
|
+
output = result.stdout or ''
|
|
634
|
+
stderr = result.stderr or ''
|
|
635
|
+
if 'class not found' in stderr.lower() or (result.returncode != 0 and 'class not found' in output.lower()):
|
|
636
|
+
print_class_not_found_hint(class_name)
|
|
637
|
+
is_interface = any('interface ' in line for line in output.splitlines()[:5])
|
|
638
|
+
for line in output.splitlines():
|
|
639
|
+
if is_interface:
|
|
640
|
+
if (line == ''
|
|
641
|
+
or re.search(r'extends\s+java\.lang\.Object$', line)
|
|
642
|
+
or re.search(r'public\s+boolean\s+equals\(', line)
|
|
643
|
+
or re.search(r'public\s+int\s+hashCode\(', line)
|
|
644
|
+
or re.search(r'public\s+java\.lang\.String\s+toString\(', line)
|
|
645
|
+
or re.search(r'public\s+final\s+java\.lang\.Class\s+getClass\(', line)):
|
|
646
|
+
continue
|
|
647
|
+
print(line)
|
|
648
|
+
except Exception as e:
|
|
649
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
650
|
+
sys.exit(1)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def cmd_enum(jar_paths, class_name):
|
|
654
|
+
jdk = find_jdk()
|
|
655
|
+
cp = resolve_classpath(jar_paths)
|
|
656
|
+
try:
|
|
657
|
+
result = subprocess.run(
|
|
658
|
+
[jdk['javap'], '-p', '-c', '-verbose', '-cp', cp, class_name],
|
|
659
|
+
capture_output=True, text=True, errors='replace'
|
|
660
|
+
)
|
|
661
|
+
output = result.stdout or ''
|
|
662
|
+
stderr = result.stderr or ''
|
|
663
|
+
if 'class not found' in stderr.lower() or (result.returncode != 0 and 'class not found' in output.lower()):
|
|
664
|
+
print_class_not_found_hint(class_name)
|
|
665
|
+
print(output)
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
# Extract enum field names
|
|
669
|
+
enum_names = []
|
|
670
|
+
short_class = class_name.split('.')[-1]
|
|
671
|
+
for line in output.splitlines():
|
|
672
|
+
if 'static final' in line and '$VALUES' not in line and short_class in line:
|
|
673
|
+
parts = line.strip().rstrip(';').split()
|
|
674
|
+
if parts:
|
|
675
|
+
enum_names.append(parts[-1])
|
|
676
|
+
|
|
677
|
+
# Extract private fields (code, desc, value, etc.)
|
|
678
|
+
field_names = []
|
|
679
|
+
for line in output.splitlines():
|
|
680
|
+
field_match = re.match(
|
|
681
|
+
r'\s+private\s+(?:final\s+)?(?:java\.lang\.String|int|java\.lang\.Integer|long|java\.lang\.Long)\s+(\w+)\s*;',
|
|
682
|
+
line
|
|
683
|
+
)
|
|
684
|
+
if field_match:
|
|
685
|
+
fname = field_match.group(1)
|
|
686
|
+
if fname not in ('serialVersionUID', '$VALUES'):
|
|
687
|
+
field_names.append(fname)
|
|
688
|
+
|
|
689
|
+
# Parse <clinit> bytecode to extract enum constructor string args
|
|
690
|
+
# Look for the "static {};" section in javap output
|
|
691
|
+
in_clinit = False
|
|
692
|
+
enum_values = {}
|
|
693
|
+
current_strings = []
|
|
694
|
+
for line in output.splitlines():
|
|
695
|
+
if re.match(r'\s*static\s*\{\}', line):
|
|
696
|
+
in_clinit = True
|
|
697
|
+
continue
|
|
698
|
+
if not in_clinit:
|
|
699
|
+
continue
|
|
700
|
+
# Stop at next method or end
|
|
701
|
+
if re.match(r'\s*(public|private|protected)\s', line) and 'static' not in line:
|
|
702
|
+
break
|
|
703
|
+
|
|
704
|
+
# ldc or ldc_w with String comment
|
|
705
|
+
ldc_match = re.search(r'ldc(?:_w)?\s+#\d+\s+//\s*String\s+(.+)', line)
|
|
706
|
+
if ldc_match:
|
|
707
|
+
current_strings.append(ldc_match.group(1).strip())
|
|
708
|
+
continue
|
|
709
|
+
|
|
710
|
+
# putstatic for an enum field
|
|
711
|
+
putstatic_match = re.search(r'putstatic\s+#\d+\s*//.*Field\s+(\w+):', line)
|
|
712
|
+
if putstatic_match:
|
|
713
|
+
field_name = putstatic_match.group(1)
|
|
714
|
+
if field_name in enum_names and field_name != '$VALUES':
|
|
715
|
+
# First string is typically the enum name itself (from Enum.<init>), skip it
|
|
716
|
+
args = [s for s in current_strings if s != field_name]
|
|
717
|
+
enum_values[field_name] = args
|
|
718
|
+
current_strings = []
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
# new starts a fresh enum instance
|
|
722
|
+
if re.search(r'new\s+#', line):
|
|
723
|
+
current_strings = []
|
|
724
|
+
|
|
725
|
+
# Output
|
|
726
|
+
if enum_values:
|
|
727
|
+
for name in enum_names:
|
|
728
|
+
args = enum_values.get(name, [])
|
|
729
|
+
if args:
|
|
730
|
+
if field_names and len(field_names) >= len(args):
|
|
731
|
+
pairs = [f'{field_names[i]}="{args[i]}"' for i in range(len(args))]
|
|
732
|
+
print(f"{name}({', '.join(pairs)})")
|
|
733
|
+
else:
|
|
734
|
+
args_str = ', '.join(f'\"{a}\"' for a in args)
|
|
735
|
+
print(f"{name}({args_str})")
|
|
736
|
+
else:
|
|
737
|
+
print(name)
|
|
738
|
+
else:
|
|
739
|
+
for name in enum_names:
|
|
740
|
+
print(name)
|
|
741
|
+
if enum_names:
|
|
742
|
+
print(file=sys.stderr)
|
|
743
|
+
print("Note: Could not extract enum field values from bytecode.", file=sys.stderr)
|
|
744
|
+
print(" Use 'show -v' for full verbose output.", file=sys.stderr)
|
|
745
|
+
|
|
746
|
+
except Exception as e:
|
|
747
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
748
|
+
sys.exit(1)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def cmd_batch(jar_paths, class_names):
|
|
754
|
+
"""批量查看多个类的信息(单次 javap 调用)"""
|
|
755
|
+
jdk = find_jdk()
|
|
756
|
+
cp = resolve_classpath(jar_paths)
|
|
757
|
+
try:
|
|
758
|
+
javap_args = [jdk['javap'], '-p', '-cp', cp] + class_names
|
|
759
|
+
result = subprocess.run(javap_args, capture_output=True, text=True, errors='replace')
|
|
760
|
+
output = result.stdout or ''
|
|
761
|
+
stderr = result.stderr or ''
|
|
762
|
+
if 'class not found' in stderr.lower():
|
|
763
|
+
not_found = re.findall(r"Class (\S+) not found", stderr)
|
|
764
|
+
if not_found:
|
|
765
|
+
print(f"WARNING: Classes not found: {', '.join(not_found)}", file=sys.stderr)
|
|
766
|
+
print(output)
|
|
767
|
+
except Exception as e:
|
|
768
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
769
|
+
sys.exit(1)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def cmd_help():
|
|
773
|
+
sep = os.pathsep
|
|
774
|
+
print("JAR Decompile Tool (Python - Cross Platform)")
|
|
775
|
+
print()
|
|
776
|
+
print("Usage:")
|
|
777
|
+
print(" decompile.py check Environment pre-check")
|
|
778
|
+
print(" decompile.py repo Print Maven repo path")
|
|
779
|
+
print(" decompile.py resolve <FQN> [-p projectDir] Find JAR containing a class")
|
|
780
|
+
print(" decompile.py find <G> <A> <V> Find JAR from Maven coordinates")
|
|
781
|
+
print(" decompile.py list <jarPath> [keyword] List classes in JAR")
|
|
782
|
+
print(f" decompile.py show <jarPath[{sep}...]> <FQN> [-v] Show class details (-v for verbose bytecode)")
|
|
783
|
+
print(f" decompile.py api <jarPath[{sep}...]> <FQN> Show interface methods (multi-JAR supported)")
|
|
784
|
+
print(f" decompile.py enum <jarPath[{sep}...]> <FQN> Show enum values (multi-JAR supported)")
|
|
785
|
+
print(f" decompile.py batch <jarPath[{sep}...]> <FQN1> <FQN2> ... Batch show multiple classes")
|
|
786
|
+
print()
|
|
787
|
+
print(f"Note: show/api/enum accept multiple JAR paths separated by {sep} (or ,) for cross-JAR class resolution.")
|
|
788
|
+
print(f" Example: decompile.py show \"jar1.jar{sep}jar2.jar\" com.example.MyClass")
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
# ============================================================
|
|
792
|
+
# 主入口
|
|
793
|
+
# ============================================================
|
|
794
|
+
def main():
|
|
795
|
+
# Ensure stdout/stderr can handle unicode on Windows
|
|
796
|
+
if sys.platform == 'win32':
|
|
797
|
+
import io
|
|
798
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
799
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
800
|
+
args = sys.argv[1:]
|
|
801
|
+
if not args:
|
|
802
|
+
cmd_help()
|
|
803
|
+
return
|
|
804
|
+
|
|
805
|
+
cmd = args[0]
|
|
806
|
+
|
|
807
|
+
if cmd == 'check':
|
|
808
|
+
cmd_check()
|
|
809
|
+
elif cmd == 'repo':
|
|
810
|
+
cmd_repo()
|
|
811
|
+
elif cmd == 'resolve':
|
|
812
|
+
if len(args) < 2:
|
|
813
|
+
print("Usage: decompile.py resolve <fully-qualified-class-name> [-p projectDir]", file=sys.stderr)
|
|
814
|
+
sys.exit(3)
|
|
815
|
+
class_name = args[1]
|
|
816
|
+
project_dir = ''
|
|
817
|
+
if '-p' in args:
|
|
818
|
+
idx = args.index('-p')
|
|
819
|
+
if idx + 1 < len(args):
|
|
820
|
+
project_dir = args[idx + 1]
|
|
821
|
+
cmd_resolve(class_name, project_dir)
|
|
822
|
+
elif cmd == 'find':
|
|
823
|
+
if len(args) < 4:
|
|
824
|
+
print("Usage: decompile.py find <groupId> <artifactId> <version>", file=sys.stderr)
|
|
825
|
+
sys.exit(3)
|
|
826
|
+
cmd_find(args[1], args[2], args[3])
|
|
827
|
+
elif cmd == 'list':
|
|
828
|
+
if len(args) < 2:
|
|
829
|
+
print("Usage: decompile.py list <jarPath> [keyword]", file=sys.stderr)
|
|
830
|
+
sys.exit(3)
|
|
831
|
+
keyword = args[2] if len(args) >= 3 else None
|
|
832
|
+
cmd_list(args[1], keyword)
|
|
833
|
+
elif cmd == 'show':
|
|
834
|
+
if len(args) < 3:
|
|
835
|
+
print("Usage: decompile.py show <jarPath[;...]> <fully-qualified-class-name> [-v]", file=sys.stderr)
|
|
836
|
+
sys.exit(3)
|
|
837
|
+
verbose = '-v' in args or '--verbose' in args
|
|
838
|
+
cmd_show(args[1], args[2], verbose=verbose)
|
|
839
|
+
elif cmd == 'api':
|
|
840
|
+
if len(args) < 3:
|
|
841
|
+
print("Usage: decompile.py api <jarPath[;...]> <fully-qualified-class-name>", file=sys.stderr)
|
|
842
|
+
sys.exit(3)
|
|
843
|
+
cmd_api(args[1], args[2])
|
|
844
|
+
elif cmd == 'enum':
|
|
845
|
+
if len(args) < 3:
|
|
846
|
+
print("Usage: decompile.py enum <jarPath[;...]> <fully-qualified-class-name>", file=sys.stderr)
|
|
847
|
+
sys.exit(3)
|
|
848
|
+
cmd_enum(args[1], args[2])
|
|
849
|
+
elif cmd == 'batch':
|
|
850
|
+
if len(args) < 3:
|
|
851
|
+
print("Usage: decompile.py batch <jarPath[;...]> <FQN1> [FQN2] ...", file=sys.stderr)
|
|
852
|
+
sys.exit(3)
|
|
853
|
+
cmd_batch(args[1], args[2:])
|
|
854
|
+
elif cmd in ('help', '-h', '--help'):
|
|
855
|
+
cmd_help()
|
|
856
|
+
else:
|
|
857
|
+
print(f"Unknown command: {cmd}", file=sys.stderr)
|
|
858
|
+
cmd_help()
|
|
859
|
+
sys.exit(1)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
if __name__ == '__main__':
|
|
863
|
+
main()
|