@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.
Files changed (183) hide show
  1. package/README.md +41 -0
  2. package/cbb/dev-standards/rules/cbb-ai-behavior.md +104 -0
  3. package/cbb/dev-standards/rules/cbb-coding-rule.md +50 -0
  4. package/cbb/dev-standards/rules/cbb-priority.md +56 -0
  5. package/cbb/dev-standards/skills/architecture-specs/SKILL.md +129 -0
  6. package/cbb/dev-standards/skills/coding-specs/SKILL.md +376 -0
  7. package/cbb/dev-standards/skills/coding-specs/references/concurrency.md +53 -0
  8. package/cbb/dev-standards/skills/coding-specs/references/config-center.md +16 -0
  9. package/cbb/dev-standards/skills/coding-specs/references/distributed.md +42 -0
  10. package/cbb/dev-standards/skills/coding-specs/references/es-coding.md +60 -0
  11. package/cbb/dev-standards/skills/coding-specs/references/scheduled-task.md +25 -0
  12. package/cbb/dev-standards/skills/coding-specs/references/security.md +11 -0
  13. package/cbb/dev-standards/skills/coding-specs/references/unit-testing.md +43 -0
  14. package/cbb/dev-standards/skills/es-design-specs/SKILL.md +104 -0
  15. package/cbb/dev-standards/skills/mysql-design-specs/SKILL.md +89 -0
  16. package/cbb/lib/install/claude-code.js +20 -0
  17. package/cbb/lib/install/cleanup.js +64 -0
  18. package/cbb/lib/install/init.js +1875 -0
  19. package/cbb/lib/install/qoder.js +20 -0
  20. package/cbb/lib/install/workspaces.js +232 -0
  21. package/cbb/lib/openspec/index.js +554 -0
  22. package/cbb/lib/superpowers/index.js +265 -0
  23. package/cbb/lib/utils/check-update.js +147 -0
  24. package/cbb/lib/utils/checkbox.js +383 -0
  25. package/cbb/lib/utils/gitignore.js +69 -0
  26. package/cbb/lib/utils/output.js +64 -0
  27. package/cbb/lib/utils/settings.js +119 -0
  28. package/cbb/lib/utils/version.js +135 -0
  29. package/cbb/lib/wiki/api-client.js +358 -0
  30. package/cbb/lib/wiki/cli.js +427 -0
  31. package/cbb/lib/wiki/convert.js +464 -0
  32. package/cbb/lib/wiki/index.js +218 -0
  33. package/cbb/lib/wiki/mermaid-guard.js +103 -0
  34. package/cbb/lib/wiki/split.js +131 -0
  35. package/cbb/tools/cbb-decompile-jar/SKILL.md +220 -0
  36. package/cbb/tools/cbb-decompile-jar/scripts/decompile.py +863 -0
  37. package/cbb/tools/cbb-design-to-wiki/SKILL.md +180 -0
  38. package/cbb/tools/cbb-mvn-guide/SKILL.md +281 -0
  39. package/cbb/tools/cbb-mvn-guide/scripts/mvn_jdk_manager.py +378 -0
  40. package/cbb/tools/cbb-mvn-guide/scripts/mvn_pom_jdk_reader.py +197 -0
  41. package/cbb/tools/cbb-plantuml-authoring/SKILL.md +448 -0
  42. package/cbb/tools/cbb-plantuml-authoring/references/drawing-templates.md +479 -0
  43. package/cbb/tools/cbb-wiki-ops/SKILL.md +134 -0
  44. package/cbb/tools/cbb-wiki-ops/scripts/check-auth.js +44 -0
  45. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +96 -0
  46. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +67 -0
  47. package/cbb/worktrees/_shared/scripts/push-core.js +136 -0
  48. package/cbb/worktrees/_shared/scripts/silence-popup.js +23 -0
  49. package/cbb/worktrees/commands/close.md +64 -0
  50. package/cbb/worktrees/commands/extend.md +65 -0
  51. package/cbb/worktrees/commands/init.md +55 -0
  52. package/cbb/worktrees/commands/push.md +42 -0
  53. package/cbb/worktrees/skills/openspec-close-worktree/SKILL.md +449 -0
  54. package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-env.js +76 -0
  55. package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-unarchived.js +48 -0
  56. package/cbb/worktrees/skills/openspec-close-worktree/scripts/delete-branches.js +136 -0
  57. package/cbb/worktrees/skills/openspec-close-worktree/scripts/discover-apps.js +76 -0
  58. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-target-worktree.js +96 -0
  59. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-workspace-root.js +67 -0
  60. package/cbb/worktrees/skills/openspec-close-worktree/scripts/remove-worktrees.js +124 -0
  61. package/cbb/worktrees/skills/openspec-close-worktree/scripts/safety-check.js +174 -0
  62. package/cbb/worktrees/skills/openspec-close-worktree/scripts/silence-popup.js +23 -0
  63. package/cbb/worktrees/skills/openspec-extend-worktree/SKILL.md +390 -0
  64. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-env.js +95 -0
  65. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-repos.js +98 -0
  66. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js +135 -0
  67. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-target-worktree.js +96 -0
  68. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-workspace-root.js +67 -0
  69. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/install-ai.js +150 -0
  70. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/list-available-apps.js +88 -0
  71. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/push-core.js +136 -0
  72. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/silence-popup.js +23 -0
  73. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/sync-repos.js +93 -0
  74. package/cbb/worktrees/skills/openspec-init-worktree/SKILL.md +539 -0
  75. package/cbb/worktrees/skills/openspec-init-worktree/scripts/auto-open.js +136 -0
  76. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env-deep.js +87 -0
  77. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env.js +90 -0
  78. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-repos.js +171 -0
  79. package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-branches.js +103 -0
  80. package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-worktrees.js +154 -0
  81. package/cbb/worktrees/skills/openspec-init-worktree/scripts/find-workspace-root.js +67 -0
  82. package/cbb/worktrees/skills/openspec-init-worktree/scripts/generate-app-options.js +93 -0
  83. package/cbb/worktrees/skills/openspec-init-worktree/scripts/install-ai.js +177 -0
  84. package/cbb/worktrees/skills/openspec-init-worktree/scripts/parse-config.js +67 -0
  85. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-branches.js +101 -0
  86. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-core.js +136 -0
  87. package/cbb/worktrees/skills/openspec-init-worktree/scripts/silence-popup.js +23 -0
  88. package/cbb/worktrees/skills/openspec-init-worktree/scripts/sync-repos.js +95 -0
  89. package/cbb/worktrees/skills/openspec-init-worktree/scripts/update-gitignore.js +134 -0
  90. package/cbb/worktrees/skills/openspec-push-worktrees/SKILL.md +307 -0
  91. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/commit-worktrees.js +269 -0
  92. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-target-worktree.js +96 -0
  93. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-workspace-root.js +67 -0
  94. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-core.js +136 -0
  95. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-worktrees.js +159 -0
  96. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/silence-popup.js +23 -0
  97. package/config/cbb.yaml +6 -0
  98. package/config/config.sample.json +16 -0
  99. package/config/openspec/config.yaml +28 -0
  100. package/config/openspec/schemas/spec-driven/schema.yaml +247 -0
  101. package/config/openspec/schemas/spec-driven/templates/design.md +475 -0
  102. package/config/openspec/schemas/spec-driven/templates/proposal.md +58 -0
  103. package/config/openspec/schemas/spec-driven/templates/spec.md +8 -0
  104. package/config/openspec/schemas/spec-driven/templates/tasks.md +9 -0
  105. package/config/workspaces.json +15 -0
  106. package/openspec/.version +6 -0
  107. package/openspec/commands/apply.md +182 -0
  108. package/openspec/commands/archive.md +223 -0
  109. package/openspec/commands/bulk-archive.md +334 -0
  110. package/openspec/commands/continue.md +112 -0
  111. package/openspec/commands/explore.md +206 -0
  112. package/openspec/commands/ff.md +111 -0
  113. package/openspec/commands/new.md +70 -0
  114. package/openspec/commands/onboard.md +555 -0
  115. package/openspec/commands/propose.md +157 -0
  116. package/openspec/commands/sync.md +256 -0
  117. package/openspec/commands/update.md +85 -0
  118. package/openspec/commands/verify.md +169 -0
  119. package/openspec/skills/openspec-apply-change/SKILL.md +187 -0
  120. package/openspec/skills/openspec-archive-change/SKILL.md +181 -0
  121. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +338 -0
  122. package/openspec/skills/openspec-continue-change/SKILL.md +117 -0
  123. package/openspec/skills/openspec-explore/SKILL.md +342 -0
  124. package/openspec/skills/openspec-ff-change/SKILL.md +116 -0
  125. package/openspec/skills/openspec-new-change/SKILL.md +76 -0
  126. package/openspec/skills/openspec-onboard/SKILL.md +560 -0
  127. package/openspec/skills/openspec-propose/SKILL.md +162 -0
  128. package/openspec/skills/openspec-sync-specs/SKILL.md +261 -0
  129. package/openspec/skills/openspec-update-change/SKILL.md +90 -0
  130. package/openspec/skills/openspec-verify-change/SKILL.md +174 -0
  131. package/package.json +46 -0
  132. package/superpowers/.version +6 -0
  133. package/superpowers/skills/brainstorming/SKILL.md +250 -0
  134. package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
  135. package/superpowers/skills/brainstorming/scripts/helper.js +167 -0
  136. package/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
  137. package/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
  138. package/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
  139. package/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  140. package/superpowers/skills/brainstorming/visual-companion.md +299 -0
  141. package/superpowers/skills/dispatching-parallel-agents/SKILL.md +167 -0
  142. package/superpowers/skills/executing-plans/SKILL.md +64 -0
  143. package/superpowers/skills/finishing-a-development-branch/SKILL.md +225 -0
  144. package/superpowers/skills/receiving-code-review/SKILL.md +205 -0
  145. package/superpowers/skills/requesting-code-review/SKILL.md +95 -0
  146. package/superpowers/skills/requesting-code-review/code-reviewer.md +181 -0
  147. package/superpowers/skills/subagent-driven-development/SKILL.md +568 -0
  148. package/superpowers/skills/subagent-driven-development/implementer-prompt.md +154 -0
  149. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +115 -0
  150. package/superpowers/skills/subagent-driven-development/scripts/review-package +46 -0
  151. package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +40 -0
  152. package/superpowers/skills/subagent-driven-development/scripts/task-brief +41 -0
  153. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
  154. package/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
  155. package/superpowers/skills/systematic-debugging/SKILL.md +283 -0
  156. package/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  157. package/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
  158. package/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
  159. package/superpowers/skills/systematic-debugging/find-polluter.sh +72 -0
  160. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
  161. package/superpowers/skills/systematic-debugging/test-academic.md +14 -0
  162. package/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
  163. package/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
  164. package/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
  165. package/superpowers/skills/test-driven-development/SKILL.md +320 -0
  166. package/superpowers/skills/test-driven-development/writing-good-tests.md +198 -0
  167. package/superpowers/skills/using-git-worktrees/SKILL.md +167 -0
  168. package/superpowers/skills/using-superpowers/SKILL.md +63 -0
  169. package/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
  170. package/superpowers/skills/using-superpowers/references/codex-tools.md +108 -0
  171. package/superpowers/skills/using-superpowers/references/gemini-tools.md +63 -0
  172. package/superpowers/skills/using-superpowers/references/hermes-tools.md +56 -0
  173. package/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
  174. package/superpowers/skills/verification-before-completion/SKILL.md +120 -0
  175. package/superpowers/skills/writing-plans/SKILL.md +171 -0
  176. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
  177. package/superpowers/skills/writing-skills/SKILL.md +679 -0
  178. package/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
  179. package/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  180. package/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
  181. package/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
  182. package/superpowers/skills/writing-skills/render-graphs.js +169 -0
  183. 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()