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
wl_cli/mcp/k8s.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Read-only Kubernetes diagnostics MCP tools over SSH."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from .protocol import serve as serve_protocol
|
|
7
|
+
|
|
8
|
+
SSH_HOST = os.environ.get("K8S_SSH_HOST", "ubuntu-server")
|
|
9
|
+
KUBECONFIG = "/etc/rancher/k3s/k3s.yaml"
|
|
10
|
+
DEFAULT_NS = os.environ.get("K8S_DEFAULT_NS", "")
|
|
11
|
+
SERVER_NAME = "k8s-diagnose"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _resolve_ns(namespace: str) -> str:
|
|
15
|
+
"""Fall back to K8S_DEFAULT_NS when no namespace is supplied."""
|
|
16
|
+
return namespace or DEFAULT_NS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _kubectl(kubectl_args: list[str], timeout: int = 60) -> str:
|
|
20
|
+
"""Run a read-only kubectl command on the K3s host over SSH."""
|
|
21
|
+
full_cmd = [
|
|
22
|
+
"ssh",
|
|
23
|
+
SSH_HOST,
|
|
24
|
+
"--",
|
|
25
|
+
"sudo",
|
|
26
|
+
f"KUBECONFIG={KUBECONFIG}",
|
|
27
|
+
"kubectl",
|
|
28
|
+
] + kubectl_args
|
|
29
|
+
display = " ".join(full_cmd)
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
full_cmd, capture_output=True, text=True, timeout=timeout
|
|
33
|
+
)
|
|
34
|
+
if result.returncode == 0:
|
|
35
|
+
return result.stdout
|
|
36
|
+
err = (result.stderr or result.stdout).strip()
|
|
37
|
+
if result.returncode == 255:
|
|
38
|
+
return (
|
|
39
|
+
f"Error: SSH to {SSH_HOST} failed (exit 255): {err}\nCommand: {display}"
|
|
40
|
+
)
|
|
41
|
+
return f"Error: kubectl exit {result.returncode}: {err}\nCommand: {display}"
|
|
42
|
+
except subprocess.TimeoutExpired:
|
|
43
|
+
return f"Error: command timed out after {timeout}s: {display}"
|
|
44
|
+
except FileNotFoundError as exc:
|
|
45
|
+
return f"Error: ssh/kubectl not found ({exc}) — check SSH host '{SSH_HOST}': {display}"
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
return f"Error: failed to run command ({exc}): {display}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def k8s_get(
|
|
51
|
+
resource: str, name: str = "", namespace: str = "", output: str = ""
|
|
52
|
+
) -> str:
|
|
53
|
+
"""List Kubernetes resources with kubectl get."""
|
|
54
|
+
namespace = _resolve_ns(namespace)
|
|
55
|
+
args = ["get", resource]
|
|
56
|
+
if name:
|
|
57
|
+
args.append(name)
|
|
58
|
+
if namespace:
|
|
59
|
+
args += ["-n", namespace]
|
|
60
|
+
elif not name:
|
|
61
|
+
args.append("-A")
|
|
62
|
+
if output:
|
|
63
|
+
args += ["-o", output]
|
|
64
|
+
return _kubectl(args)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def k8s_describe(resource: str, name: str = "", namespace: str = "") -> str:
|
|
68
|
+
"""Describe a Kubernetes resource."""
|
|
69
|
+
namespace = _resolve_ns(namespace)
|
|
70
|
+
args = ["describe", resource]
|
|
71
|
+
if name:
|
|
72
|
+
args.append(name)
|
|
73
|
+
if namespace:
|
|
74
|
+
args += ["-n", namespace]
|
|
75
|
+
return _kubectl(args)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def k8s_logs(
|
|
79
|
+
pod: str,
|
|
80
|
+
namespace: str = "",
|
|
81
|
+
tail: int = 50,
|
|
82
|
+
container: str = "",
|
|
83
|
+
since: str = "",
|
|
84
|
+
previous: bool = False,
|
|
85
|
+
all_containers: bool = False,
|
|
86
|
+
timeout: int = 60,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Get logs from a pod."""
|
|
89
|
+
namespace = _resolve_ns(namespace)
|
|
90
|
+
args = ["logs", pod]
|
|
91
|
+
if namespace:
|
|
92
|
+
args += ["-n", namespace]
|
|
93
|
+
if tail > 0:
|
|
94
|
+
args += ["--tail", str(tail)]
|
|
95
|
+
if container:
|
|
96
|
+
args += ["-c", container]
|
|
97
|
+
if since:
|
|
98
|
+
args += ["--since", since]
|
|
99
|
+
if previous:
|
|
100
|
+
args += ["--previous"]
|
|
101
|
+
if all_containers:
|
|
102
|
+
args += ["--all-containers"]
|
|
103
|
+
return _kubectl(args, timeout=timeout)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def k8s_top(resource_type: str, namespace: str = "") -> str:
|
|
107
|
+
"""Show CPU and memory usage."""
|
|
108
|
+
namespace = _resolve_ns(namespace)
|
|
109
|
+
args = ["top", resource_type]
|
|
110
|
+
if namespace and resource_type == "pod":
|
|
111
|
+
args += ["-n", namespace]
|
|
112
|
+
return _kubectl(args)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def k8s_cluster_info() -> str:
|
|
116
|
+
"""Show cluster endpoints and services."""
|
|
117
|
+
return _kubectl(["cluster-info"])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def k8s_api_resources() -> str:
|
|
121
|
+
"""List available API resources."""
|
|
122
|
+
return _kubectl(["api-resources"])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def k8s_explain(resource: str) -> str:
|
|
126
|
+
"""Show documentation for a Kubernetes resource."""
|
|
127
|
+
return _kubectl(["explain", resource])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def k8s_version() -> str:
|
|
131
|
+
"""Show client and server Kubernetes versions."""
|
|
132
|
+
return _kubectl(["version"])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def k8s_events(namespace: str = "", field_selector: str = "", output: str = "") -> str:
|
|
136
|
+
"""List Kubernetes events."""
|
|
137
|
+
namespace = _resolve_ns(namespace)
|
|
138
|
+
args = ["get", "events"]
|
|
139
|
+
if namespace:
|
|
140
|
+
args += ["-n", namespace]
|
|
141
|
+
else:
|
|
142
|
+
args.append("-A")
|
|
143
|
+
if field_selector:
|
|
144
|
+
args += ["--field-selector", field_selector]
|
|
145
|
+
if output:
|
|
146
|
+
args += ["-o", output]
|
|
147
|
+
return _kubectl(args)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_ALLOWED_ROLLOUT = ("status", "history")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def k8s_rollout(action: str, resource: str, name: str, namespace: str = "") -> str:
|
|
154
|
+
"""Show rollout status or history for a workload."""
|
|
155
|
+
if action not in _ALLOWED_ROLLOUT:
|
|
156
|
+
return f"Error: rollout action must be one of {_ALLOWED_ROLLOUT} (read-only)"
|
|
157
|
+
namespace = _resolve_ns(namespace)
|
|
158
|
+
args = ["rollout", action, f"{resource}/{name}"]
|
|
159
|
+
if namespace:
|
|
160
|
+
args += ["-n", namespace]
|
|
161
|
+
return _kubectl(args)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
TOOLS: dict[str, dict] = {
|
|
165
|
+
"k8s_get": {
|
|
166
|
+
"fn": k8s_get,
|
|
167
|
+
"desc": "Get Kubernetes resources (pods, deployments, services, nodes, events, etc.). Read-only — only `get` is executed. Use when you need a resource list or its current state.",
|
|
168
|
+
"params": {
|
|
169
|
+
"resource": {
|
|
170
|
+
"type": "str",
|
|
171
|
+
"required": True,
|
|
172
|
+
"desc": "resource type (e.g. pods, deployments, svc, nodes, events, ingress)",
|
|
173
|
+
},
|
|
174
|
+
"name": {
|
|
175
|
+
"type": "str",
|
|
176
|
+
"required": False,
|
|
177
|
+
"desc": "specific resource name",
|
|
178
|
+
},
|
|
179
|
+
"namespace": {
|
|
180
|
+
"type": "str",
|
|
181
|
+
"required": False,
|
|
182
|
+
"desc": "Kubernetes namespace (all namespaces when omitted)",
|
|
183
|
+
},
|
|
184
|
+
"output": {
|
|
185
|
+
"type": "str",
|
|
186
|
+
"required": False,
|
|
187
|
+
"desc": "output format: wide, yaml, json, name",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"k8s_describe": {
|
|
192
|
+
"fn": k8s_describe,
|
|
193
|
+
"desc": "Describe a Kubernetes resource in detail. Read-only — only `describe` is executed. Use when `get` isn't enough — events, conditions, container status.",
|
|
194
|
+
"params": {
|
|
195
|
+
"resource": {
|
|
196
|
+
"type": "str",
|
|
197
|
+
"required": True,
|
|
198
|
+
"desc": "resource type (e.g. pod, deployment, service, node)",
|
|
199
|
+
},
|
|
200
|
+
"name": {
|
|
201
|
+
"type": "str",
|
|
202
|
+
"required": False,
|
|
203
|
+
"desc": "specific resource name",
|
|
204
|
+
},
|
|
205
|
+
"namespace": {
|
|
206
|
+
"type": "str",
|
|
207
|
+
"required": False,
|
|
208
|
+
"desc": "Kubernetes namespace",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
"k8s_logs": {
|
|
213
|
+
"fn": k8s_logs,
|
|
214
|
+
"desc": "Get logs from a pod. Read-only — only `logs` is executed. Use when a pod is misbehaving or you need to inspect its output.",
|
|
215
|
+
"params": {
|
|
216
|
+
"pod": {"type": "str", "required": True, "desc": "pod name"},
|
|
217
|
+
"namespace": {
|
|
218
|
+
"type": "str",
|
|
219
|
+
"required": False,
|
|
220
|
+
"desc": "Kubernetes namespace",
|
|
221
|
+
},
|
|
222
|
+
"tail": {
|
|
223
|
+
"type": "int",
|
|
224
|
+
"required": False,
|
|
225
|
+
"desc": "last N lines (0 = all)",
|
|
226
|
+
},
|
|
227
|
+
"container": {
|
|
228
|
+
"type": "str",
|
|
229
|
+
"required": False,
|
|
230
|
+
"desc": "specific container name within the pod",
|
|
231
|
+
},
|
|
232
|
+
"since": {
|
|
233
|
+
"type": "str",
|
|
234
|
+
"required": False,
|
|
235
|
+
"desc": "fetch logs since duration (e.g. 1h, 10m, 30s)",
|
|
236
|
+
},
|
|
237
|
+
"previous": {
|
|
238
|
+
"type": "bool",
|
|
239
|
+
"required": False,
|
|
240
|
+
"desc": "fetch logs from the previous (restarted) container",
|
|
241
|
+
},
|
|
242
|
+
"all_containers": {
|
|
243
|
+
"type": "bool",
|
|
244
|
+
"required": False,
|
|
245
|
+
"desc": "fetch logs from all containers in the pod",
|
|
246
|
+
},
|
|
247
|
+
"timeout": {
|
|
248
|
+
"type": "int",
|
|
249
|
+
"required": False,
|
|
250
|
+
"desc": "SSH/kubectl timeout in seconds",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
"k8s_top": {
|
|
255
|
+
"fn": k8s_top,
|
|
256
|
+
"desc": "Show resource (CPU/memory) usage. Read-only — only `top` is executed. Use to find nodes or pods under pressure.",
|
|
257
|
+
"params": {
|
|
258
|
+
"resource_type": {
|
|
259
|
+
"type": "str",
|
|
260
|
+
"required": True,
|
|
261
|
+
"desc": "node or pod",
|
|
262
|
+
},
|
|
263
|
+
"namespace": {
|
|
264
|
+
"type": "str",
|
|
265
|
+
"required": False,
|
|
266
|
+
"desc": "namespace (only applies to pod)",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
"k8s_cluster_info": {
|
|
271
|
+
"fn": k8s_cluster_info,
|
|
272
|
+
"desc": "Show cluster info (master endpoint, services). Read-only.",
|
|
273
|
+
"params": {},
|
|
274
|
+
},
|
|
275
|
+
"k8s_api_resources": {
|
|
276
|
+
"fn": k8s_api_resources,
|
|
277
|
+
"desc": "List available API resources on the cluster. Read-only.",
|
|
278
|
+
"params": {},
|
|
279
|
+
},
|
|
280
|
+
"k8s_explain": {
|
|
281
|
+
"fn": k8s_explain,
|
|
282
|
+
"desc": "Get documentation for a Kubernetes resource type. Read-only.",
|
|
283
|
+
"params": {
|
|
284
|
+
"resource": {
|
|
285
|
+
"type": "str",
|
|
286
|
+
"required": True,
|
|
287
|
+
"desc": "resource type to explain (e.g. pod, deployment, service)",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
"k8s_version": {
|
|
292
|
+
"fn": k8s_version,
|
|
293
|
+
"desc": "Show Kubernetes version info (client and server). Read-only.",
|
|
294
|
+
"params": {},
|
|
295
|
+
},
|
|
296
|
+
"k8s_events": {
|
|
297
|
+
"fn": k8s_events,
|
|
298
|
+
"desc": "List Kubernetes events, optionally filtered by namespace or field-selector (e.g. type=Warning). Read-only — only `get events` is executed. Use when investigating failures.",
|
|
299
|
+
"params": {
|
|
300
|
+
"namespace": {
|
|
301
|
+
"type": "str",
|
|
302
|
+
"required": False,
|
|
303
|
+
"desc": "Kubernetes namespace (all namespaces when omitted)",
|
|
304
|
+
},
|
|
305
|
+
"field_selector": {
|
|
306
|
+
"type": "str",
|
|
307
|
+
"required": False,
|
|
308
|
+
"desc": "field selector, e.g. type=Warning or reason=Failed",
|
|
309
|
+
},
|
|
310
|
+
"output": {
|
|
311
|
+
"type": "str",
|
|
312
|
+
"required": False,
|
|
313
|
+
"desc": "output format: wide, yaml, json",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
"k8s_rollout": {
|
|
318
|
+
"fn": k8s_rollout,
|
|
319
|
+
"desc": "Show rollout status or history for a workload (deployment/statefulset/daemonset). Read-only — status/history only.",
|
|
320
|
+
"params": {
|
|
321
|
+
"action": {
|
|
322
|
+
"type": "str",
|
|
323
|
+
"required": True,
|
|
324
|
+
"desc": "status or history (read-only; other actions are rejected)",
|
|
325
|
+
},
|
|
326
|
+
"resource": {
|
|
327
|
+
"type": "str",
|
|
328
|
+
"required": True,
|
|
329
|
+
"desc": "workload type, e.g. deployment",
|
|
330
|
+
},
|
|
331
|
+
"name": {"type": "str", "required": True, "desc": "workload name"},
|
|
332
|
+
"namespace": {
|
|
333
|
+
"type": "str",
|
|
334
|
+
"required": False,
|
|
335
|
+
"desc": "Kubernetes namespace",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def serve() -> None:
|
|
343
|
+
"""Run the Kubernetes diagnostics MCP stdio server."""
|
|
344
|
+
serve_protocol(TOOLS, SERVER_NAME)
|
wl_cli/mcp/protocol.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Minimal JSON-RPC over stdio protocol for wl MCP servers.
|
|
2
|
+
|
|
3
|
+
The two MCP servers share this implementation. Each server only supplies its
|
|
4
|
+
tool registry and wire name; schema derivation, argument coercion, response
|
|
5
|
+
rendering, and method dispatch live here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
14
|
+
SERVER_VERSION = "1.0.0"
|
|
15
|
+
|
|
16
|
+
Handler = Callable[[dict[str, Any]], str]
|
|
17
|
+
|
|
18
|
+
_JSON_TYPES = {"str": "string", "int": "integer", "bool": "boolean"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _coerce(args: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
"""Coerce arguments according to their declared parameter types."""
|
|
23
|
+
coerced: dict[str, Any] = {}
|
|
24
|
+
for key, value in args.items():
|
|
25
|
+
kind = params.get(key, {}).get("type")
|
|
26
|
+
if kind == "int":
|
|
27
|
+
coerced[key] = int(value)
|
|
28
|
+
elif (
|
|
29
|
+
kind == "bool"
|
|
30
|
+
and isinstance(value, str)
|
|
31
|
+
and value.lower()
|
|
32
|
+
in (
|
|
33
|
+
"true",
|
|
34
|
+
"false",
|
|
35
|
+
)
|
|
36
|
+
):
|
|
37
|
+
coerced[key] = value.lower() == "true"
|
|
38
|
+
else:
|
|
39
|
+
coerced[key] = value
|
|
40
|
+
return coerced
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render(result: Any) -> str:
|
|
44
|
+
"""Render a tool result as text without escaping non-ASCII characters."""
|
|
45
|
+
if isinstance(result, str):
|
|
46
|
+
return result
|
|
47
|
+
return json.dumps(result, indent=2, ensure_ascii=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _error(msg_id: Any, code: int, message: str) -> str:
|
|
51
|
+
"""Build a JSON-RPC error response."""
|
|
52
|
+
return json.dumps(
|
|
53
|
+
{"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_handler(tools: dict[str, dict[str, Any]], server_name: str) -> Handler:
|
|
58
|
+
"""Build a protocol handler bound to a tool registry and server name."""
|
|
59
|
+
|
|
60
|
+
def _handle(msg: dict[str, Any]) -> str:
|
|
61
|
+
method = msg.get("method", "")
|
|
62
|
+
msg_id = msg.get("id")
|
|
63
|
+
|
|
64
|
+
if method == "initialize":
|
|
65
|
+
return json.dumps(
|
|
66
|
+
{
|
|
67
|
+
"jsonrpc": "2.0",
|
|
68
|
+
"id": msg_id,
|
|
69
|
+
"result": {
|
|
70
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
71
|
+
"serverInfo": {"name": server_name, "version": SERVER_VERSION},
|
|
72
|
+
"capabilities": {"tools": {}},
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
if method == "notifications/initialized":
|
|
77
|
+
return ""
|
|
78
|
+
if method == "shutdown":
|
|
79
|
+
return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": None})
|
|
80
|
+
|
|
81
|
+
if method == "tools/list":
|
|
82
|
+
return json.dumps(
|
|
83
|
+
{
|
|
84
|
+
"jsonrpc": "2.0",
|
|
85
|
+
"id": msg_id,
|
|
86
|
+
"result": {
|
|
87
|
+
"tools": [
|
|
88
|
+
{
|
|
89
|
+
"name": name,
|
|
90
|
+
"description": spec["desc"],
|
|
91
|
+
"inputSchema": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
key: {
|
|
95
|
+
"type": _JSON_TYPES[decl["type"]],
|
|
96
|
+
"description": decl["desc"],
|
|
97
|
+
}
|
|
98
|
+
for key, decl in spec["params"].items()
|
|
99
|
+
},
|
|
100
|
+
"required": [
|
|
101
|
+
key
|
|
102
|
+
for key, decl in spec["params"].items()
|
|
103
|
+
if decl["required"]
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
for name, spec in tools.items()
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if method == "tools/call":
|
|
114
|
+
tool_name = msg["params"]["name"]
|
|
115
|
+
args = msg["params"].get("arguments", {})
|
|
116
|
+
if tool_name not in tools:
|
|
117
|
+
return _error(msg_id, -32601, f"Unknown tool: {tool_name}")
|
|
118
|
+
try:
|
|
119
|
+
result = tools[tool_name]["fn"](
|
|
120
|
+
**_coerce(args, tools[tool_name]["params"])
|
|
121
|
+
)
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
return _error(msg_id, -32603, str(exc))
|
|
124
|
+
return json.dumps(
|
|
125
|
+
{
|
|
126
|
+
"jsonrpc": "2.0",
|
|
127
|
+
"id": msg_id,
|
|
128
|
+
"result": {"content": [{"type": "text", "text": _render(result)}]},
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return _error(msg_id, -32601, f"Unknown method: {method}")
|
|
133
|
+
|
|
134
|
+
return _handle
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def serve(tools: dict[str, dict[str, Any]], server_name: str) -> None:
|
|
138
|
+
"""Run the JSON-RPC line protocol until stdin reaches EOF.
|
|
139
|
+
|
|
140
|
+
Malformed JSON receives a ``-32700`` response and does not terminate the
|
|
141
|
+
loop, so clients can recover and send the next request.
|
|
142
|
+
"""
|
|
143
|
+
handle = make_handler(tools, server_name)
|
|
144
|
+
for raw_line in sys.stdin:
|
|
145
|
+
line = raw_line.strip()
|
|
146
|
+
if not line:
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
message = json.loads(line)
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
response = _error(None, -32700, "Parse error")
|
|
152
|
+
else:
|
|
153
|
+
if not isinstance(message, dict):
|
|
154
|
+
response = _error(None, -32600, "Invalid request")
|
|
155
|
+
else:
|
|
156
|
+
response = handle(message)
|
|
157
|
+
if response:
|
|
158
|
+
sys.stdout.write(response + "\n")
|
|
159
|
+
sys.stdout.flush()
|
wl_cli/platform.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Platform reporting seam for ``wl`` (issue #226 / wlcc #321).
|
|
2
|
+
|
|
3
|
+
**Scope (#226):** this module provides the shared computation of the platform
|
|
4
|
+
matrix fields and the payload shape agreed in wlcc #321. Wiring it into the
|
|
5
|
+
CLI is deliberately out of scope: ``wl env check`` is delivered by #221 and
|
|
6
|
+
will be the first consumer. No command in this repository calls
|
|
7
|
+
``platform_payload()`` yet.
|
|
8
|
+
|
|
9
|
+
The distribution is a pure-Python wheel (``py3-none-any``): every Python
|
|
10
|
+
3.11+ platform can install and run ``wl`` without a Rust toolchain. The native
|
|
11
|
+
extension is optional and diagnostic only, so it never decides ``supported``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import glob
|
|
17
|
+
import sysconfig
|
|
18
|
+
from platform import libc_ver, machine, python_version, system
|
|
19
|
+
|
|
20
|
+
MIN_PYTHON: tuple[int, int] = (3, 11)
|
|
21
|
+
|
|
22
|
+
#: ``platform.system()`` -> report prefix. Windows is shortened to ``win`` so
|
|
23
|
+
#: the identifiers read like the wheel tags users see (``win-amd64``).
|
|
24
|
+
_OS_PREFIXES = {"darwin": "darwin", "linux": "linux", "windows": "win"}
|
|
25
|
+
|
|
26
|
+
#: musl's dynamic loader; ``platform.libc_ver()`` does not recognise musl on
|
|
27
|
+
#: real musl systems, so the loader on disk is the fallback probe.
|
|
28
|
+
_MUSL_LOADER_GLOB = "/lib/ld-musl-*.so.1"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _os_name() -> str:
|
|
32
|
+
"""Return the report's OS component (``darwin``/``linux``/``win``)."""
|
|
33
|
+
raw = system().lower()
|
|
34
|
+
return _OS_PREFIXES.get(raw, raw)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _arch_name() -> str:
|
|
38
|
+
"""Return the CPU architecture as reported by ``platform.machine()``.
|
|
39
|
+
|
|
40
|
+
Normalising here would lose information: ``uname -m`` already yields the
|
|
41
|
+
names users expect per OS (``arm64`` on macOS, ``aarch64`` on Linux,
|
|
42
|
+
``AMD64``/``ARM64`` on Windows).
|
|
43
|
+
"""
|
|
44
|
+
return machine().lower() or "unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _host_gnu_type() -> str:
|
|
48
|
+
"""Return the interpreter's build triple (``HOST_GNU_TYPE``), lower-cased.
|
|
49
|
+
|
|
50
|
+
CPython builds for glibc report triples ending in ``-gnu`` and musl builds
|
|
51
|
+
end in ``-musl``; this is the reliable in-process musl signal.
|
|
52
|
+
"""
|
|
53
|
+
value = sysconfig.get_config_var("HOST_GNU_TYPE")
|
|
54
|
+
return str(value).lower() if value else ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _musl_loader_present() -> bool:
|
|
58
|
+
"""Return whether a musl dynamic loader exists on disk."""
|
|
59
|
+
return bool(glob.glob(_MUSL_LOADER_GLOB))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _libc_name() -> str:
|
|
63
|
+
"""Return the Linux userland's libc: ``glibc``, ``musl`` or ``unknown``.
|
|
64
|
+
|
|
65
|
+
``platform.libc_ver()`` only reliably recognises glibc; on real musl
|
|
66
|
+
systems it returns ``("", "")`` (or a generic ``libc``), which is why the
|
|
67
|
+
build triple and the loader on disk are probed first.
|
|
68
|
+
"""
|
|
69
|
+
host_type = _host_gnu_type()
|
|
70
|
+
if "musl" in host_type:
|
|
71
|
+
return "musl"
|
|
72
|
+
if "gnu" in host_type:
|
|
73
|
+
return "glibc"
|
|
74
|
+
if _musl_loader_present():
|
|
75
|
+
return "musl"
|
|
76
|
+
lib, _version = libc_ver()
|
|
77
|
+
lib = lib.lower()
|
|
78
|
+
if lib in {"glibc", "musl"}:
|
|
79
|
+
return lib
|
|
80
|
+
return "unknown"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _platform_identifier() -> str:
|
|
84
|
+
"""Build ``darwin-arm64`` / ``linux-x86_64-glibc`` / ``win-amd64``."""
|
|
85
|
+
os_name = _os_name()
|
|
86
|
+
arch = _arch_name()
|
|
87
|
+
if os_name == "linux":
|
|
88
|
+
return f"linux-{arch}-{_libc_name()}"
|
|
89
|
+
return f"{os_name}-{arch}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _python_supported(version: str) -> bool:
|
|
93
|
+
"""Return whether *version* meets the pure-Python requirement (3.11+)."""
|
|
94
|
+
parts = version.split(".")
|
|
95
|
+
try:
|
|
96
|
+
major, minor = int(parts[0]), int(parts[1])
|
|
97
|
+
except (IndexError, ValueError):
|
|
98
|
+
return False
|
|
99
|
+
return (major, minor) >= MIN_PYTHON
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _native_extension_present() -> bool:
|
|
103
|
+
"""Import the optional extension flag lazily to avoid import cycles."""
|
|
104
|
+
from .lib.rust_utils import HAS_RUST_EXTENSION
|
|
105
|
+
|
|
106
|
+
return bool(HAS_RUST_EXTENSION)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def platform_payload() -> dict[str, str | bool | None]:
|
|
110
|
+
"""Return the platform matrix fields defined by wlcc #321.
|
|
111
|
+
|
|
112
|
+
This is the seam #221's ``wl env check`` will serialise; it is not
|
|
113
|
+
consumed by any CLI command in this ticket.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
``platform``: ``os-arch[-libc]`` identifier (e.g. ``linux-x86_64-glibc``).
|
|
117
|
+
``python``: ``X.Y.Z`` interpreter version.
|
|
118
|
+
``native_extension``: whether the optional Rust extension imported;
|
|
119
|
+
diagnostic only, it never gates support.
|
|
120
|
+
``supported``: true for Python >= 3.11 (the pure-Python path).
|
|
121
|
+
``reason``: ``None`` when supported, else a readable explanation.
|
|
122
|
+
"""
|
|
123
|
+
version = python_version()
|
|
124
|
+
supported = _python_supported(version)
|
|
125
|
+
reason = (
|
|
126
|
+
None
|
|
127
|
+
if supported
|
|
128
|
+
else (
|
|
129
|
+
f"Python {version} is not supported: wl requires Python 3.11 or "
|
|
130
|
+
"newer (pure-Python wheel; no native extension is required)."
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return {
|
|
134
|
+
"platform": _platform_identifier(),
|
|
135
|
+
"python": version,
|
|
136
|
+
"native_extension": _native_extension_present(),
|
|
137
|
+
"supported": supported,
|
|
138
|
+
"reason": reason,
|
|
139
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Quality domain (issue #220).
|
|
2
|
+
|
|
3
|
+
The quality domain's **existing implementation** lives in
|
|
4
|
+
:mod:`wl_cli.commands` (``test`` / ``format`` / ``lint`` / ``init`` /
|
|
5
|
+
``git-flow`` and the other command modules) with its Rust acceleration in
|
|
6
|
+
:mod:`wl_cli.lib`. This ticket deliberately does **not** relocate those
|
|
7
|
+
modules: the move is out of scope, and the user-visible ``wl`` surface must
|
|
8
|
+
not change while it happens. This package only names the domain so later
|
|
9
|
+
splits have an anchor.
|
|
10
|
+
|
|
11
|
+
Dependency direction: this package (like ``wl_cli.commands`` and
|
|
12
|
+
``wl_cli.lib``) may not import ``wl_cli.cc`` or ``wl_cli.mcp`` — enforced by
|
|
13
|
+
``tests/test_domain_import_direction.py``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
DOMAIN = "quality"
|
|
17
|
+
|
|
18
|
+
__all__ = ["DOMAIN"]
|