py-wlcli 0.8.0__py3-none-any.whl

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 (320) hide show
  1. py_wlcli-0.8.0.dist-info/METADATA +139 -0
  2. py_wlcli-0.8.0.dist-info/RECORD +320 -0
  3. py_wlcli-0.8.0.dist-info/WHEEL +4 -0
  4. py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
  5. py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
  6. wl_cli/__init__.py +27 -0
  7. wl_cli/__main__.py +130 -0
  8. wl_cli/capabilities.py +79 -0
  9. wl_cli/cc/__init__.py +5 -0
  10. wl_cli/cc/checks.py +1019 -0
  11. wl_cli/cc/cli.py +370 -0
  12. wl_cli/cc/mutate.py +1350 -0
  13. wl_cli/cli_click.py +156 -0
  14. wl_cli/commands/__init__.py +84 -0
  15. wl_cli/commands/base.py +80 -0
  16. wl_cli/commands/buildcommands/__init__.py +1 -0
  17. wl_cli/commands/buildcommands/build.py +71 -0
  18. wl_cli/commands/buildcommands/build_utils.py +47 -0
  19. wl_cli/commands/buildcommands/builders/__init__.py +18 -0
  20. wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
  21. wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
  22. wl_cli/commands/buildcommands/builders/environment.py +81 -0
  23. wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
  24. wl_cli/commands/buildcommands/builders/stubs.py +58 -0
  25. wl_cli/commands/buildcommands/dist.py +326 -0
  26. wl_cli/commands/buildcommands/test.py +89 -0
  27. wl_cli/commands/clean/__init__.py +1 -0
  28. wl_cli/commands/clean/build.py +151 -0
  29. wl_cli/commands/clean/clean.py +294 -0
  30. wl_cli/commands/clean/clean_utils.py +77 -0
  31. wl_cli/commands/clean/core.py +236 -0
  32. wl_cli/commands/clean/dead_code/__init__.py +0 -0
  33. wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
  34. wl_cli/commands/clean/dead_code/detector.py +161 -0
  35. wl_cli/commands/clean/dead_code/fixer.py +176 -0
  36. wl_cli/commands/clean/dead_code/reporter.py +295 -0
  37. wl_cli/commands/clean/dead_code/utils.py +0 -0
  38. wl_cli/commands/clean/lfs.py +215 -0
  39. wl_cli/commands/clean/python.py +81 -0
  40. wl_cli/commands/clean/rust.py +91 -0
  41. wl_cli/commands/clean/utils/__init__.py +34 -0
  42. wl_cli/commands/clean/venv.py +91 -0
  43. wl_cli/commands/cleanup.py +73 -0
  44. wl_cli/commands/click_base.py +79 -0
  45. wl_cli/commands/config/__init__.py +1 -0
  46. wl_cli/commands/config/config.py +291 -0
  47. wl_cli/commands/debug/__init__.py +6 -0
  48. wl_cli/commands/debug/debug.py +309 -0
  49. wl_cli/commands/debug/debug_cache.py +86 -0
  50. wl_cli/commands/debug/debug_subagent.py +156 -0
  51. wl_cli/commands/debug/output_formatter.py +165 -0
  52. wl_cli/commands/debug/source_hasher.py +92 -0
  53. wl_cli/commands/format/__init__.py +13 -0
  54. wl_cli/commands/format/format_cache_adapter.py +125 -0
  55. wl_cli/commands/format/format_command.py +197 -0
  56. wl_cli/commands/format/format_coordinator.py +191 -0
  57. wl_cli/commands/format/format_executor.py +470 -0
  58. wl_cli/commands/format/format_outcome.py +44 -0
  59. wl_cli/commands/format/format_path_handler.py +1016 -0
  60. wl_cli/commands/format/format_progress.py +62 -0
  61. wl_cli/commands/format/format_reporter.py +31 -0
  62. wl_cli/commands/format/python_formatter.py +506 -0
  63. wl_cli/commands/format/rust_formatter.py +106 -0
  64. wl_cli/commands/gate/__init__.py +1 -0
  65. wl_cli/commands/gate/gate_command.py +862 -0
  66. wl_cli/commands/gate/preflight.py +445 -0
  67. wl_cli/commands/git/__init__.py +1 -0
  68. wl_cli/commands/git/config.py +169 -0
  69. wl_cli/commands/git/git_facts.py +60 -0
  70. wl_cli/commands/git/git_flow.py +86 -0
  71. wl_cli/commands/git/hook.py +112 -0
  72. wl_cli/commands/git/policy.py +216 -0
  73. wl_cli/commands/git/push_update.py +82 -0
  74. wl_cli/commands/initenv/__init__.py +1 -0
  75. wl_cli/commands/initenv/common/__init__.py +23 -0
  76. wl_cli/commands/initenv/common/check_git_bash.py +78 -0
  77. wl_cli/commands/initenv/common/error_handler.py +68 -0
  78. wl_cli/commands/initenv/common/exceptions.py +52 -0
  79. wl_cli/commands/initenv/components/__init__.py +4 -0
  80. wl_cli/commands/initenv/components/cargo_sync.py +240 -0
  81. wl_cli/commands/initenv/components/config_manager.py +106 -0
  82. wl_cli/commands/initenv/components/directory_utils.py +71 -0
  83. wl_cli/commands/initenv/components/git/__init__.py +9 -0
  84. wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
  85. wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
  86. wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
  87. wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
  88. wl_cli/commands/initenv/components/git/git_utility.py +254 -0
  89. wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
  90. wl_cli/commands/initenv/components/git_initializer.py +212 -0
  91. wl_cli/commands/initenv/components/i18n_manager.py +104 -0
  92. wl_cli/commands/initenv/components/log_manager.py +241 -0
  93. wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
  94. wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
  95. wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
  96. wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
  97. wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
  98. wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
  99. wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
  100. wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
  101. wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
  102. wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
  103. wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
  104. wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
  105. wl_cli/commands/initenv/components/rust_initializer.py +145 -0
  106. wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
  107. wl_cli/commands/initenv/components/symlink_manager.py +52 -0
  108. wl_cli/commands/initenv/components/venv_manager.py +118 -0
  109. wl_cli/commands/initenv/initenv.py +169 -0
  110. wl_cli/commands/initenv/services/__init__.py +11 -0
  111. wl_cli/commands/initenv/services/environment_initializer.py +288 -0
  112. wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
  113. wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
  114. wl_cli/commands/lfs/README.md +221 -0
  115. wl_cli/commands/lfs/__init__.py +5 -0
  116. wl_cli/commands/lfs/cli.py +106 -0
  117. wl_cli/commands/lfs/config.py +164 -0
  118. wl_cli/commands/lfs/hook.py +61 -0
  119. wl_cli/commands/lfs/initializer.py +531 -0
  120. wl_cli/commands/lfs/manager.py +95 -0
  121. wl_cli/commands/lfs/policy.py +165 -0
  122. wl_cli/commands/lfs/watcher.py +294 -0
  123. wl_cli/commands/lint/__init__.py +1 -0
  124. wl_cli/commands/lint/lint_command.py +297 -0
  125. wl_cli/commands/lint/lint_executor.py +409 -0
  126. wl_cli/commands/lint/lint_reporter.py +579 -0
  127. wl_cli/commands/lint/lint_run.py +258 -0
  128. wl_cli/commands/lint/mypy_config.py +90 -0
  129. wl_cli/commands/registry.py +103 -0
  130. wl_cli/commands/self/__init__.py +7 -0
  131. wl_cli/commands/self/common/__init__.py +8 -0
  132. wl_cli/commands/self/common/capabilities.py +30 -0
  133. wl_cli/commands/self/common/config.py +27 -0
  134. wl_cli/commands/self/common/version_cache.py +85 -0
  135. wl_cli/commands/self/handlers/__init__.py +19 -0
  136. wl_cli/commands/self/handlers/base_handler.py +28 -0
  137. wl_cli/commands/self/handlers/install_handler.py +85 -0
  138. wl_cli/commands/self/handlers/status_handler.py +66 -0
  139. wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
  140. wl_cli/commands/self/handlers/update_handler.py +102 -0
  141. wl_cli/commands/self/handlers/version_handler.py +16 -0
  142. wl_cli/commands/self/self_command.py +237 -0
  143. wl_cli/commands/self/services/__init__.py +15 -0
  144. wl_cli/commands/self/services/installation_manager.py +187 -0
  145. wl_cli/commands/self/services/local_installer.py +165 -0
  146. wl_cli/commands/self/services/pypi_updater.py +278 -0
  147. wl_cli/commands/self/services/windows_update.py +174 -0
  148. wl_cli/commands/testcommands/__init__.py +8 -0
  149. wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
  150. wl_cli/commands/testcommands/batch_processor.py +278 -0
  151. wl_cli/commands/testcommands/cache_manager.py +1156 -0
  152. wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
  153. wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
  154. wl_cli/commands/testcommands/progress_manager.py +79 -0
  155. wl_cli/commands/testcommands/python_env_detector.py +207 -0
  156. wl_cli/commands/testcommands/report_generator.py +101 -0
  157. wl_cli/commands/testcommands/single_test_executor.py +250 -0
  158. wl_cli/commands/testcommands/speed_checker.py +118 -0
  159. wl_cli/commands/testcommands/test_command_builder.py +165 -0
  160. wl_cli/commands/testcommands/test_commands.py +786 -0
  161. wl_cli/commands/testcommands/test_reporter.py +181 -0
  162. wl_cli/commands/testcommands/test_result_handler.py +96 -0
  163. wl_cli/commands/testcommands/test_runner.py +1343 -0
  164. wl_cli/commands/testcommands/test_utils.py +197 -0
  165. wl_cli/commands/testcommands/utils/__init__.py +17 -0
  166. wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
  167. wl_cli/commands/testcommands/utils/env_info.py +36 -0
  168. wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
  169. wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
  170. wl_cli/commands/testcommands/utils/project_root.py +61 -0
  171. wl_cli/commands/update/__init__.py +9 -0
  172. wl_cli/commands/update/services/__init__.py +21 -0
  173. wl_cli/commands/update/services/backup_manager.py +207 -0
  174. wl_cli/commands/update/services/component_manager.py +83 -0
  175. wl_cli/commands/update/services/conflict_resolver.py +189 -0
  176. wl_cli/commands/update/services/vendors_updater.py +85 -0
  177. wl_cli/commands/update/services/version_tracker.py +127 -0
  178. wl_cli/commands/update/update_command.py +270 -0
  179. wl_cli/commands/update/utils/__init__.py +3 -0
  180. wl_cli/commands/version/__init__.py +9 -0
  181. wl_cli/commands/version/version_command.py +155 -0
  182. wl_cli/commands/workflow.py +477 -0
  183. wl_cli/env/__init__.py +5 -0
  184. wl_cli/env/cli.py +167 -0
  185. wl_cli/env_status.py +157 -0
  186. wl_cli/envelope.py +91 -0
  187. wl_cli/exceptions.py +85 -0
  188. wl_cli/lib/__init__.py +3 -0
  189. wl_cli/lib/rust_utils.py +35 -0
  190. wl_cli/mcp/__init__.py +6 -0
  191. wl_cli/mcp/capabilities.py +19 -0
  192. wl_cli/mcp/cli.py +22 -0
  193. wl_cli/mcp/gitlab.py +553 -0
  194. wl_cli/mcp/k8s.py +344 -0
  195. wl_cli/mcp/protocol.py +159 -0
  196. wl_cli/platform.py +139 -0
  197. wl_cli/quality/__init__.py +18 -0
  198. wl_cli/utils/__init__.py +7 -0
  199. wl_cli/utils/cache.py +491 -0
  200. wl_cli/utils/cache_ledger.py +355 -0
  201. wl_cli/utils/cache_store.py +546 -0
  202. wl_cli/utils/config/__init__.py +27 -0
  203. wl_cli/utils/config/config_loader.py +163 -0
  204. wl_cli/utils/config/core.py +862 -0
  205. wl_cli/utils/config/defaults.py +142 -0
  206. wl_cli/utils/config/env_exporter.py +86 -0
  207. wl_cli/utils/config/env_parser.py +108 -0
  208. wl_cli/utils/config/validation.py +280 -0
  209. wl_cli/utils/error_handler.py +165 -0
  210. wl_cli/utils/file_copier.py +56 -0
  211. wl_cli/utils/file_operations.py +506 -0
  212. wl_cli/utils/language_utils.py +122 -0
  213. wl_cli/utils/lazy_import.py +240 -0
  214. wl_cli/utils/log_levels.py +35 -0
  215. wl_cli/utils/log_rotators.py +74 -0
  216. wl_cli/utils/logging.py +458 -0
  217. wl_cli/utils/output_context.py +142 -0
  218. wl_cli/utils/performance/__init__.py +1 -0
  219. wl_cli/utils/performance/decorators.py +111 -0
  220. wl_cli/utils/performance/exporter.py +38 -0
  221. wl_cli/utils/performance/models.py +16 -0
  222. wl_cli/utils/performance/monitor.py +161 -0
  223. wl_cli/utils/performance_monitor.py +32 -0
  224. wl_cli/utils/platform_adapter.py +131 -0
  225. wl_cli/utils/pre_commit.py +266 -0
  226. wl_cli/utils/pre_commit_config.py +72 -0
  227. wl_cli/utils/root.py +521 -0
  228. wl_cli/utils/root_fallback.py +256 -0
  229. wl_cli/utils/run_evidence.py +236 -0
  230. wl_cli/utils/structured_logger.py +18 -0
  231. wl_cli/utils/structured_logging/__init__.py +19 -0
  232. wl_cli/utils/structured_logging/config.py +337 -0
  233. wl_cli/utils/structured_logging/core.py +177 -0
  234. wl_cli/utils/structured_logging/env.py +56 -0
  235. wl_cli/utils/structured_logging/filtering.py +75 -0
  236. wl_cli/utils/structured_logging/filters.py +55 -0
  237. wl_cli/utils/structured_logging/formatters.py +251 -0
  238. wl_cli/utils/structured_logging/handlers.py +308 -0
  239. wl_cli/utils/structured_logging/logger.py +354 -0
  240. wl_cli/utils/subprocess.py +71 -0
  241. wl_cli/utils/subprocess_utils.py +260 -0
  242. wl_cli/utils/test_utils/__init__.py +5 -0
  243. wl_cli/utils/test_utils/test_filter.py +36 -0
  244. wl_cli/utils/uv_run.py +79 -0
  245. wl_cli/utils/uv_tool.py +117 -0
  246. wl_cli/utils/uv_tree.py +71 -0
  247. wl_cli/utils/uv_workspace.py +174 -0
  248. wl_cli/utils/version/__init__.py +15 -0
  249. wl_cli/utils/version/pypi_version_checker.py +95 -0
  250. wl_cli/utils/version/version_comparator.py +133 -0
  251. wl_cli/utils/version/version_detectors.py +312 -0
  252. wl_cli/utils/version/version_service.py +131 -0
  253. wl_cli/utils/version/version_updater.py +198 -0
  254. wl_cli/utils/wl_dir_updater.py +1440 -0
  255. wl_cli/utils/workspace_detector/__init__.py +28 -0
  256. wl_cli/utils/workspace_detector/core.py +161 -0
  257. wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
  258. wl_cli/utils/workspace_detector/exceptions.py +5 -0
  259. wl_cli/utils/workspace_detector/types.py +17 -0
  260. wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
  261. wl_cli/vendors/config/.codespellrc +2 -0
  262. wl_cli/vendors/config/dependencies.toml +10 -0
  263. wl_cli/vendors/config/mypy.ini +62 -0
  264. wl_cli/vendors/config/pyproject_defaults.toml +28 -0
  265. wl_cli/vendors/config/template_config.toml +24 -0
  266. wl_cli/vendors/config/tool_configs.toml +76 -0
  267. wl_cli/vendors/git/.gitignore +122 -0
  268. wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
  269. wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
  270. wl_cli/vendors/hooks/commit-msg.sample +24 -0
  271. wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
  272. wl_cli/vendors/hooks/post-update.sample +8 -0
  273. wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
  274. wl_cli/vendors/hooks/pre-commit +45 -0
  275. wl_cli/vendors/hooks/pre-commit.legacy +31 -0
  276. wl_cli/vendors/hooks/pre-commit.sample +49 -0
  277. wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
  278. wl_cli/vendors/hooks/pre-push.sample +60 -0
  279. wl_cli/vendors/hooks/pre-rebase.sample +169 -0
  280. wl_cli/vendors/hooks/pre-receive.sample +24 -0
  281. wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
  282. wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
  283. wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
  284. wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
  285. wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
  286. wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
  287. wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
  288. wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
  289. wl_cli/vendors/hooks/unix/post-update.sample +8 -0
  290. wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
  291. wl_cli/vendors/hooks/unix/pre-commit +190 -0
  292. wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
  293. wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
  294. wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
  295. wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
  296. wl_cli/vendors/hooks/unix/pre-push +417 -0
  297. wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
  298. wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
  299. wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
  300. wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
  301. wl_cli/vendors/hooks/update.sample +128 -0
  302. wl_cli/vendors/hooks/win/work.bat +63 -0
  303. wl_cli/vendors/project/.gitignore +90 -0
  304. wl_cli/vendors/project/.python-version +1 -0
  305. wl_cli/vendors/project/LICENSE +21 -0
  306. wl_cli/vendors/project/README.md +0 -0
  307. wl_cli/vendors/project/pyproject.toml +27 -0
  308. wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
  309. wl_cli/vendors/project/pyproject_workspace.toml +14 -0
  310. wl_cli/vendors/readme/README.md +120 -0
  311. wl_cli/vendors/rust/Cargo.toml +14 -0
  312. wl_cli/vendors/rust/lib/__init__.py +3 -0
  313. wl_cli/vendors/rust/lib/rust_utils.py +31 -0
  314. wl_cli/vendors/rust/src/lib.rs +13 -0
  315. wl_cli/vendors/rust/src/main.rs +3 -0
  316. wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
  317. wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
  318. wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
  319. wl_cli/vendors/sop_doc/python-sop.md +342 -0
  320. wl_cli/vendors/sop_doc/rust-readme.md +219 -0
