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,531 @@
1
+ """LFS Lock workflow initialization (moved from initenv/services)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ...utils.platform_adapter import PlatformAdapter
11
+ from ...utils.root import Root, RootKind
12
+ from ...utils.root_fallback import current_root
13
+
14
+
15
+ def lfs_lock_init_failure_message(project_root: Path | str) -> str:
16
+ """Return the bilingual error text for a missing ``.wl`` directory.
17
+
18
+ ``LfsLockInitializer.initialize()`` reports a project that was never
19
+ initialized by returning False; every caller must surface that as a failed
20
+ command instead of printing success (#194).
21
+ """
22
+ return (
23
+ "LFS lock workflow was not initialized: no .wl directory at "
24
+ f"{project_root}. Run 'wl init env' first.\n"
25
+ f"LFS 锁工作流未初始化:{project_root} 下没有 .wl 目录,请先运行 "
26
+ "'wl init env'。"
27
+ )
28
+
29
+
30
+ class LfsLockInitializer:
31
+ """Coordinates the deployment of LFS lock workflow configuration."""
32
+
33
+ def __init__(self, project_root: Path | None = None) -> None:
34
+ # Root-anchored (#125): the *project* root the CWD belongs to, not the
35
+ # CWD itself, so ``wl init lfs`` from a subdirectory writes the lock
36
+ # config into the root's ``.wl`` (see ``initialize``) instead of
37
+ # silently no-op'ing because the subdirectory has no ``.wl``.
38
+ self.project_root = project_root or current_root(RootKind.PROJECT).path
39
+ self._wl_python_cache: str | None = None
40
+
41
+ def _wl_dir(self) -> Path:
42
+ """Return the root-anchored ``.wl`` directory of this project (#125)."""
43
+ return Root(self.project_root, RootKind.PROJECT).wl_dir()
44
+
45
+ def wl_dir_ready(self) -> bool:
46
+ """Return True when the project has a ``.wl`` directory.
47
+
48
+ Side-effect free, so callers can probe the precondition *before*
49
+ anything that mutates the repository runs (installing hooks,
50
+ rewriting config, deploying ``work.bat``). ``initialize()`` re-checks
51
+ because the directory could disappear between the probe and the
52
+ write (#198).
53
+ """
54
+ return self._wl_dir().is_dir()
55
+
56
+ def _run_uv_install(self, python_path: str, package_spec: str) -> bool:
57
+ """Try to install a package using uv pip install --python."""
58
+ uv_cmd = shutil.which("uv") or "uv"
59
+ try:
60
+ result = subprocess.run(
61
+ [uv_cmd, "pip", "install", "--python", python_path, package_spec],
62
+ capture_output=True,
63
+ text=True,
64
+ timeout=120,
65
+ )
66
+ return result.returncode == 0
67
+ except (
68
+ subprocess.CalledProcessError,
69
+ FileNotFoundError,
70
+ subprocess.TimeoutExpired,
71
+ ):
72
+ return False
73
+
74
+ def _run_pip_install(self, python_path: str, package_spec: str) -> bool:
75
+ """Try to install a package using python -m pip install."""
76
+ try:
77
+ result = subprocess.run(
78
+ [python_path, "-m", "pip", "install", package_spec],
79
+ capture_output=True,
80
+ text=True,
81
+ timeout=120,
82
+ )
83
+ return result.returncode == 0
84
+ except (
85
+ subprocess.CalledProcessError,
86
+ FileNotFoundError,
87
+ subprocess.TimeoutExpired,
88
+ ):
89
+ return False
90
+
91
+ def _check_watchdog_installed(self) -> bool:
92
+ """检查 watchdog 是否已安装。
93
+
94
+ Returns:
95
+ True if watchdog is installed, False otherwise.
96
+ """
97
+ try:
98
+ import watchdog # noqa: F401 # availability probe (optional LFS extra)
99
+
100
+ return True
101
+ except ImportError:
102
+ return False
103
+
104
+ def _check_watchdog_in_wl_python(self) -> bool:
105
+ """检查 wl.exe 的 Python 环境中是否安装了 watchdog。
106
+
107
+ Returns:
108
+ True if watchdog is installed in wl's Python, False otherwise.
109
+ """
110
+ wl_python = self._find_wl_python_path()
111
+ if not wl_python:
112
+ return False
113
+
114
+ try:
115
+ result = subprocess.run(
116
+ [wl_python, "-c", "import watchdog"],
117
+ capture_output=True,
118
+ text=True,
119
+ timeout=30,
120
+ )
121
+ return result.returncode == 0
122
+ except (
123
+ subprocess.CalledProcessError,
124
+ FileNotFoundError,
125
+ subprocess.TimeoutExpired,
126
+ ):
127
+ return False
128
+
129
+ def _get_python_executable(self) -> str:
130
+ """获取可用的 Python 可执行文件路径。
131
+
132
+ 优先使用系统 Python,避免使用 wl.exe 绑定的 uv 环境。
133
+ 总是返回一个路径(最后回退到 sys.executable)。
134
+ """
135
+ # 尝试常见的 Python 命令
136
+ python_commands = ["python", "python3", "py"]
137
+
138
+ for cmd in python_commands:
139
+ python_path = shutil.which(cmd)
140
+ if python_path:
141
+ return python_path
142
+
143
+ # 如果都找不到,返回 sys.executable 作为最后手段
144
+ return sys.executable
145
+
146
+ def _find_wl_python_path(self) -> str | None:
147
+ """查找 wl.exe 使用的 Python 解释器路径。
148
+
149
+ Returns:
150
+ Python 解释器路径,如果找不到则返回 None。
151
+ """
152
+ if self._wl_python_cache is not None:
153
+ return self._wl_python_cache
154
+
155
+ # 方法1:从 uv tools 目录查找
156
+ result = self._find_wl_python_from_uv_tools()
157
+ if result:
158
+ self._wl_python_cache = result
159
+ return result
160
+
161
+ # 方法2:从 wl.exe 位置查找
162
+ result = self._find_wl_python_from_wl_exe()
163
+ self._wl_python_cache = result
164
+ return result
165
+
166
+ def _find_wl_python_from_uv_tools(self) -> str | None:
167
+ """从 uv tools 目录查找 Python 解释器。"""
168
+ if PlatformAdapter.is_windows():
169
+ uv_tools_base = Path.home() / "AppData" / "Roaming" / "uv" / "tools"
170
+ else:
171
+ uv_tools_base = Path.home() / ".local" / "share" / "uv" / "tools"
172
+
173
+ if uv_tools_base.exists():
174
+ wl_cli_dir = uv_tools_base / "py-wlcli"
175
+ if wl_cli_dir.exists():
176
+ scripts_dir = wl_cli_dir / "Scripts"
177
+
178
+ # 尝试在 Scripts 目录中查找
179
+ python_in_scripts = scripts_dir / "python.exe"
180
+ if python_in_scripts.exists():
181
+ return str(python_in_scripts)
182
+
183
+ # 直接在 wl-cli 目录中查找
184
+ python_in_tools = wl_cli_dir / "python.exe"
185
+ if python_in_tools.exists():
186
+ return str(python_in_tools)
187
+
188
+ # 查找 python3.exe
189
+ python3_in_scripts = scripts_dir / "python3.exe"
190
+ if python3_in_scripts.exists():
191
+ return str(python3_in_scripts)
192
+
193
+ return None
194
+
195
+ def _find_wl_python_from_wl_exe(self) -> str | None:
196
+ """从 wl.exe 位置查找 Python 解释器。"""
197
+ for cmd_name in ["wl", "wl.exe"]:
198
+ try:
199
+ result = subprocess.run(
200
+ ["where" if PlatformAdapter.is_windows() else "which", cmd_name],
201
+ capture_output=True,
202
+ text=True,
203
+ shell=False,
204
+ )
205
+ if result.returncode == 0 and result.stdout.strip():
206
+ wl_path = Path(result.stdout.strip().split("\n")[0].strip())
207
+ if wl_path.exists():
208
+ return self._search_python_around_wl_path(wl_path)
209
+ except (subprocess.CalledProcessError, FileNotFoundError):
210
+ continue
211
+
212
+ return None
213
+
214
+ def _search_python_around_wl_path(self, wl_path: Path) -> str | None:
215
+ """在 wl.exe 路径周围搜索 Python 解释器。"""
216
+ # wl.exe 在 Scripts 目录中
217
+ scripts_dir = wl_path.parent
218
+ python_in_scripts = scripts_dir / "python.exe"
219
+ if python_in_scripts.exists():
220
+ return str(python_in_scripts)
221
+
222
+ # 向上查找
223
+ tools_dir = scripts_dir.parent
224
+ python_in_tools = tools_dir / "python.exe"
225
+ if python_in_tools.exists():
226
+ return str(python_in_tools)
227
+
228
+ # 在父目录中查找 Scripts
229
+ return self._search_parent_dirs_for_python(tools_dir)
230
+
231
+ def _search_parent_dirs_for_python(self, start_dir: Path) -> str | None:
232
+ """在父目录中搜索 Python 解释器。"""
233
+ for parent in start_dir.parents:
234
+ python_candidates = [
235
+ parent / "python.exe",
236
+ parent / "Scripts" / "python.exe",
237
+ parent / "bin" / "python",
238
+ parent / "bin" / "python.exe",
239
+ ]
240
+ for python_candidate in python_candidates:
241
+ if python_candidate.exists():
242
+ return str(python_candidate)
243
+ return None
244
+
245
+ def _find_wl_python_version(self) -> str | None:
246
+ """查找 wl.exe 的 Python 版本。
247
+
248
+ Returns:
249
+ Python 版本字符串,如 "3.11", "3.12" 等,如果找不到则返回 None。
250
+ """
251
+ wl_python = self._find_wl_python_path()
252
+ if not wl_python:
253
+ return None
254
+
255
+ try:
256
+ result = subprocess.run(
257
+ [wl_python, "--version"],
258
+ capture_output=True,
259
+ text=True,
260
+ timeout=10,
261
+ )
262
+ if result.returncode == 0:
263
+ # 输出格式: "Python 3.11.x"
264
+ version_str = result.stdout.strip()
265
+ if "Python" in version_str:
266
+ parts = version_str.split()
267
+ if len(parts) >= 2:
268
+ return parts[1] # 返回 "3.11.x"
269
+ except (
270
+ subprocess.CalledProcessError,
271
+ FileNotFoundError,
272
+ subprocess.TimeoutExpired,
273
+ ):
274
+ pass
275
+
276
+ return None
277
+
278
+ def _install_watchdog(self) -> bool:
279
+ import click
280
+
281
+ click.echo("Installing watchdog for LFS watch functionality...")
282
+ click.echo("正在安装 watchdog 以支持 LFS watch 功能...")
283
+
284
+ wl_python = self._find_wl_python_path()
285
+ if wl_python:
286
+ if self._run_uv_install(wl_python, "watchdog"):
287
+ click.echo(f"✓ watchdog installed via uv for {wl_python}")
288
+ click.echo("✓ watchdog 安装成功(通过 uv)")
289
+ return True
290
+ if self._run_pip_install(wl_python, "watchdog"):
291
+ click.echo(f"✓ watchdog installed via pip for {wl_python}")
292
+ click.echo("✓ watchdog 安装成功(通过 pip)")
293
+ return True
294
+
295
+ system_python = self._get_python_executable()
296
+ if self._run_pip_install(system_python, "watchdog"):
297
+ click.echo("✓ watchdog installed via system pip")
298
+ click.echo("✓ watchdog 安装成功")
299
+ return True
300
+
301
+ click.echo("Failed to install watchdog automatically.", err=True)
302
+ click.echo("无法自动安装 watchdog。", err=True)
303
+ return False
304
+
305
+ def _check_pyyaml_installed(self) -> bool:
306
+ """检查 wl.exe 的 Python 环境中是否安装了 PyYAML。
307
+
308
+ Returns:
309
+ True if PyYAML is installed in wl's Python, False otherwise.
310
+ """
311
+ wl_python = self._find_wl_python_path()
312
+ if not wl_python:
313
+ return False
314
+
315
+ try:
316
+ result = subprocess.run(
317
+ [wl_python, "-c", "import yaml"],
318
+ capture_output=True,
319
+ text=True,
320
+ timeout=30,
321
+ )
322
+ return result.returncode == 0
323
+ except (
324
+ subprocess.CalledProcessError,
325
+ FileNotFoundError,
326
+ subprocess.TimeoutExpired,
327
+ ):
328
+ return False
329
+
330
+ def _install_pyyaml(self) -> bool:
331
+ import click
332
+
333
+ click.echo("Installing PyYAML for LFS lock configuration...")
334
+ click.echo("正在安装 PyYAML 以支持 LFS lock 配置...")
335
+
336
+ wl_python = self._find_wl_python_path()
337
+ if wl_python:
338
+ if self._run_uv_install(wl_python, "pyyaml>=6.0.0"):
339
+ click.echo(f"✓ PyYAML installed via uv for {wl_python}")
340
+ click.echo("✓ PyYAML 安装成功(通过 uv)")
341
+ return True
342
+ if self._run_pip_install(wl_python, "pyyaml>=6.0.0"):
343
+ click.echo(f"✓ PyYAML installed via pip for {wl_python}")
344
+ click.echo("✓ PyYAML 安装成功(通过 pip)")
345
+ return True
346
+
347
+ system_python = self._get_python_executable()
348
+ if self._run_pip_install(system_python, "pyyaml>=6.0.0"):
349
+ click.echo("✓ PyYAML installed via system pip")
350
+ click.echo("✓ PyYAML 安装成功")
351
+ return True
352
+
353
+ click.echo("Failed to install PyYAML automatically.", err=True)
354
+ click.echo("无法自动安装 PyYAML。", err=True)
355
+ return False
356
+
357
+ def _check_and_install_pyyaml(self) -> bool:
358
+ """检查并安装 PyYAML,如果未安装则提示用户。
359
+
360
+ Returns:
361
+ True if PyYAML is installed (either was already or just installed),
362
+ False if PyYAML could not be installed.
363
+ """
364
+ import click
365
+
366
+ # 检查 wl.exe 的 Python 中是否有 PyYAML
367
+ wl_has_pyyaml = self._check_pyyaml_installed()
368
+
369
+ if wl_has_pyyaml:
370
+ click.echo("✓ PyYAML is already installed")
371
+ click.echo("✓ PyYAML 已安装")
372
+ return True
373
+
374
+ click.echo("")
375
+ click.echo("=" * 50)
376
+ click.echo("WARNING: PyYAML is not installed in wl's Python!", err=True)
377
+ click.echo("警告: wl 的 Python 环境中未安装 PyYAML!", err=True)
378
+ click.echo("")
379
+ click.echo("PyYAML is required for loading LFS lock configuration.")
380
+ click.echo("Without PyYAML, only default configuration will be available.")
381
+ click.echo("")
382
+ click.echo("PyYAML 是加载 LFS lock 配置文件的必要依赖。")
383
+ click.echo("没有 PyYAML,只能使用默认配置。")
384
+ click.echo("=" * 50)
385
+
386
+ # 尝试自动安装
387
+ if self._install_pyyaml():
388
+ if self._check_pyyaml_installed():
389
+ return True
390
+
391
+ click.echo("")
392
+ click.echo("Please install PyYAML manually by running:")
393
+ click.echo("请手动安装 PyYAML,运行以下命令:")
394
+ click.echo("")
395
+ click.echo(" uv pip install --python <wl-python-path> pyyaml>=6.0.0")
396
+ click.echo("")
397
+ wl_python = self._find_wl_python_path()
398
+ if wl_python:
399
+ click.echo(
400
+ f' Example: uv pip install --python "{wl_python}" pyyaml>=6.0.0'
401
+ )
402
+ click.echo("")
403
+
404
+ return False
405
+
406
+ def _check_and_install_watchdog(self) -> bool:
407
+ """检查并安装 watchdog,如果未安装则提示用户。
408
+
409
+ Returns:
410
+ True if watchdog is installed (either was already or just installed),
411
+ False if watchdog could not be installed.
412
+ """
413
+ import click
414
+
415
+ # 检查用户系统的 Python 中是否有 watchdog
416
+ system_has_watchdog = self._check_watchdog_installed()
417
+
418
+ # 检查 wl.exe 的 Python 中是否有 watchdog
419
+ wl_has_watchdog = self._check_watchdog_in_wl_python()
420
+
421
+ if system_has_watchdog and wl_has_watchdog:
422
+ click.echo("✓ watchdog is already installed")
423
+ click.echo("✓ watchdog 已安装")
424
+ return True
425
+
426
+ if not wl_has_watchdog:
427
+ click.echo("")
428
+ click.echo("=" * 50)
429
+ click.echo("WARNING: watchdog is not installed in wl's Python!", err=True)
430
+ click.echo("警告: wl 的 Python 环境中未安装 watchdog!", err=True)
431
+ click.echo("")
432
+ click.echo("watchdog is required for 'wl lfs watch' functionality.")
433
+ click.echo("The work.bat script needs watchdog to monitor file changes.")
434
+ click.echo("")
435
+ click.echo("watchdog 是 'wl lfs watch' 功能的必要依赖。")
436
+ click.echo("work.bat 脚本需要 watchdog 来监控文件变化。")
437
+ click.echo("")
438
+
439
+ # 尝试自动安装
440
+ if self._install_watchdog():
441
+ # 再次检查是否安装成功
442
+ if self._check_watchdog_in_wl_python():
443
+ return True
444
+
445
+ click.echo("")
446
+ click.echo("Please install watchdog manually by running:")
447
+ click.echo("请手动安装 watchdog,运行以下命令:")
448
+ click.echo("")
449
+ click.echo(" uv pip install --python <wl-python-path> watchdog")
450
+ click.echo("")
451
+ wl_python = self._find_wl_python_path()
452
+ if wl_python:
453
+ click.echo(f' Example: uv pip install --python "{wl_python}" watchdog')
454
+ click.echo("")
455
+ click.echo("=" * 50)
456
+ return False
457
+
458
+ click.echo("✓ watchdog is already installed")
459
+ click.echo("✓ watchdog 已安装")
460
+ return True
461
+
462
+ def _deploy_work_bat(self) -> None:
463
+ """Copy work.bat template to project root on Windows platforms."""
464
+ if not PlatformAdapter.is_windows():
465
+ return
466
+
467
+ import importlib.resources
468
+
469
+ bat_path = self.project_root / "work.bat"
470
+ try:
471
+ template = importlib.resources.files("wl_cli.vendors.hooks.win").joinpath(
472
+ "work.bat"
473
+ )
474
+ shutil.copy(str(template), str(bat_path))
475
+ import click
476
+
477
+ click.echo(f" work.bat created at {bat_path}")
478
+ click.echo(" Double-click work.bat to start an LFS work session.")
479
+ except Exception as e:
480
+ import click
481
+
482
+ click.echo(f" Warning: could not create work.bat: {e}", err=True)
483
+
484
+ def initialize(self) -> bool:
485
+ """Write .wl/lfs-lock.yml if it does not exist, or upgrade from v1.
486
+
487
+ Returns:
488
+ True when the lock workflow config is in place, False when the
489
+ project has no ``.wl`` directory yet (the project was never
490
+ initialized). Callers must treat False as a failure and tell the
491
+ user to run ``wl init env`` first — never as a silent skip (#194).
492
+ """
493
+ wl_dir = self._wl_dir()
494
+ if not self.wl_dir_ready():
495
+ return False
496
+
497
+ # 检查并安装必要依赖
498
+ self._check_and_install_watchdog()
499
+ self._check_and_install_pyyaml()
500
+
501
+ config_path = wl_dir / "lfs-lock.yml"
502
+ if config_path.exists():
503
+ existing = config_path.read_text()
504
+ if "auto_lock:" in existing:
505
+ # Deploy work.bat even when config already exists
506
+ self._deploy_work_bat()
507
+ return True
508
+ # v1 config detected — upgrade to v2
509
+
510
+ content = (
511
+ "# Git LFS file lock workflow configuration\n"
512
+ "# See: wl lfs --help\n"
513
+ "\n"
514
+ "lock:\n"
515
+ " mode: strict # strict | warn | off\n"
516
+ "\n"
517
+ "auto_lock:\n"
518
+ " fallback: true # pre-push auto-lock when watch not running\n"
519
+ " skip_locked_by_others: true\n"
520
+ "\n"
521
+ "auto_unlock:\n"
522
+ " on_push: true # auto-unlock after push detection\n"
523
+ " retry_count: 3\n"
524
+ " retry_delay: 2\n"
525
+ "\n"
526
+ "checks:\n"
527
+ " pre_push: true\n"
528
+ )
529
+ config_path.write_text(content)
530
+ self._deploy_work_bat()
531
+ return True
@@ -0,0 +1,95 @@
1
+ """LFS lock manager — wraps git lfs lock/unlock/locks and git check-attr."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+
8
+
9
+ class LfsLockManager:
10
+ """Manages git LFS file locks by wrapping native git lfs commands."""
11
+
12
+ def __init__(self, cwd: str | None = None) -> None:
13
+ self.cwd = cwd
14
+
15
+ def _run_git(self, args: list[str], **kwargs) -> subprocess.CompletedProcess:
16
+ return subprocess.run(
17
+ ["git", *args],
18
+ capture_output=True,
19
+ text=True,
20
+ encoding="utf-8",
21
+ cwd=self.cwd,
22
+ **kwargs,
23
+ )
24
+
25
+ def get_lfs_files(self, changed_files: frozenset[str]) -> frozenset[str]:
26
+ """Filter a set of file paths to only those tracked by Git LFS."""
27
+ return frozenset(f for f in changed_files if self.is_file_lfs_tracked(f))
28
+
29
+ def is_file_lfs_tracked(self, file_path: str) -> bool:
30
+ """Check if a file matches an LFS filter in .gitattributes."""
31
+ result = self._run_git(["check-attr", "filter", "--", file_path])
32
+ if result.returncode != 0:
33
+ return False
34
+ return "filter: lfs" in result.stdout
35
+
36
+ def lock(self, file_path: str) -> tuple[bool, str]:
37
+ """Lock an LFS file.
38
+
39
+ Returns:
40
+ tuple of (success: bool, error_reason: str).
41
+ error_reason is empty string on success.
42
+ """
43
+ result = self._run_git(["lfs", "lock", file_path])
44
+ if result.returncode == 0:
45
+ return True, ""
46
+
47
+ # 解析错误原因
48
+ stderr = (result.stderr or "").lower()
49
+ if "already locked" in stderr:
50
+ # 提取锁定者信息
51
+ if "by" in stderr:
52
+ locked_by = stderr.split("by")[-1].strip()
53
+ if locked_by:
54
+ return False, f"already locked by {locked_by}"
55
+ return False, "already locked"
56
+ elif "not found" in stderr or "does not exist" in stderr:
57
+ return False, "file not found"
58
+ elif "not tracked" in stderr or "not an lfs object" in stderr:
59
+ return False, "not tracked by LFS"
60
+ elif "authentication" in stderr or "login" in stderr:
61
+ return False, "authentication required"
62
+ elif "network" in stderr or "connection" in stderr or "timeout" in stderr:
63
+ return False, "network error"
64
+ else:
65
+ return False, "unknown error"
66
+
67
+ def unlock(self, file_path: str, force: bool = False) -> bool:
68
+ """Unlock an LFS file. Use force=True for admin force-unlock."""
69
+ cmd = ["lfs", "unlock"]
70
+ if force:
71
+ cmd.append("--force")
72
+ cmd.append(file_path)
73
+ result = self._run_git(cmd)
74
+ return result.returncode == 0
75
+
76
+ def get_locks(self, timeout: int | None = None) -> list[dict]:
77
+ """Return all current LFS locks as a list of dicts."""
78
+ result = self._run_git(["lfs", "locks", "--json"], timeout=timeout)
79
+ if result.returncode != 0 or not result.stdout.strip():
80
+ return []
81
+ return [json.loads(line) for line in result.stdout.splitlines() if line]
82
+
83
+ def lock_files(self, files: list[str]) -> dict[str, tuple[bool, str]]:
84
+ """Lock multiple LFS files. Returns {file_path: (success, reason)} for each."""
85
+ results: dict[str, tuple[bool, str]] = {}
86
+ for file_path in files:
87
+ results[file_path] = self.lock(file_path)
88
+ return results
89
+
90
+ def unlock_files(self, files: list[str]) -> dict[str, bool]:
91
+ """Unlock multiple LFS files. Returns {file_path: success} for each."""
92
+ results: dict[str, bool] = {}
93
+ for file_path in files:
94
+ results[file_path] = self.unlock(file_path)
95
+ return results