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,207 @@
1
+ """
2
+ Backup Manager - manages backup and rollback of .wl directory.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import shutil
8
+ import time
9
+ import uuid
10
+ from typing import TYPE_CHECKING
11
+
12
+ from wl_cli.utils.logging import log_info_pair
13
+
14
+ from ....utils.root import RootKind
15
+ from ....utils.root_fallback import current_root
16
+
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+
20
+
21
+ class BackupManager:
22
+ """Manage backup and rollback of .wl directory."""
23
+
24
+ MAX_BACKUPS = 5
25
+
26
+ def __init__(self, wl_dir: Path | None = None):
27
+ """Initialize BackupManager.
28
+
29
+ Args:
30
+ wl_dir: Path to .wl directory. Defaults to './.wl'.
31
+ """
32
+ # Root-anchored (#125): the deploy root's ``.wl``, not the process
33
+ # CWD's, so ``wl update rollback`` from a subdirectory finds the root's
34
+ # backups instead of none. The scratch snapshots below
35
+ # (``temp_backup / ".wl"``) are deliberately *not* root-derived: they are
36
+ # a matched write/restore pair, not a deployed config path.
37
+ self.wl_dir = wl_dir or current_root(RootKind.DEPLOY).wl_dir()
38
+ self.backup_dir = self.wl_dir / "backup"
39
+
40
+ def _ensure_backup_dir(self) -> None:
41
+ """Ensure backup directory exists."""
42
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ def _generate_backup_id(self) -> str:
45
+ """Generate a unique backup ID based on timestamp and random UUID.
46
+
47
+ Returns:
48
+ Backup ID string.
49
+ """
50
+ import time as time_module
51
+
52
+ timestamp = time_module.strftime("%Y%m%d_%H%M%S")
53
+ micros = str(int(time_module.time() * 1_000_000))[-6:]
54
+ random_suffix = str(uuid.uuid4())[:4]
55
+ return f"backup_{timestamp}_{micros}_{random_suffix}"
56
+
57
+ def create_backup(self) -> str:
58
+ """Create a backup of the current .wl directory.
59
+
60
+ Returns:
61
+ Backup ID of the created backup.
62
+ """
63
+ backup_id = self._generate_backup_id()
64
+ backup_path = self.backup_dir / backup_id
65
+
66
+ log_info_pair(f"Creating backup: {backup_id}", f"正在创建备份:{backup_id}")
67
+
68
+ self._ensure_backup_dir()
69
+ backup_path.mkdir(parents=True, exist_ok=True)
70
+
71
+ if self.wl_dir.exists():
72
+ self._copy_directory_contents(self.wl_dir, backup_path)
73
+
74
+ self.cleanup_old_backups()
75
+
76
+ log_info_pair(f"✓ Backup created: {backup_id}", f"✓ 备份已创建:{backup_id}")
77
+
78
+ return backup_id
79
+
80
+ def _copy_directory_contents(self, source: Path, dest: Path) -> None:
81
+ """Copy contents from source directory to destination, skipping backup.
82
+
83
+ Args:
84
+ source: Source directory path
85
+ dest: Destination directory path
86
+ """
87
+ for item in source.iterdir():
88
+ if item.name == "backup":
89
+ continue
90
+ target = dest / item.name
91
+ dest.mkdir(parents=True, exist_ok=True)
92
+ if item.is_dir():
93
+ shutil.copytree(str(item), str(target), dirs_exist_ok=True)
94
+ else:
95
+ shutil.copy2(str(item), str(target))
96
+
97
+ def _clear_wl_directory(self) -> None:
98
+ """Clear contents of .wl directory, keeping backup folder."""
99
+ self.wl_dir.mkdir(parents=True, exist_ok=True)
100
+ for item in list(self.wl_dir.iterdir()):
101
+ if item.name == "backup":
102
+ continue
103
+ if item.is_dir():
104
+ shutil.rmtree(str(item))
105
+ else:
106
+ item.unlink()
107
+
108
+ def _restore_from_temp_backup(self, temp_backup: Path) -> None:
109
+ """Restore from temporary backup directory.
110
+
111
+ Args:
112
+ temp_backup: Temporary backup directory path
113
+ """
114
+ # `.wl` here is the scratch snapshot's inner directory name, matched
115
+ # between write and restore -- not a root-derived config path (#121).
116
+ temp_wl_dir = temp_backup / ".wl"
117
+ if temp_wl_dir.exists():
118
+ self._clear_wl_directory()
119
+ self._copy_directory_contents(temp_wl_dir, self.wl_dir)
120
+ shutil.rmtree(str(temp_backup))
121
+
122
+ def rollback_to_backup(self, backup_id: str | None = None) -> bool:
123
+ """Rollback to a specific backup or the latest one.
124
+
125
+ Args:
126
+ backup_id: Optional backup ID to rollback to. If None, uses latest.
127
+
128
+ Returns:
129
+ True if rollback successful, False otherwise.
130
+ """
131
+ if not backup_id:
132
+ backups = self.list_backups()
133
+ if not backups:
134
+ log_info_pair(
135
+ "No backups available to rollback to", "没有可用的备份可回滚"
136
+ )
137
+ return False
138
+ backup_id = backups[-1]
139
+
140
+ backup_path = self.backup_dir / backup_id
141
+
142
+ if not backup_path.exists():
143
+ log_info_pair(f"Backup not found: {backup_id}", f"备份不存在:{backup_id}")
144
+ return False
145
+
146
+ log_info_pair(
147
+ f"Rolling back to backup: {backup_id}", f"正在回滚到备份:{backup_id}"
148
+ )
149
+
150
+ temp_backup = None
151
+ try:
152
+ if self.wl_dir.exists():
153
+ temp_backup = (
154
+ self.wl_dir.parent / f".wl_rollback_temp_{int(time.time())}"
155
+ )
156
+ # Scratch snapshot layout name; matched by _restore_from_temp_backup.
157
+ temp_wl_dir = temp_backup / ".wl"
158
+ self._copy_directory_contents(self.wl_dir, temp_wl_dir)
159
+
160
+ self._clear_wl_directory()
161
+ self._copy_directory_contents(backup_path, self.wl_dir)
162
+
163
+ if temp_backup and temp_backup.exists():
164
+ shutil.rmtree(str(temp_backup))
165
+
166
+ log_info_pair(
167
+ f"✓ Rollback successful to: {backup_id}", f"✓ 成功回滚到:{backup_id}"
168
+ )
169
+ return True
170
+
171
+ except Exception as e:
172
+ if temp_backup and temp_backup.exists():
173
+ self._restore_from_temp_backup(temp_backup)
174
+
175
+ log_info_pair(f"Rollback failed: {e}", f"回滚失败:{e}")
176
+ return False
177
+
178
+ def list_backups(self) -> list[str]:
179
+ """List all available backups.
180
+
181
+ Returns:
182
+ List of backup IDs, sorted from oldest to newest.
183
+ """
184
+ if not self.backup_dir.exists():
185
+ return []
186
+
187
+ backups = []
188
+ for item in self.backup_dir.iterdir():
189
+ if item.is_dir() and item.name.startswith("backup_"):
190
+ backups.append(item.name)
191
+
192
+ backups.sort()
193
+ return backups
194
+
195
+ def cleanup_old_backups(self) -> None:
196
+ """Clean up old backups, keeping only the most recent MAX_BACKUPS."""
197
+ backups = self.list_backups()
198
+
199
+ if len(backups) > self.MAX_BACKUPS:
200
+ to_remove = backups[: len(backups) - self.MAX_BACKUPS]
201
+
202
+ for backup_id in to_remove:
203
+ backup_path = self.backup_dir / backup_id
204
+ log_info_pair(
205
+ f"Removing old backup: {backup_id}", f"移除旧备份:{backup_id}"
206
+ )
207
+ shutil.rmtree(backup_path)
@@ -0,0 +1,83 @@
1
+ """
2
+ Component manager for managing updatable components.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+
10
+ class ComponentManager:
11
+ """Manage updatable components."""
12
+
13
+ def __init__(self):
14
+ """Initialize ComponentManager with default components."""
15
+ self._components: dict[str, dict] = {
16
+ "git_config": {
17
+ "description": "Git configuration files",
18
+ "source_path": "vendors/git/",
19
+ },
20
+ "editor_config": {
21
+ "description": "Editor configuration",
22
+ "source_path": "vendors/config/",
23
+ },
24
+ }
25
+
26
+ def get_all_components(self) -> dict[str, dict]:
27
+ """Get all updatable components.
28
+
29
+ Returns:
30
+ Dictionary of all components with their information.
31
+ """
32
+ return self._components.copy()
33
+
34
+ def get_component(self, component_id: str) -> dict | None:
35
+ """Get component information by ID.
36
+
37
+ Args:
38
+ component_id: ID of the component to get.
39
+
40
+ Returns:
41
+ Component information if found, None otherwise.
42
+ """
43
+ return self._components.get(component_id)
44
+
45
+ def component_exists(self, component_id: str) -> bool:
46
+ """Check if component exists.
47
+
48
+ Args:
49
+ component_id: ID of the component to check.
50
+
51
+ Returns:
52
+ True if component exists, False otherwise.
53
+ """
54
+ return component_id in self._components
55
+
56
+ def get_component_source_path(self, component_id: str) -> Path | None:
57
+ """Get component source path.
58
+
59
+ Args:
60
+ component_id: ID of the component.
61
+
62
+ Returns:
63
+ Source path as Path object if component exists, None otherwise.
64
+ """
65
+ component = self.get_component(component_id)
66
+ if component:
67
+ return Path(component["source_path"])
68
+ return None
69
+
70
+ def register_component(
71
+ self, component_id: str, description: str, source_path: str
72
+ ) -> None:
73
+ """Register a new component.
74
+
75
+ Args:
76
+ component_id: ID of the new component.
77
+ description: Description of the new component.
78
+ source_path: Source path of the new component.
79
+ """
80
+ self._components[component_id] = {
81
+ "description": description,
82
+ "source_path": source_path,
83
+ }
@@ -0,0 +1,189 @@
1
+ """
2
+ Conflict Resolver - detects and resolves file conflicts.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ from enum import Enum
9
+ from typing import TYPE_CHECKING
10
+
11
+ from wl_cli.commands.update.services.version_tracker import VersionTracker
12
+ from wl_cli.utils.logging import log_info_pair
13
+
14
+ from ....utils.root import RootKind
15
+ from ....utils.root_fallback import current_root
16
+
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+
20
+
21
+ class ConflictResolution(Enum):
22
+ """Conflict resolution strategies."""
23
+
24
+ OVERWRITE = "overwrite"
25
+ KEEP = "keep"
26
+ SKIP = "skip"
27
+
28
+
29
+ class ConflictResolver:
30
+ """Detect and resolve file conflicts."""
31
+
32
+ def __init__(self, wl_dir: Path | None = None):
33
+ """Initialize ConflictResolver.
34
+
35
+ Args:
36
+ wl_dir: Path to .wl directory. Defaults to './.wl'.
37
+ """
38
+ # Root-anchored (#125): the deploy root's ``.wl``, matching the update
39
+ # family this resolver serves. Was a bare ``Path(".wl")``, i.e. the
40
+ # process CWD's ``.wl`` (no production caller yet, but same ruling).
41
+ self.wl_dir = wl_dir or current_root(RootKind.DEPLOY).wl_dir()
42
+ self.version_tracker = VersionTracker(self.wl_dir)
43
+
44
+ def compute_file_hash(self, file_path: Path) -> str:
45
+ """Compute SHA-256 hash of a file.
46
+
47
+ Args:
48
+ file_path: Path to the file.
49
+
50
+ Returns:
51
+ SHA-256 hash of the file content.
52
+ """
53
+ if not file_path.exists():
54
+ return ""
55
+
56
+ hash_obj = hashlib.sha256()
57
+ with open(file_path, "rb") as f:
58
+ for chunk in iter(lambda: f.read(4096), b""):
59
+ hash_obj.update(chunk)
60
+ return hash_obj.hexdigest()
61
+
62
+ def detect_conflicts(
63
+ self, source_dir: Path, target_dir: Path
64
+ ) -> dict[str, ConflictResolution]:
65
+ """Detect conflicts between source and target files.
66
+
67
+ Args:
68
+ source_dir: Source directory path.
69
+ target_dir: Target directory path.
70
+
71
+ Returns:
72
+ Dictionary mapping file paths to conflict resolution status.
73
+ """
74
+ conflicts: dict[str, ConflictResolution] = {}
75
+
76
+ if not source_dir.exists() or not target_dir.exists():
77
+ return conflicts
78
+
79
+ for source_file in source_dir.rglob("*"):
80
+ if not source_file.is_file():
81
+ continue
82
+
83
+ relative_path = source_file.relative_to(source_dir)
84
+ target_file = target_dir / relative_path
85
+
86
+ if target_file.exists():
87
+ source_hash = self.compute_file_hash(source_file)
88
+ target_hash = self.compute_file_hash(target_file)
89
+
90
+ if source_hash != target_hash:
91
+ file_path_str = str(relative_path)
92
+
93
+ if self.version_tracker.is_custom_file(file_path_str):
94
+ conflicts[file_path_str] = ConflictResolution.KEEP
95
+ else:
96
+ conflicts[file_path_str] = ConflictResolution.OVERWRITE
97
+
98
+ return conflicts
99
+
100
+ def resolve_conflict(
101
+ self,
102
+ file_path: str,
103
+ source_file: Path,
104
+ target_file: Path,
105
+ resolution: ConflictResolution,
106
+ ) -> bool:
107
+ """Resolve a file conflict using specified strategy.
108
+
109
+ Args:
110
+ file_path: Relative path to the file.
111
+ source_file: Source file path.
112
+ target_file: Target file path.
113
+ resolution: Conflict resolution strategy.
114
+
115
+ Returns:
116
+ True if conflict resolved successfully, False otherwise.
117
+ """
118
+ if resolution == ConflictResolution.OVERWRITE:
119
+ log_info_pair(f"Overwriting: {file_path}", f"正在覆盖:{file_path}")
120
+
121
+ target_file.parent.mkdir(parents=True, exist_ok=True)
122
+ import shutil
123
+
124
+ shutil.copy2(source_file, target_file)
125
+
126
+ if self.version_tracker.is_custom_file(file_path):
127
+ self.version_tracker.remove_custom_file(file_path)
128
+
129
+ return True
130
+
131
+ elif resolution == ConflictResolution.KEEP:
132
+ log_info_pair(
133
+ f"Keeping user modified: {file_path}", f"保留用户修改:{file_path}"
134
+ )
135
+
136
+ if not self.version_tracker.is_custom_file(file_path):
137
+ self.version_tracker.add_custom_file(file_path)
138
+
139
+ return True
140
+
141
+ elif resolution == ConflictResolution.SKIP:
142
+ log_info_pair(f"Skipping: {file_path}", f"跳过:{file_path}")
143
+ return True
144
+
145
+ return False
146
+
147
+ def identify_custom_files(self, source_dir: Path, target_dir: Path) -> list[str]:
148
+ """Identify files in target that have been modified by user.
149
+
150
+ Args:
151
+ source_dir: Source directory (original template).
152
+ target_dir: Target directory (user's copy).
153
+
154
+ Returns:
155
+ List of custom file paths.
156
+ """
157
+ custom_files: list[str] = []
158
+
159
+ if not source_dir.exists() or not target_dir.exists():
160
+ return custom_files
161
+
162
+ for target_file in target_dir.rglob("*"):
163
+ if not target_file.is_file():
164
+ continue
165
+
166
+ relative_path = target_file.relative_to(target_dir)
167
+ source_file = source_dir / relative_path
168
+
169
+ if source_file.exists():
170
+ source_hash = self.compute_file_hash(source_file)
171
+ target_hash = self.compute_file_hash(target_file)
172
+
173
+ if source_hash != target_hash:
174
+ custom_files.append(str(relative_path))
175
+ else:
176
+ custom_files.append(str(relative_path))
177
+
178
+ return custom_files
179
+
180
+ def mark_as_custom(self, file_path: str) -> None:
181
+ """Mark a file as user-customized.
182
+
183
+ Args:
184
+ file_path: Path to the file.
185
+ """
186
+ self.version_tracker.add_custom_file(file_path)
187
+ log_info_pair(
188
+ f"Marked as custom: {file_path}", f"已标记为自定义文件:{file_path}"
189
+ )
@@ -0,0 +1,85 @@
1
+ """
2
+ Vendors Updater - manages updating vendor templates from package.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from wl_cli.utils.logging import log_info_pair
10
+ from wl_cli.utils.wl_dir_updater import (
11
+ _get_package_vendors_path,
12
+ check_and_update_vendors,
13
+ )
14
+
15
+ from ....utils.root import RootKind
16
+ from ....utils.root_fallback import current_root
17
+
18
+ if TYPE_CHECKING:
19
+ from pathlib import Path
20
+
21
+
22
+ class VendorsUpdater:
23
+ """Manage updating vendor templates from package."""
24
+
25
+ def __init__(self, wl_dir: Path | None = None):
26
+ """Initialize VendorsUpdater.
27
+
28
+ Args:
29
+ wl_dir: Path to .wl directory. Defaults to './.wl'.
30
+ """
31
+ # Root-anchored (#125): the deploy root's ``.wl``, matching the update
32
+ # family. ``self.wl_dir`` is currently never read, so the value is inert
33
+ # -- fixed for consistency, not because a path was observed wrong.
34
+ self.wl_dir = wl_dir or current_root(RootKind.DEPLOY).wl_dir()
35
+ self.package_vendors_path = _get_package_vendors_path()
36
+
37
+ def update(
38
+ self,
39
+ dry_run: bool = False,
40
+ component_id: str | None = None,
41
+ force: bool = False,
42
+ ) -> dict:
43
+ """Update vendors directory.
44
+
45
+ Args:
46
+ dry_run: If True, only show what would be done.
47
+ component_id: Optional specific component to update.
48
+ force: Copy even when the vendor stamp is already current.
49
+
50
+ Returns:
51
+ Update result dictionary.
52
+ """
53
+ log_info_pair("Starting vendor update...", "开始更新供应商模板...")
54
+
55
+ result: dict = check_and_update_vendors(
56
+ dry_run=dry_run, component_id=component_id, force=force
57
+ )
58
+
59
+ if dry_run:
60
+ log_info_pair(
61
+ "Dry run completed. No changes made.", "干运行完成,未做任何更改。"
62
+ )
63
+ else:
64
+ if result.get("updated"):
65
+ log_info_pair("Vendors updated successfully!", "供应商模板更新成功!")
66
+ else:
67
+ log_info_pair(
68
+ "Vendors are already up to date.", "供应商模板已是最新版本。"
69
+ )
70
+
71
+ return result
72
+
73
+ def check(self) -> dict:
74
+ """Check for vendor updates.
75
+
76
+ Returns:
77
+ Check result dictionary.
78
+ """
79
+ log_info_pair("Checking for vendor updates...", "检查供应商模板更新...")
80
+
81
+ result: dict = check_and_update_vendors(dry_run=True)
82
+
83
+ log_info_pair("Check completed.", "检查完成。")
84
+
85
+ return result
@@ -0,0 +1,127 @@
1
+ """
2
+ Version tracker for managing version information of vendor templates.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from datetime import datetime
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+
15
+ class VersionTracker:
16
+ """Manage version tracking for vendor templates."""
17
+
18
+ VERSION_FILE = ".version.json"
19
+
20
+ def __init__(self, wl_dir: Path):
21
+ """Initialize VersionTracker.
22
+
23
+ Args:
24
+ wl_dir: Path to .wl directory.
25
+ """
26
+ self.wl_dir = wl_dir
27
+ self.vendors_dir = wl_dir / "vendors"
28
+ self.version_file = self.vendors_dir / self.VERSION_FILE
29
+
30
+ def ensure_vendors_dir(self) -> None:
31
+ """Ensure vendors directory exists."""
32
+ self.vendors_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+ def get_version_info(self) -> dict | None:
35
+ """Get current version information.
36
+
37
+ Returns:
38
+ Version information dict if file exists and valid, None otherwise.
39
+ """
40
+ if not self.version_file.exists():
41
+ return None
42
+
43
+ try:
44
+ with open(self.version_file, encoding="utf-8") as f:
45
+ data: dict = json.load(f)
46
+ return data
47
+ except (OSError, json.JSONDecodeError):
48
+ return None
49
+
50
+ def update_version_info(
51
+ self, version: str, source: str, custom_files: list[str] | None = None
52
+ ) -> None:
53
+ """Update version information.
54
+
55
+ Args:
56
+ version: Version string.
57
+ source: Source of the update.
58
+ custom_files: Optional list of custom files to track.
59
+ """
60
+ self.ensure_vendors_dir()
61
+
62
+ version_info = {
63
+ "version": version,
64
+ "last_updated": datetime.now().isoformat(),
65
+ "source": source,
66
+ "custom_files": custom_files or [],
67
+ }
68
+
69
+ with open(self.version_file, "w", encoding="utf-8") as f:
70
+ json.dump(version_info, f, indent=2, ensure_ascii=False)
71
+
72
+ def add_custom_file(self, file_path: str) -> None:
73
+ """Add custom file to tracking list.
74
+
75
+ Args:
76
+ file_path: Path to custom file.
77
+ """
78
+ version_info = self.get_version_info() or {}
79
+ custom_files = version_info.get("custom_files", [])
80
+
81
+ if file_path not in custom_files:
82
+ custom_files.append(file_path)
83
+ version_info["custom_files"] = custom_files
84
+
85
+ if "version" in version_info and "source" in version_info:
86
+ self.update_version_info(
87
+ version_info["version"],
88
+ version_info["source"],
89
+ custom_files,
90
+ )
91
+
92
+ def remove_custom_file(self, file_path: str) -> None:
93
+ """Remove custom file from tracking list.
94
+
95
+ Args:
96
+ file_path: Path to custom file to remove.
97
+ """
98
+ version_info = self.get_version_info()
99
+ if not version_info:
100
+ return
101
+
102
+ custom_files = version_info.get("custom_files", [])
103
+ if file_path in custom_files:
104
+ custom_files.remove(file_path)
105
+ version_info["custom_files"] = custom_files
106
+
107
+ if "version" in version_info and "source" in version_info:
108
+ self.update_version_info(
109
+ version_info["version"],
110
+ version_info["source"],
111
+ custom_files,
112
+ )
113
+
114
+ def is_custom_file(self, file_path: str) -> bool:
115
+ """Check if file is a custom file.
116
+
117
+ Args:
118
+ file_path: Path to check.
119
+
120
+ Returns:
121
+ True if file is in custom files list, False otherwise.
122
+ """
123
+ version_info = self.get_version_info()
124
+ if not version_info:
125
+ return False
126
+
127
+ return file_path in version_info.get("custom_files", [])