@@ -0,0 +1,258 @@
1
+ """One ``wl lint`` run, and the verdict it carries (#170).
2
+
3
+ Before this module a single ``wl lint`` started ruff four times (once for the
4
+ verdict, once per report section) and mypy three times (once for the main
5
+ section, once per ``error.md`` scope), while ``issues_found`` re-read ruff's
6
+ stdout a fourth time. The executor now runs each requested tool exactly once
7
+ and hands the finished results back as a :class:`LintRun`; the reporter only
8
+ renders it.
9
+
10
+ The value is deliberately dumb. It stores the subprocess results and derives
11
+ the two decisions lint makes -- :attr:`LintRun.exit_code` and
12
+ :attr:`LintRun.issues_found`. Both are read off ruff's *status*, never off its
13
+ stdout, which is what let the old reporter call a launch failure "no issues".
14
+
15
+ ruff's report section is its own ``--output-format full`` text, captured
16
+ verbatim (#170 review F1/F5). The old report stored exactly that text; the
17
+ first cut of this module ran ruff with ``--output-format json`` and rendered a
18
+ lossy concise line per finding, so the report shrank from ruff's snippets,
19
+ carets and ``help:`` lines down to one line each. Grouping the verbatim blocks
20
+ by the path on their ``-->`` line keeps the single execution *and* the old
21
+ report's content -- see :func:`parse_ruff_output`.
22
+ """
23
+
24
+ import re
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+
28
+ from ...utils.pre_commit import PreCommitRun
29
+ from ...utils.subprocess_utils import SubprocessResult
30
+
31
+ #: ruff's ``full`` renderer locates every diagnostic with one arrow line. The
32
+ #: path is captured greedily so Windows drive letters (``C:\...:1:1``) and
33
+ #: paths containing spaces survive; the trailing ``:row:column`` is what makes
34
+ #: the match unambiguous.
35
+ _LOCATION_LINE = re.compile(r"^\s*--> (.+):(\d+):(\d+)$")
36
+
37
+
38
+ def display_path(path: str, project_root: Path) -> str:
39
+ """*path* relative to *project_root* when it lies under it, else unchanged.
40
+
41
+ Both sides are resolved first: mypy reports the real path, while the root
42
+ can arrive through a symlink (``/var`` vs ``/private/var`` on macOS), and
43
+ an unresolved comparison would leave every finding looking "outside" the
44
+ scope it belongs to.
45
+ """
46
+ candidate = Path(path)
47
+ if not candidate.is_absolute():
48
+ return path
49
+ try:
50
+ return str(candidate.resolve().relative_to(project_root.resolve()))
51
+ except (OSError, ValueError):
52
+ return path
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class RuffBlock:
57
+ """One diagnostic block, verbatim from ``ruff check --output-format full``.
58
+
59
+ ``text`` keeps ruff's own header, ``-->`` location, source snippet, caret
60
+ and ``help:`` lines exactly as emitted. ``path``/``row``/``column`` are
61
+ lifted from the ``-->`` line so the reporter can group the block into its
62
+ per-directory section without re-rendering anything.
63
+ """
64
+
65
+ text: str
66
+ path: str
67
+ row: int
68
+ column: int
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class RuffOutput:
73
+ """One ruff run's full text, split into diagnostic blocks plus the rest.
74
+
75
+ ``tail`` holds everything that is not a diagnostic block -- ruff's own
76
+ summary (``Found N errors.`` and the ``[*] N fixable ...`` line, including
77
+ the ``--unsafe-fixes`` hint) and ``All checks passed!``. Keeping it
78
+ verbatim is what makes the fixable count and the ``[*]`` markers ruff's
79
+ own facts instead of a second, hand-rolled tally (#170 review F2).
80
+ """
81
+
82
+ blocks: tuple[RuffBlock, ...]
83
+ tail: tuple[str, ...]
84
+
85
+ @property
86
+ def empty(self) -> bool:
87
+ """Whether the run produced no text at all (launch failure)."""
88
+ return not self.blocks and not self.tail
89
+
90
+
91
+ def parse_ruff_output(stdout: str) -> RuffOutput:
92
+ """Split ``ruff check --output-format full`` text into blocks and tail.
93
+
94
+ ruff separates its rendered diagnostics with a blank line, and every
95
+ snippet line carries a gutter (`` |`` or ``12 | ...``), so a truly empty
96
+ line is always a block boundary. A block is a *diagnostic* when it carries
97
+ an ``--> path:row:column`` line; anything else (the summary, or a ruff
98
+ version that only printed a final sentence) is kept in the tail.
99
+
100
+ The function is pure and never re-renders: block text is handed back
101
+ byte-for-byte, which is what lets the differential test in
102
+ ``tests/test_lint_report_equivalence.py`` compare against ruff itself.
103
+ """
104
+ text = (stdout or "").replace("\r\n", "\n").replace("\r", "\n")
105
+ groups: list[list[str]] = []
106
+ current: list[str] = []
107
+ for line in text.split("\n"):
108
+ if not line.strip():
109
+ if current:
110
+ groups.append(current)
111
+ current = []
112
+ continue
113
+ current.append(line)
114
+ if current:
115
+ groups.append(current)
116
+
117
+ blocks: list[RuffBlock] = []
118
+ tail: list[str] = []
119
+ for group in groups:
120
+ location: re.Match[str] | None = None
121
+ for line in group:
122
+ location = _LOCATION_LINE.match(line)
123
+ if location is not None:
124
+ break
125
+ if location is None:
126
+ tail.extend(group)
127
+ continue
128
+ blocks.append(
129
+ RuffBlock(
130
+ text="\n".join(group),
131
+ path=location.group(1),
132
+ row=int(location.group(2)),
133
+ column=int(location.group(3)),
134
+ )
135
+ )
136
+ return RuffOutput(blocks=tuple(blocks), tail=tuple(tail))
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class LintRun:
141
+ """The finished tools of one ``wl lint`` execution.
142
+
143
+ ``project_root``, ``quiet`` and ``fix`` are the inputs the render needs;
144
+ ``paths`` is the scope as the user asked for it (``()`` means the default,
145
+ repository-wide scope). ``format_code`` is the #166 format prefix's verdict:
146
+ the caller stops before ruff when it is non-zero, so in practice it is
147
+ ``0`` here, but keeping it in the value makes :attr:`exit_code` the single
148
+ place the whole lint exit contract is spelled (#170 audit patch 5).
149
+
150
+ Field order note: ``project_root`` has no default, so it sits beside
151
+ ``ruff`` ahead of the optional results rather than after ``paths`` where the
152
+ ticket sketched it.
153
+ """
154
+
155
+ ruff: SubprocessResult
156
+ project_root: Path
157
+ mypy: SubprocessResult | None = None
158
+ pre_commit: PreCommitRun | None = None
159
+ paths: tuple[str, ...] = ()
160
+ quiet: bool = False
161
+ fix: bool = False
162
+ format_code: int = 0
163
+
164
+ @property
165
+ def ruff_launch_failed(self) -> bool:
166
+ """Whether ruff itself could not be started.
167
+
168
+ Asked of ``launch_error``, never of ``returncode < 0``: a child killed
169
+ by a signal also reports a negative code (#100).
170
+ """
171
+ return self.ruff.launch_error is not None
172
+
173
+ @property
174
+ def exit_code(self) -> int:
175
+ """The lint verdict: prefix, ruff, mypy, or a failing pre-commit hook.
176
+
177
+ A run that never started exits ``1`` rather than propagating the
178
+ executor's ``-1`` sentinel to ``sys.exit`` (where ``-1`` becomes 255).
179
+ A run that *was* killed by a signal is a failure too: ``subprocess``
180
+ reports it as ``-signum``, and ``max(format_code, -9)`` used to hand
181
+ back ``0`` -- a ruff killed by OOM/SIGKILL passed the gate (#170 review
182
+ F3). Both non-verdict states collapse to ``1``.
183
+
184
+ mypy follows the same rule whenever :attr:`mypy_failure` names a run
185
+ that produced no verdict -- it could not be started, was killed by a
186
+ signal, exited with code ``>= 2``, or exited non-zero with no
187
+ diagnostics at all -- and only on the paths that ran it (``--report``
188
+ and the default; ``--noreport`` never has a mypy result). An ordinary
189
+ ``rc == 1`` mypy exit *with* diagnostics is a type-error verdict and
190
+ deliberately leaves the exit code alone (#172 wave 9 ruling).
191
+
192
+ A hook that *judged the check failed* is part of the verdict too
193
+ (#188): the report already rendered it as ``Failed`` while the exit
194
+ code said ``0``, so the terminal announced success for a red report.
195
+ Auto-fix-only runs stay green -- :attr:`PreCommitRun.hooks_failed`
196
+ already excludes blocks that merely rewrote files (#149).
197
+ """
198
+ ruff_code = self.ruff.returncode
199
+ if self.ruff_launch_failed or self.ruff.was_signalled:
200
+ ruff_code = 1
201
+ code = max(self.format_code, ruff_code)
202
+ # One predicate for both the report and the exit code (#172 review
203
+ # M1): a mypy that never produced a verdict must not let the terminal
204
+ # announce success while the report says "mypy failed". Ordinary
205
+ # type errors answer ``None`` here and stay a verdict.
206
+ if self.mypy_failure is not None:
207
+ code = max(code, 1)
208
+ if self.pre_commit is not None and self.pre_commit.hooks_failed:
209
+ code = max(code, 1)
210
+ return code
211
+
212
+ @property
213
+ def issues_found(self) -> bool:
214
+ """Whether ruff judged the code to have issues.
215
+
216
+ Only ruff's status is read. Its stdout is *not* consulted -- the old
217
+ reporter decided this by ``stdout.strip()``, so a launch failure (empty
218
+ stdout) read as "no issues" (#170).
219
+ """
220
+ return self.ruff_launch_failed or self.ruff.returncode != 0
221
+
222
+ @property
223
+ def ruff_output(self) -> RuffOutput:
224
+ """The captured ``full`` text of the one ruff run, as blocks + tail."""
225
+ return parse_ruff_output(self.ruff.stdout)
226
+
227
+ @property
228
+ def mypy_failure(self) -> str | None:
229
+ """A one-line description when mypy could not produce a verdict.
230
+
231
+ The exit code, not the presence of output, is the discriminator:
232
+ ``0`` is clean, ``1`` means "type errors found" -- a verdict the gate
233
+ leaves alone -- and ``>= 2`` is abnormal termination (bad config, bad
234
+ command, internal error). So ``returncode >= 2`` is a failure whatever
235
+ mypy managed to print first; a broken mypy that emitted partial
236
+ diagnostics before dying must not pass for a type-error verdict
237
+ (#172 r1). ``rc == 1`` is a failure only when stdout is empty, which
238
+ is what a config file that cannot be found looks like:
239
+ ``mypy: error: Cannot find config file ...`` on stderr, empty stdout.
240
+ A child killed by a signal is a failure regardless of what it managed
241
+ to print first: ``was_signalled`` is asked separately because a
242
+ ``-signum`` exit with partial diagnostics would otherwise pass for a
243
+ normal type-error verdict (#172 wave 9). Naming these keeps the report
244
+ from answering "No issues found." to a run that never started (#170
245
+ review F4).
246
+ """
247
+ result = self.mypy
248
+ if result is None:
249
+ return None
250
+ if result.launch_error is not None:
251
+ return f"mypy could not be started: {result.launch_error}"
252
+ if result.was_signalled:
253
+ return f"mypy was killed by signal {-result.returncode}."
254
+ if result.returncode >= 2:
255
+ return f"mypy failed with exit code {result.returncode}."
256
+ if result.returncode != 0 and not (result.stdout or "").strip():
257
+ return f"mypy failed with exit code {result.returncode}."
258
+ return None
@@ -0,0 +1,90 @@
1
+ """Single 'which mypy config file applies?' decision (map #35 / ticket #42).
2
+
3
+ Consolidates the three duplicated ``.wl/mypy.ini`` existence checks that used
4
+ to live in LintExecutor and LintReporter into one module, so moving the mypy
5
+ baseline (industrialization IS-2 keeps it in ``.wl/mypy.ini``) is a one-place
6
+ change.
7
+
8
+ When ``.wl/mypy.ini`` is absent (a fresh clone that has not run ``wl init``, or
9
+ a CI job that has not materialised it yet), the decision falls back to the
10
+ vendored ``vendors/config/mypy.ini`` template shipped inside the package, so
11
+ callers always get a usable ``--config-file``.
12
+ """
13
+
14
+ import os
15
+ from pathlib import Path
16
+
17
+ from ...utils.root import Root, RootKind
18
+
19
+ # Vendored mypy baseline shipped with the package and deployed to
20
+ # ``.wl/mypy.ini`` by ``wl init``. ``parents[2]`` of this module is the
21
+ # ``wl_cli`` package directory.
22
+ _VENDORED_MYPY_INI = (
23
+ Path(__file__).resolve().parents[2] / "vendors" / "config" / "mypy.ini"
24
+ )
25
+
26
+
27
+ def mypy_config_file(project_root: Path, *, relative: bool = False) -> Path | None:
28
+ """Return the mypy config to pass to ``--config-file``, or ``None``.
29
+
30
+ Prefers the project baseline ``.wl/mypy.ini``; when that is missing, falls
31
+ back to the vendored template shipped with the package. ``None`` is
32
+ returned only when neither is available.
33
+
34
+ Both the **selection** and the returned *form* are anchored at
35
+ *project_root* -- the directory mypy will run from. The default yields an
36
+ absolute path, which is what both callers use: ``LintExecutor`` (mypy runs
37
+ with *project_root* as cwd, but an absolute config cannot be broken by a
38
+ symlinked root spelling, #170 review F4) and ``wl gate`` when it names the
39
+ config in its warning. ``relative=True`` still yields a token relative to
40
+ *project_root*; both sides are resolved first, so a symlinked root cannot
41
+ turn the vendored fallback into ``../private/tmp/...``.
42
+
43
+ The selection must be anchored here, not at the process cwd: callers build
44
+ the command *before* running it (``LintExecutor.prepare_mypy_command`` can
45
+ be invoked from any directory, and ``wl gate`` builds every step up front),
46
+ so probing a bare ``Path(".wl/mypy.ini")`` would read the *caller's* cwd --
47
+ picking a different baseline whenever the two disagree, and falling back to
48
+ the vendored template when the project baseline is merely out of view (the
49
+ review F3 residual).
50
+ """
51
+ # The ``.wl/mypy.ini`` layout lives in the root module; ask it rather than
52
+ # spelling the path here (#114). The derived path is a pure builder from
53
+ # *project_root*, so the kind is only a label, not a resolution.
54
+ candidate = Root(project_root, RootKind.PROJECT).mypy_ini()
55
+ try:
56
+ baseline_exists = candidate.exists()
57
+ except OSError:
58
+ # Same contract as the vendored probe below: a path that cannot be
59
+ # stat'ed means "not this baseline", never a crash.
60
+ baseline_exists = False
61
+ if baseline_exists:
62
+ if relative:
63
+ return _relative_to(candidate, project_root)
64
+ return candidate
65
+
66
+ try:
67
+ vendored_exists = _VENDORED_MYPY_INI.exists()
68
+ except OSError:
69
+ return None
70
+ if not vendored_exists:
71
+ return None
72
+
73
+ if relative:
74
+ return _relative_to(_VENDORED_MYPY_INI, project_root)
75
+ return _VENDORED_MYPY_INI
76
+
77
+
78
+ def _relative_to(path: Path, project_root: Path) -> Path:
79
+ """*path* expressed relative to *project_root*, with both sides resolved.
80
+
81
+ The vendored template is already resolved (``Path(__file__).resolve()``),
82
+ while *project_root* can still be a symlink spelling -- ``/tmp`` on macOS
83
+ resolves to ``/private/tmp``. A lexical ``os.path.relpath`` between the two
84
+ spellings produced a token like ``../../private/tmp/...`` that mypy could
85
+ not open; resolving both first keeps the token honest (#170 review F4).
86
+ """
87
+ try:
88
+ return Path(os.path.relpath(path.resolve(), start=project_root.resolve()))
89
+ except OSError:
90
+ return Path(os.path.relpath(path, start=project_root))
@@ -0,0 +1,103 @@
1
+ """Command registration and management module.
2
+
3
+ Single source of truth for the user-visible ``wl`` command surface is the
4
+ :data:`COMMAND_TABLE` defined below. Each entry maps a canonical (user-visible
5
+ Click) command name to a :class:`CommandSpec` describing where the Click
6
+ command object lives and how to obtain it.
7
+ """
8
+
9
+ import importlib
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CommandSpec:
15
+ """Static spec of one user-visible Click command on the ``wl`` group.
16
+
17
+ Attributes:
18
+ module_path: Import path of the command module, relative to
19
+ :attr:`package` (e.g. ``"buildcommands.build"``).
20
+ symbol: Name of the module-level Click command object, or of a
21
+ zero-argument factory returning one.
22
+ aliases: Extra user-visible names registered alongside the canonical
23
+ name.
24
+ instantiate: If True, call the symbol to obtain the Click object
25
+ (``obj = obj()``); otherwise the module attribute is used as-is.
26
+ package: Import root :attr:`module_path` is resolved against. Legacy
27
+ commands live under ``wl_cli.commands``; domain namespaces (env /
28
+ cc / mcp, issue #220) live directly under ``wl_cli``.
29
+ """
30
+
31
+ module_path: str
32
+ symbol: str
33
+ aliases: tuple[str, ...] = ()
34
+ instantiate: bool = False
35
+ package: str = "wl_cli.commands"
36
+
37
+
38
+ #: Canonical user-visible command name -> its spec.
39
+ COMMAND_TABLE: dict[str, CommandSpec] = {
40
+ "lint": CommandSpec("lint.lint_command", "lint_command"),
41
+ "clean": CommandSpec("clean.clean", "clean_command", aliases=("c",)),
42
+ "cleanup": CommandSpec("cleanup", "cleanup_command"),
43
+ "format": CommandSpec("format.format_command", "format_command", aliases=("f",)),
44
+ "config": CommandSpec("config.config", "config_command"),
45
+ "test": CommandSpec("testcommands.test_commands", "test_command"),
46
+ "build": CommandSpec("buildcommands.build", "build_command", aliases=("b",)),
47
+ "builddist": CommandSpec("buildcommands.dist", "builddist_command"),
48
+ "buildtest": CommandSpec("buildcommands.test", "buildtest_command"),
49
+ "init": CommandSpec("initenv.initenv", "initenv_group", aliases=("i",)),
50
+ "self": CommandSpec("self.self_command", "self_command", instantiate=True),
51
+ "update": CommandSpec("update.update_command", "update_command"),
52
+ "workflow": CommandSpec("workflow", "workflow_command"),
53
+ "version": CommandSpec(
54
+ "version.version_command", "version_command", instantiate=True
55
+ ),
56
+ "debug": CommandSpec("debug.debug", "debug_command"),
57
+ "git-flow": CommandSpec("git.git_flow", "git_flow_command"),
58
+ "lfs": CommandSpec("lfs.cli", "lfs_command"),
59
+ "gate": CommandSpec("gate.gate_command", "gate_command"),
60
+ # Domain namespaces (issue #220). Their Click groups live at
61
+ # ``src/wl_cli/<domain>/cli.py``, outside ``wl_cli.commands``, hence the
62
+ # explicit package; imports stay lazy — ``wl --help`` imports the module
63
+ # only when the group is registered, never at registry import time.
64
+ "env": CommandSpec("env.cli", "env_group", package="wl_cli"),
65
+ "cc": CommandSpec("cc.cli", "cc_group", package="wl_cli"),
66
+ "mcp": CommandSpec("mcp.cli", "mcp_group", package="wl_cli"),
67
+ }
68
+
69
+ #: Live alias -> canonical command name, derived from the table.
70
+ _ALIAS_TO_COMMAND: dict[str, str] = {
71
+ alias: name for name, spec in COMMAND_TABLE.items() for alias in spec.aliases
72
+ }
73
+
74
+
75
+ def list_commands() -> list[str]:
76
+ """Return the canonical user-visible command names in registration order."""
77
+ return list(COMMAND_TABLE)
78
+
79
+
80
+ def get_command(name: str):
81
+ """Return the Click command/group object for a canonical name or alias.
82
+
83
+ The owning module is imported on first access. Aliases resolve to their
84
+ canonical command before lookup.
85
+
86
+ Args:
87
+ name: A canonical command name or one of its aliases.
88
+
89
+ Returns:
90
+ The Click command/group object.
91
+
92
+ Raises:
93
+ ValueError: If ``name`` is not a known command or alias.
94
+ """
95
+ resolved = _ALIAS_TO_COMMAND.get(name, name)
96
+ spec = COMMAND_TABLE.get(resolved)
97
+ if spec is None:
98
+ raise ValueError(f"Command '{name}' not found")
99
+ module = importlib.import_module(f"{spec.package}.{spec.module_path}")
100
+ obj = getattr(module, spec.symbol)
101
+ if spec.instantiate:
102
+ obj = obj()
103
+ return obj
@@ -0,0 +1,7 @@
1
+ """
2
+ Self commands module.
3
+ """
4
+
5
+ from .self_command import SelfCommand
6
+
7
+ __all__ = ["SelfCommand"]
@@ -0,0 +1,8 @@
1
+ """
2
+ Common shared components for self module.
3
+ """
4
+
5
+ from .config import SelfConfig
6
+ from .version_cache import VersionCache
7
+
8
+ __all__ = ["SelfConfig", "VersionCache"]
@@ -0,0 +1,30 @@
1
+ """Backwards-compatible re-export of the shared capability registry (#221).
2
+
3
+ The single source of truth now lives in :mod:`wl_cli.capabilities`: both
4
+ ``wl env check`` (``wl_cli.env``) and this package's deprecated
5
+ ``wl self capabilities`` alias (``wl_cli.commands``) must serve the same
6
+ dotted names, and the quality domain may not import the env domain (#220).
7
+ This module keeps the historical import path working; it defines no data.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ....capabilities import (
13
+ CAPABILITIES,
14
+ CAPABILITY_SINCE,
15
+ SCHEMA_VERSION,
16
+ capabilities_payload,
17
+ capability_details,
18
+ capability_names,
19
+ capability_since,
20
+ )
21
+
22
+ __all__ = [
23
+ "CAPABILITIES",
24
+ "CAPABILITY_SINCE",
25
+ "SCHEMA_VERSION",
26
+ "capabilities_payload",
27
+ "capability_details",
28
+ "capability_names",
29
+ "capability_since",
30
+ ]
@@ -0,0 +1,27 @@
1
+ """
2
+ Configuration for self commands.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from wl_cli.utils.config import get_config
8
+
9
+
10
+ @dataclass
11
+ class SelfConfig:
12
+ """Configuration container for self commands."""
13
+
14
+ cache_enabled: bool
15
+ cache_timeout: int
16
+ parallel_enabled: bool
17
+ max_workers: int
18
+
19
+ @classmethod
20
+ def load(cls) -> "SelfConfig":
21
+ """Load configuration from config store."""
22
+ return cls(
23
+ cache_enabled=get_config("cache_enabled", True),
24
+ cache_timeout=get_config("cache_timeout", 300),
25
+ parallel_enabled=get_config("parallel_enabled", True),
26
+ max_workers=get_config("max_workers", 4),
27
+ )
@@ -0,0 +1,85 @@
1
+ """
2
+ Generic version cache with TTL support.
3
+ """
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+
9
+ class VersionCache:
10
+ """Generic cache for version information with time-based expiration."""
11
+
12
+ def __init__(self, default_timeout: int = 300) -> None:
13
+ """
14
+ Initialize the version cache.
15
+
16
+ Args:
17
+ default_timeout: Default cache timeout in seconds (default: 5 minutes)
18
+ """
19
+ self._cache: dict[str, Any] = {}
20
+ self._last_update: dict[str, float] = {}
21
+ self._default_timeout = default_timeout
22
+
23
+ def get(self, key: str) -> Any | None:
24
+ """
25
+ Get a cached value if it exists and hasn't expired.
26
+
27
+ Args:
28
+ key: Cache key to retrieve
29
+
30
+ Returns:
31
+ The cached value if valid, None otherwise
32
+ """
33
+ if key not in self._cache:
34
+ return None
35
+
36
+ current_time = time.time()
37
+ if current_time - self._last_update[key] > self._default_timeout:
38
+ return None
39
+
40
+ return self._cache[key]
41
+
42
+ def set(self, key: str, value: Any) -> None:
43
+ """
44
+ Store a value in the cache.
45
+
46
+ Args:
47
+ key: Cache key
48
+ value: Value to cache
49
+ """
50
+ self._cache[key] = value
51
+ self._last_update[key] = time.time()
52
+
53
+ def get_or_set(self, key: str, supplier) -> Any:
54
+ """
55
+ Get from cache or supply a new value.
56
+
57
+ Args:
58
+ key: Cache key
59
+ supplier: Function to call if value not cached or expired
60
+
61
+ Returns:
62
+ The cached or newly supplied value
63
+ """
64
+ value = self.get(key)
65
+ if value is None:
66
+ value = supplier()
67
+ if value is not None:
68
+ self.set(key, value)
69
+ return value
70
+
71
+ def invalidate(self, key: str) -> None:
72
+ """
73
+ Invalidate a specific cache entry.
74
+
75
+ Args:
76
+ key: Cache key to invalidate
77
+ """
78
+ if key in self._cache:
79
+ del self._cache[key]
80
+ del self._last_update[key]
81
+
82
+ def clear(self) -> None:
83
+ """Clear all cache entries."""
84
+ self._cache.clear()
85
+ self._last_update.clear()
@@ -0,0 +1,19 @@
1
+ """
2
+ Subcommand handlers for self command.
3
+ """
4
+
5
+ from .base_handler import BaseHandler
6
+ from .install_handler import InstallHandler
7
+ from .status_handler import StatusHandler
8
+ from .uninstall_handler import UninstallHandler
9
+ from .update_handler import UpdateHandler
10
+ from .version_handler import VersionHandler
11
+
12
+ __all__ = [
13
+ "BaseHandler",
14
+ "VersionHandler",
15
+ "UpdateHandler",
16
+ "InstallHandler",
17
+ "UninstallHandler",
18
+ "StatusHandler",
19
+ ]
@@ -0,0 +1,28 @@
1
+ """
2
+ Base class for self command handlers.
3
+ """
4
+
5
+ import sys
6
+ from abc import ABC, abstractmethod
7
+
8
+ from wl_cli.utils.logging import log_error_pair
9
+
10
+
11
+ class BaseHandler(ABC):
12
+ """Abstract base class for self command subcommand handlers."""
13
+
14
+ @abstractmethod
15
+ def execute(self) -> None:
16
+ """Execute the subcommand."""
17
+ pass
18
+
19
+ def handle_error(self, error: Exception, exit_code: int = 1) -> None:
20
+ """
21
+ Handle an error with logging and exit.
22
+
23
+ Args:
24
+ error: The exception to handle
25
+ exit_code: Exit code to use (default: 1)
26
+ """
27
+ log_error_pair(f"Unexpected error: {error}", f"意外错误:{error}")
28
+ sys.exit(exit_code)