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.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- 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
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))
|