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,477 @@
1
+ """GitHub Actions workflow validation using wrkflw."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import cast
9
+
10
+ import click
11
+
12
+ from .base import BaseCommand
13
+
14
+
15
+ class ValidationMode(Enum):
16
+ """Validation behavior mode."""
17
+
18
+ WARN = "warn" # Validation failures are warnings only
19
+ STRICT = "strict" # Validation failures block progress
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class WorkflowValidationResult:
24
+ """Result of workflow validation."""
25
+
26
+ success: bool
27
+ validated_files: int
28
+ warnings: int
29
+ message: str
30
+ wrkflw_available: bool
31
+
32
+ def __str__(self) -> str:
33
+ """Format result for display."""
34
+ if not self.wrkflw_available:
35
+ return f"💡 {self.message}"
36
+ if not self.success:
37
+ return f"❌ Workflow validation FAILED: {self.message}"
38
+ if self.warnings > 0:
39
+ return (
40
+ f"⚠️ Workflow validation: {self.warnings} warning(s) - {self.message}"
41
+ )
42
+ return (
43
+ f"✅ Workflow validation passed - {self.validated_files} file(s) validated"
44
+ )
45
+
46
+
47
+ class WorkflowChangeDetector:
48
+ """Detect changes to GitHub Actions workflow files."""
49
+
50
+ def __init__(self, repo_root: Path | None = None):
51
+ """Initialize detector.
52
+
53
+ Args:
54
+ repo_root: Path to repository root (default: cwd)
55
+ """
56
+ self.repo_root = repo_root or Path.cwd()
57
+
58
+ def _run_git_diff(self) -> list[str]:
59
+ """Run git diff to get changed files."""
60
+ try:
61
+ result = subprocess.run(
62
+ ["git", "diff", "--name-only", "HEAD"],
63
+ cwd=self.repo_root,
64
+ capture_output=True,
65
+ text=True,
66
+ check=True,
67
+ )
68
+ return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
69
+ except (subprocess.CalledProcessError, FileNotFoundError):
70
+ return []
71
+
72
+ def _run_git_untracked(self) -> list[str]:
73
+ """Run git ls-files to get untracked files."""
74
+ try:
75
+ result = subprocess.run(
76
+ ["git", "ls-files", "--others", "--exclude-standard"],
77
+ cwd=self.repo_root,
78
+ capture_output=True,
79
+ text=True,
80
+ check=True,
81
+ )
82
+ return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
83
+ except (subprocess.CalledProcessError, FileNotFoundError):
84
+ return []
85
+
86
+ def get_changed_workflows(self) -> list[str]:
87
+ """Get list of workflow files that have changed or are untracked."""
88
+ try:
89
+ changed = self._run_git_diff()
90
+ untracked = self._run_git_untracked()
91
+ all_files = set(changed + untracked)
92
+ return [f for f in all_files if ".github/workflows/" in f]
93
+ except (subprocess.CalledProcessError, FileNotFoundError):
94
+ return []
95
+
96
+ def has_workflow_changes(self) -> bool:
97
+ """Check if any workflow files have changed or are untracked."""
98
+ return len(self.get_changed_workflows()) > 0
99
+
100
+
101
+ class WorkflowValidator:
102
+ """Validate GitHub Actions workflows using wrkflw."""
103
+
104
+ def __init__(
105
+ self,
106
+ repo_root: Path | None = None,
107
+ mode: ValidationMode = ValidationMode.WARN,
108
+ ):
109
+ """Initialize validator.
110
+
111
+ Args:
112
+ repo_root: Path to repository root (default: cwd)
113
+ mode: Validation mode (WARN or STRICT)
114
+ """
115
+ if repo_root:
116
+ self.repo_root = repo_root
117
+ else:
118
+ try:
119
+ self.repo_root = Path.cwd()
120
+ except FileNotFoundError:
121
+ self.repo_root = Path("/tmp")
122
+ self.mode = mode
123
+ self.detector = WorkflowChangeDetector(self.repo_root)
124
+
125
+ def _run_wrkflw(
126
+ self, files: list[str] | None = None
127
+ ) -> subprocess.CompletedProcess[str]:
128
+ """Run wrkflw validate command.
129
+
130
+ Args:
131
+ files: Optional list of files to validate (validates all if None)
132
+ """
133
+ cmd = ["wrkflw", "validate", "--exit-code"]
134
+ if files:
135
+ cmd.extend(files)
136
+
137
+ return subprocess.run(
138
+ cmd,
139
+ cwd=self.repo_root,
140
+ capture_output=True,
141
+ text=True,
142
+ check=True,
143
+ )
144
+
145
+ def validate_all(self) -> WorkflowValidationResult:
146
+ """Validate all workflow files.
147
+
148
+ Returns:
149
+ Validation result with success status and message
150
+ """
151
+ file_count = self._count_workflow_files()
152
+
153
+ try:
154
+ result = self._run_wrkflw()
155
+ output = result.stdout + result.stderr
156
+ return WorkflowValidationResult(
157
+ success=True,
158
+ validated_files=file_count,
159
+ warnings=0,
160
+ message=output or "All workflows valid",
161
+ wrkflw_available=True,
162
+ )
163
+ except FileNotFoundError:
164
+ return WorkflowValidationResult(
165
+ success=True,
166
+ validated_files=0,
167
+ warnings=0,
168
+ message="wrkflw not installed. Install with: brew install wrkflw",
169
+ wrkflw_available=False,
170
+ )
171
+ except subprocess.CalledProcessError as e:
172
+ error_output = (e.stdout + e.stderr).strip()
173
+ if self.mode == ValidationMode.STRICT:
174
+ return WorkflowValidationResult(
175
+ success=False,
176
+ validated_files=file_count,
177
+ warnings=0,
178
+ message=error_output or "Validation failed",
179
+ wrkflw_available=True,
180
+ )
181
+ else:
182
+ # WARN mode - show warning but don't block
183
+ return WorkflowValidationResult(
184
+ success=True,
185
+ validated_files=file_count,
186
+ warnings=1,
187
+ message=error_output or "Validation warnings",
188
+ wrkflw_available=True,
189
+ )
190
+
191
+ def validate_file(self, workflow_file: Path | str) -> WorkflowValidationResult:
192
+ """Validate a specific workflow file.
193
+
194
+ Args:
195
+ workflow_file: Path to workflow file to validate
196
+
197
+ Returns:
198
+ Validation result with success status and message
199
+ """
200
+ file_path = Path(workflow_file)
201
+ if not file_path.is_absolute():
202
+ file_path = self.repo_root / file_path
203
+
204
+ if not file_path.exists():
205
+ return WorkflowValidationResult(
206
+ success=True,
207
+ validated_files=0,
208
+ warnings=1,
209
+ message=f"Workflow file not found: {workflow_file}",
210
+ wrkflw_available=True,
211
+ )
212
+
213
+ try:
214
+ result = self._run_wrkflw([str(file_path)])
215
+ output = result.stdout + result.stderr
216
+ return WorkflowValidationResult(
217
+ success=True,
218
+ validated_files=1,
219
+ warnings=0,
220
+ message=output or "Workflow valid",
221
+ wrkflw_available=True,
222
+ )
223
+ except FileNotFoundError:
224
+ return WorkflowValidationResult(
225
+ success=True,
226
+ validated_files=0,
227
+ warnings=0,
228
+ message="wrkflw not installed. Install with: brew install wrkflw",
229
+ wrkflw_available=False,
230
+ )
231
+ except subprocess.CalledProcessError as e:
232
+ error_output = (e.stdout + e.stderr).strip()
233
+ if self.mode == ValidationMode.STRICT:
234
+ return WorkflowValidationResult(
235
+ success=False,
236
+ validated_files=0,
237
+ warnings=0,
238
+ message=error_output or "Validation failed",
239
+ wrkflw_available=True,
240
+ )
241
+ else:
242
+ return WorkflowValidationResult(
243
+ success=True,
244
+ validated_files=0,
245
+ warnings=1,
246
+ message=error_output or "Validation warnings",
247
+ wrkflw_available=True,
248
+ )
249
+
250
+ def validate_changed(self) -> WorkflowValidationResult:
251
+ """Validate only changed workflow files.
252
+
253
+ Returns:
254
+ Validation result with success status and message
255
+ """
256
+ changed_files = self.detector.get_changed_workflows()
257
+
258
+ if not changed_files:
259
+ return WorkflowValidationResult(
260
+ success=True,
261
+ validated_files=0,
262
+ warnings=0,
263
+ message="No workflow changes detected",
264
+ wrkflw_available=True,
265
+ )
266
+
267
+ try:
268
+ result = self._run_wrkflw(changed_files)
269
+ output = result.stdout + result.stderr
270
+ return WorkflowValidationResult(
271
+ success=True,
272
+ validated_files=len(changed_files),
273
+ warnings=0,
274
+ message=output or "All changed workflows valid",
275
+ wrkflw_available=True,
276
+ )
277
+ except FileNotFoundError:
278
+ return WorkflowValidationResult(
279
+ success=True,
280
+ validated_files=0,
281
+ warnings=0,
282
+ message="wrkflw not installed. Install with: brew install wrkflw",
283
+ wrkflw_available=False,
284
+ )
285
+ except subprocess.CalledProcessError as e:
286
+ error_output = (e.stdout + e.stderr).strip()
287
+ if self.mode == ValidationMode.STRICT:
288
+ return WorkflowValidationResult(
289
+ success=False,
290
+ validated_files=0,
291
+ warnings=0,
292
+ message=error_output or "Validation failed",
293
+ wrkflw_available=True,
294
+ )
295
+ else:
296
+ return WorkflowValidationResult(
297
+ success=True,
298
+ validated_files=0,
299
+ warnings=1,
300
+ message=error_output or "Validation warnings",
301
+ wrkflw_available=True,
302
+ )
303
+
304
+ def _count_workflow_files(self) -> int:
305
+ """Count workflow files in the repository."""
306
+ workflow_dir = self.repo_root / ".github" / "workflows"
307
+ if workflow_dir.exists():
308
+ return len(list(workflow_dir.glob("*.yml"))) + len(
309
+ list(workflow_dir.glob("*.yaml"))
310
+ )
311
+ return 0
312
+
313
+
314
+ class WorkflowCoordinator:
315
+ """Coordinator for workflow validation operations."""
316
+
317
+ def __init__(
318
+ self,
319
+ repo_root: Path | None = None,
320
+ mode: ValidationMode = ValidationMode.WARN,
321
+ validator: WorkflowValidator | None = None,
322
+ ):
323
+ """Initialize coordinator."""
324
+ if validator is not None:
325
+ self.validator = validator
326
+ else:
327
+ self.validator = WorkflowValidator(repo_root=repo_root, mode=mode)
328
+
329
+ def execute(self, **kwargs) -> int:
330
+ """Execute workflow validation.
331
+
332
+ Args:
333
+ **kwargs: Command arguments
334
+
335
+ Returns:
336
+ Exit code (0 on success, 1 on failure in strict mode)
337
+ """
338
+ workflow_file = kwargs.get("workflow_file")
339
+ force_workflow = (
340
+ kwargs.get("workflow", False)
341
+ or kwargs.get("ci", False)
342
+ or kwargs.get("all", False)
343
+ )
344
+ strict_workflow = kwargs.get("strict_workflow", False)
345
+
346
+ if strict_workflow:
347
+ self.validator.mode = ValidationMode.STRICT
348
+
349
+ if workflow_file:
350
+ result = self.validator.validate_file(workflow_file)
351
+ elif force_workflow:
352
+ result = self.validator.validate_all()
353
+ elif self.validator.detector.has_workflow_changes():
354
+ result = self.validator.validate_changed()
355
+ else:
356
+ print("📋 No workflow changes detected, skipping validation")
357
+ return 0
358
+
359
+ print(str(result))
360
+
361
+ return 0 if result.success else 1
362
+
363
+
364
+ class WorkflowCommand:
365
+ """Implementation of the workflow command."""
366
+
367
+ def __init__(self):
368
+ """Initialize workflow command."""
369
+ self.coordinator = WorkflowCoordinator()
370
+
371
+ @property
372
+ def name(self) -> str:
373
+ return "workflow"
374
+
375
+ @property
376
+ def help(self) -> str:
377
+ return "Validate GitHub Actions workflows using wrkflw"
378
+
379
+ def execute(
380
+ self,
381
+ workflow: bool,
382
+ ci: bool,
383
+ all: bool,
384
+ strict_workflow: bool,
385
+ workflow_file: str | None,
386
+ ) -> int:
387
+ """Execute workflow validation.
388
+
389
+ Args:
390
+ workflow: Force workflow validation
391
+ ci: Alias for --workflow
392
+ all: Alias for --workflow (validate all workflow files)
393
+ strict_workflow: Workflow validation failures block execution
394
+ workflow_file: Validate only the specified workflow file
395
+
396
+ Returns:
397
+ Exit code (0 on success, 1 on failure in strict mode)
398
+ """
399
+ return cast(
400
+ "int",
401
+ self.coordinator.execute(
402
+ workflow=workflow,
403
+ ci=ci,
404
+ all=all,
405
+ strict_workflow=strict_workflow,
406
+ workflow_file=workflow_file,
407
+ ),
408
+ )
409
+
410
+
411
+ @click.command()
412
+ @click.option(
413
+ "--workflow", is_flag=True, help="Force workflow validation regardless of changes"
414
+ )
415
+ @click.option("--ci", is_flag=True, help="Alias for --workflow")
416
+ @click.option(
417
+ "--all",
418
+ "all_flag",
419
+ is_flag=True,
420
+ help="Alias for --workflow (validate all workflow files)",
421
+ )
422
+ @click.option(
423
+ "--strict-workflow",
424
+ is_flag=True,
425
+ help="Workflow validation failures block execution",
426
+ )
427
+ @click.option(
428
+ "--workflow-file", type=str, help="Validate only the specified workflow file"
429
+ )
430
+ @click.pass_context
431
+ def workflow_command(
432
+ ctx: click.Context,
433
+ workflow: bool,
434
+ ci: bool,
435
+ all_flag: bool,
436
+ strict_workflow: bool,
437
+ workflow_file: str | None,
438
+ ) -> None:
439
+ """Validate GitHub Actions workflows using wrkflw."""
440
+ cmd = WorkflowCommand()
441
+ if ctx.obj and "coordinator" in ctx.obj:
442
+ cmd.coordinator = ctx.obj["coordinator"]
443
+ exit_code = cmd.execute(
444
+ workflow=workflow,
445
+ ci=ci,
446
+ all=all_flag,
447
+ strict_workflow=strict_workflow,
448
+ workflow_file=workflow_file,
449
+ )
450
+ if exit_code != 0:
451
+ sys.exit(exit_code)
452
+
453
+
454
+ class WorkflowCommandWrapper(BaseCommand):
455
+ """GitHub Actions workflow validation command (legacy argparse interface)."""
456
+
457
+ def __init__(self):
458
+ """Initialize workflow command."""
459
+ self.coordinator = WorkflowCoordinator()
460
+
461
+ def run(self, *args, **kwargs):
462
+ """Run workflow validation command."""
463
+ return super().run(*args, **kwargs)
464
+
465
+ @property
466
+ def name(self) -> str:
467
+ return "workflow"
468
+
469
+ @property
470
+ def help(self) -> str:
471
+ return "Validate GitHub Actions workflows using wrkflw"
472
+
473
+ def execute(self, *args, **kwargs):
474
+ """Execute the workflow validation command."""
475
+ exit_code = self.coordinator.execute(*args, **kwargs)
476
+ if exit_code != 0:
477
+ sys.exit(exit_code)
wl_cli/env/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Environment domain (machine-readable five-state JSON via ``wl_cli.envelope``)."""
2
+
3
+ from .cli import env_group
4
+
5
+ __all__ = ["env_group"]
wl_cli/env/cli.py ADDED
@@ -0,0 +1,167 @@
1
+ """Click namespace for the ``env`` domain (issues #220, #221).
2
+
3
+ ``wl env check`` reports **the wl that is running** as a five-state JSON
4
+ envelope (:mod:`wl_cli.envelope`); ``wl env repair`` installs the release
5
+ floor with ``uv tool install``. The payload builder lives in
6
+ :mod:`wl_cli.env_status` because the quality domain may not import this
7
+ package (#220): the deprecated ``wl self capabilities`` alias in
8
+ ``wl_cli.commands`` calls the same builder, so the two commands cannot answer
9
+ differently.
10
+
11
+ This package may import the shared layer (:mod:`wl_cli.envelope`,
12
+ :mod:`wl_cli.platform`, :mod:`wl_cli.capabilities`, :mod:`wl_cli.utils`) and
13
+ nothing from ``wl_cli.cc`` / ``wl_cli.mcp`` / ``wl_cli.quality`` -- the
14
+ dependency direction is enforced by ``tests/test_domain_import_direction.py``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import subprocess
20
+ import tempfile
21
+ from typing import Any
22
+
23
+ import click
24
+
25
+ from ..env_status import (
26
+ INSTALL_ARGV,
27
+ INSTALL_COMMAND,
28
+ build_payload,
29
+ emit_check,
30
+ install_fields,
31
+ )
32
+ from ..envelope import OK_STATUSES, dump, exit_code
33
+
34
+ #: Stable tool name in the repair envelope.
35
+ REPAIR_TOOL = "env repair"
36
+
37
+ #: How much of uv's output is quoted in a failure reason.
38
+ _ERROR_EXCERPT_LIMIT = 400
39
+
40
+
41
+ @click.group(
42
+ name="env",
43
+ help="Environment probes and repairs (machine-readable five-state JSON).",
44
+ )
45
+ def env_group() -> None:
46
+ """Environment domain entry point (``check`` and ``repair``)."""
47
+
48
+
49
+ def _summary_lines(
50
+ status: str, reason: str | None, fields: dict[str, Any]
51
+ ) -> list[str]:
52
+ """Render the human-readable ``check`` summary (no ``--json``)."""
53
+ if status == "error":
54
+ return [f"wl env check: error: {reason}"]
55
+ native = "present" if fields["native_extension"] else "absent"
56
+ capabilities = ", ".join(fields["capabilities"])
57
+ lines = [
58
+ f"wl {fields['version']} ({status})",
59
+ f"executable: {fields['executable']}",
60
+ f"platform: {fields['platform']} (Python {fields['python']})",
61
+ f"native extension: {native}",
62
+ f"capabilities: {capabilities}",
63
+ f"install: {fields['install']['command']}",
64
+ ]
65
+ if reason:
66
+ lines.append(f"reason: {reason}")
67
+ return lines
68
+
69
+
70
+ @env_group.command(name="check")
71
+ @click.option(
72
+ "--json",
73
+ "as_json",
74
+ is_flag=True,
75
+ help="Emit one five-state JSON envelope on stdout.",
76
+ )
77
+ def check(as_json: bool) -> None:
78
+ """Report the running wl: version, executable, platform and capabilities.
79
+
80
+ Read-only. In ``--json`` mode stdout is exactly one envelope line and
81
+ diagnostics go to stderr; the probe itself runs in a temporary CWD and the
82
+ dispatcher skips the startup ``.wl`` self-sync (#220), so the caller's
83
+ directory is never written to.
84
+ """
85
+ if as_json:
86
+ raise SystemExit(emit_check())
87
+
88
+ status, reason, fields = build_payload()
89
+ for line in _summary_lines(status, reason, fields):
90
+ click.echo(line)
91
+ raise SystemExit(exit_code(status))
92
+
93
+
94
+ def _excerpt(text: str) -> str:
95
+ """Flatten uv's output into one readable line (empty string when blank)."""
96
+ return " ".join(text.split())[:_ERROR_EXCERPT_LIMIT]
97
+
98
+
99
+ def _run_install() -> tuple[str, str | None, dict[str, Any]]:
100
+ """Run the literal install command in a throwaway directory.
101
+
102
+ The command is executed as an argv list (``shell=False``) with an explicit
103
+ ``--python 3.12`` and no ``--force``: uv's own semantics make an identical
104
+ install a no-op, which is what makes ``wl env repair`` idempotent. The
105
+ temporary CWD keeps a stray ``uv.toml`` / ``.python-version`` in the
106
+ caller's directory from overriding the pinned interpreter.
107
+
108
+ Returns:
109
+ ``(status, reason, fields)`` -- ``repaired`` on rc 0 and
110
+ ``unverifiable`` otherwise (uv's exit code and an output excerpt are
111
+ quoted in ``reason``; a failure is never swallowed).
112
+ """
113
+ payload: dict[str, Any] = {
114
+ "command": INSTALL_COMMAND,
115
+ "floor": install_fields()["floor"],
116
+ }
117
+ with tempfile.TemporaryDirectory(prefix="wl-env-repair-") as workdir:
118
+ try:
119
+ result = subprocess.run(
120
+ list(INSTALL_ARGV),
121
+ cwd=workdir,
122
+ capture_output=True,
123
+ text=True,
124
+ check=False,
125
+ )
126
+ except FileNotFoundError as exc:
127
+ return "unverifiable", f"could not run uv: {exc}", payload
128
+ except OSError as exc:
129
+ return "unverifiable", f"could not run uv: {exc}", payload
130
+
131
+ payload["returncode"] = result.returncode
132
+ if result.returncode == 0:
133
+ return "repaired", None, payload
134
+
135
+ excerpt = _excerpt(result.stderr) or _excerpt(result.stdout)
136
+ reason = f"uv exited with code {result.returncode}"
137
+ if excerpt:
138
+ reason = f"{reason}: {excerpt}"
139
+ return "unverifiable", reason, payload
140
+
141
+
142
+ @env_group.command(name="repair")
143
+ @click.option(
144
+ "--json",
145
+ "as_json",
146
+ is_flag=True,
147
+ help="Emit one five-state JSON envelope on stdout.",
148
+ )
149
+ def repair(as_json: bool) -> None:
150
+ """Install the release floor with ``uv tool install`` (idempotent).
151
+
152
+ The exact command printed to stderr is the exact argv executed -- one
153
+ literal, no second copy. In ``--json`` mode stdout is one envelope line;
154
+ the command and any diagnostics stay on stderr.
155
+ """
156
+ click.echo(f"Running: {INSTALL_COMMAND}", err=True)
157
+ status, reason, fields = _run_install()
158
+
159
+ if as_json:
160
+ dump(status, status in OK_STATUSES, tool=REPAIR_TOOL, reason=reason, **fields)
161
+ raise SystemExit(exit_code(status))
162
+
163
+ if status == "repaired":
164
+ click.echo(f"repaired: {INSTALL_COMMAND}")
165
+ else:
166
+ click.echo(f"wl env repair: {status}: {reason}")
167
+ raise SystemExit(exit_code(status))