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
wl_cli/mcp/gitlab.py ADDED
@@ -0,0 +1,553 @@
1
+ """GitLab MCP tools and API v4 client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import socket
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
+ from typing import Any, cast
16
+
17
+ from .protocol import serve as serve_protocol
18
+
19
+ try:
20
+ import pwd
21
+ except ImportError: # pragma: no cover - Windows
22
+ pwd = None # type: ignore[assignment]
23
+
24
+ GITLAB_URL = os.environ.get("GITLAB_URL", "https://gitlab.gewinnrobo.com")
25
+ SERVER_NAME = "gitlab"
26
+
27
+
28
+ def _cred_user() -> str:
29
+ """Return the OS account name used to derive the token.enc passphrase."""
30
+ if pwd is not None:
31
+ return pwd.getpwuid(os.getuid()).pw_name
32
+ try:
33
+ import ctypes
34
+
35
+ windll = getattr(ctypes, "windll", None)
36
+ if windll is not None:
37
+ buf = ctypes.create_unicode_buffer(256)
38
+ size = ctypes.c_ulong(len(buf))
39
+ if windll.advapi32.GetUserNameW(buf, ctypes.byref(size)):
40
+ return buf.value
41
+ except Exception:
42
+ pass
43
+ return ""
44
+
45
+
46
+ def _get_token() -> str:
47
+ """Read GITLAB_TOKEN from the environment or decrypt the wlcc token file."""
48
+ token = os.environ.get("GITLAB_TOKEN", "")
49
+ if token:
50
+ return token
51
+
52
+ config = os.path.expanduser("~/.config/wlcc/token.enc")
53
+ if not os.path.exists(config):
54
+ return ""
55
+
56
+ key = f"{socket.gethostname().split('.')[0]}-{_cred_user()}"
57
+ try:
58
+ with open(config, "rb") as token_file:
59
+ ciphertext = token_file.read()
60
+ result = subprocess.run(
61
+ ["openssl", "enc", "-d", "-aes-256-cbc", "-pbkdf2", "-pass", f"pass:{key}"],
62
+ input=ciphertext,
63
+ capture_output=True,
64
+ timeout=5,
65
+ )
66
+ return result.stdout.decode().strip() if result.returncode == 0 else ""
67
+ except FileNotFoundError:
68
+ print(
69
+ f"Warning: failed to decrypt {config}: openssl is not available",
70
+ file=sys.stderr,
71
+ )
72
+ return ""
73
+ except Exception as exc:
74
+ print(f"Warning: failed to decrypt {config}: {exc}", file=sys.stderr)
75
+ return ""
76
+
77
+
78
+ GITLAB_TOKEN = _get_token()
79
+
80
+
81
+ def _api(path: str, method: str = "GET", data: dict | None = None) -> Any:
82
+ """Call GitLab API v4 and return the parsed JSON response."""
83
+ url = f"{GITLAB_URL}/api/v4/{path.lstrip('/')}"
84
+ headers = {"PRIVATE-TOKEN": GITLAB_TOKEN, "Content-Type": "application/json"}
85
+ body = json.dumps(data).encode() if data else None
86
+ req = urllib.request.Request(url, data=body, headers=headers, method=method)
87
+ try:
88
+ resp = urllib.request.urlopen(req, timeout=30)
89
+ return json.loads(resp.read())
90
+ except urllib.error.HTTPError as exc:
91
+ err = exc.read().decode()[:500]
92
+ raise Exception(f"HTTP {exc.code}: {err}") from exc
93
+
94
+
95
+ def _api_raw(path: str) -> str:
96
+ """Fetch a raw text response, used for repository files and job logs."""
97
+ url = f"{GITLAB_URL}/api/v4/{path.lstrip('/')}"
98
+ headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
99
+ req = urllib.request.Request(url, headers=headers)
100
+ try:
101
+ resp = urllib.request.urlopen(req, timeout=30)
102
+ return cast("str", resp.read().decode())
103
+ except urllib.error.HTTPError as exc:
104
+ err = exc.read().decode()[:500]
105
+ raise Exception(f"HTTP {exc.code}: {err}") from exc
106
+
107
+
108
+ def list_projects(page: int = 1, per_page: int = 20, search: str = "") -> list[dict]:
109
+ """List GitLab projects, with optional search."""
110
+ query = f"membership=true&per_page={per_page}&page={page}"
111
+ if search:
112
+ query += f"&search={urllib.parse.quote(search)}"
113
+ return cast("list[dict]", _api(f"projects?{query}"))
114
+
115
+
116
+ def get_file(project_id: int, file_path: str, ref: str = "main") -> dict:
117
+ """Read a file from a repository."""
118
+ content = _api_raw(
119
+ f"projects/{project_id}/repository/files/{urllib.parse.quote(file_path, safe='')}/raw?ref={ref}"
120
+ )
121
+ return {"path": file_path, "ref": ref, "content": content}
122
+
123
+
124
+ def update_file(
125
+ project_id: int,
126
+ file_path: str,
127
+ content: str,
128
+ commit_message: str,
129
+ branch: str = "main",
130
+ ) -> dict:
131
+ """Create or update a file in a repository."""
132
+ result = _api(
133
+ f"projects/{project_id}/repository/files/{urllib.parse.quote(file_path, safe='')}",
134
+ "PUT",
135
+ {"branch": branch, "content": content, "commit_message": commit_message},
136
+ )
137
+ return {
138
+ "status": "updated",
139
+ "path": result.get("file_path", file_path),
140
+ "branch": branch,
141
+ }
142
+
143
+
144
+ def list_project_files(project_id: int, path: str = "") -> list[dict]:
145
+ """List files in a repository directory."""
146
+ query = "per_page=50&ref=main"
147
+ if path:
148
+ query += f"&path={urllib.parse.quote(path)}"
149
+ files = _api(f"projects/{project_id}/repository/tree?{query}")
150
+ return [{"name": item["name"], "type": item["type"]} for item in files]
151
+
152
+
153
+ def list_merge_requests(project_id: int, state: str = "opened") -> list[dict]:
154
+ """List merge requests for a project."""
155
+ merge_requests = _api(
156
+ f"projects/{project_id}/merge_requests?state={state}&per_page=20"
157
+ )
158
+ return [
159
+ {
160
+ "iid": merge_request["iid"],
161
+ "title": merge_request["title"],
162
+ "state": merge_request["state"],
163
+ "author": merge_request["author"]["username"],
164
+ }
165
+ for merge_request in merge_requests
166
+ ]
167
+
168
+
169
+ def trigger_pipeline(project_id: int, ref: str = "main") -> dict:
170
+ """Trigger a new CI/CD pipeline."""
171
+ result = _api(f"projects/{project_id}/pipeline", "POST", {"ref": ref})
172
+ return {
173
+ "status": "triggered",
174
+ "pipeline_id": result["id"],
175
+ "ref": ref,
176
+ "web_url": result.get(
177
+ "web_url", f"{GITLAB_URL}/projects/{project_id}/pipelines/{result['id']}"
178
+ ),
179
+ }
180
+
181
+
182
+ def list_pipelines(project_id: int, per_page: int = 10, scope: str = "") -> list[dict]:
183
+ """List recent pipelines for a project."""
184
+ query = f"per_page={min(per_page, 50)}"
185
+ if scope:
186
+ query += f"&scope={scope}"
187
+ return cast("list[dict]", _api(f"projects/{project_id}/pipelines?{query}"))
188
+
189
+
190
+ def get_pipeline(project_id: int, pipeline_id: int | None = None) -> dict:
191
+ """Get pipeline details, or the latest pipeline when no ID is supplied."""
192
+ if pipeline_id:
193
+ return cast("dict", _api(f"projects/{project_id}/pipelines/{pipeline_id}"))
194
+ pipelines = cast("list[dict]", _api(f"projects/{project_id}/pipelines?per_page=1"))
195
+ if pipelines:
196
+ return pipelines[0]
197
+ return {"status": "no pipelines"}
198
+
199
+
200
+ def cancel_pipeline(project_id: int, pipeline_id: int) -> dict:
201
+ """Cancel a running pipeline."""
202
+ result = _api(f"projects/{project_id}/pipelines/{pipeline_id}/cancel", "POST")
203
+ return {"status": "canceled", "pipeline_id": pipeline_id, "ref": result.get("ref")}
204
+
205
+
206
+ def retry_job(project_id: int, job_id: int) -> dict:
207
+ """Retry a failed job."""
208
+ result = _api(f"projects/{project_id}/jobs/{job_id}/retry", "POST")
209
+ return {"status": "retried", "job_id": job_id, "new_job_id": result.get("id")}
210
+
211
+
212
+ _ERROR_PATTERNS = (
213
+ "FAILED",
214
+ "Error",
215
+ "ERROR",
216
+ "E ",
217
+ "assert",
218
+ "AssertionError",
219
+ "Traceback",
220
+ "exit code",
221
+ "non-zero",
222
+ "cannot",
223
+ "not found",
224
+ "timeout",
225
+ )
226
+
227
+
228
+ def get_job_log(
229
+ project_id: int, job_id: int, tail: int = 50, error_only: bool = False
230
+ ) -> dict:
231
+ """Get a job trace, optionally filtered to likely error lines."""
232
+ log = _api_raw(f"projects/{project_id}/jobs/{job_id}/trace")
233
+ lines = log.split("\n")
234
+
235
+ if error_only:
236
+ pattern = re.compile(
237
+ r"(?i)(FAILED|Error|ERROR|E |assert|AssertionError|Traceback|except|exit code|non-zero)"
238
+ )
239
+ matched = [line for line in lines if pattern.search(line)]
240
+ lines = matched if matched else lines[-20:]
241
+
242
+ if tail and len(lines) > tail:
243
+ lines = lines[-tail:]
244
+
245
+ return {
246
+ "job_id": job_id,
247
+ "total_lines": len(log.split("\n")),
248
+ "returned_lines": len(lines),
249
+ "log": "\n".join(lines),
250
+ }
251
+
252
+
253
+ def wait_for_pipeline(
254
+ project_id: int,
255
+ pipeline_id: int,
256
+ timeout_min: int = 10,
257
+ ) -> dict:
258
+ """Poll a pipeline with adaptive intervals until it reaches a terminal state."""
259
+ schedule = [(30, 5), (120, 15), (float("inf"), 30)]
260
+ deadline = time.time() + timeout_min * 60
261
+ start = time.time()
262
+ polls = 0
263
+
264
+ while time.time() < deadline:
265
+ elapsed = time.time() - start
266
+ interval = next(seconds for maximum, seconds in schedule if elapsed <= maximum)
267
+ time.sleep(interval)
268
+ polls += 1
269
+
270
+ pipeline = _api(f"projects/{project_id}/pipelines/{pipeline_id}")
271
+ status = pipeline.get("status", "unknown")
272
+ elapsed = round(time.time() - start, 1)
273
+
274
+ if status not in ("success", "failed", "canceled"):
275
+ continue
276
+
277
+ jobs = _api(f"projects/{project_id}/pipelines/{pipeline_id}/jobs")
278
+ job_summary = [
279
+ {
280
+ "id": job["id"],
281
+ "name": job["name"],
282
+ "stage": job["stage"],
283
+ "status": job["status"],
284
+ "duration_sec": job.get("duration"),
285
+ }
286
+ for job in jobs
287
+ ]
288
+
289
+ result: dict[str, Any] = {
290
+ "status": status,
291
+ "pipeline_id": pipeline_id,
292
+ "duration_sec": elapsed,
293
+ "polls": polls,
294
+ "jobs": job_summary,
295
+ "failed_jobs": [],
296
+ }
297
+
298
+ for job in jobs:
299
+ if job["status"] != "failed":
300
+ continue
301
+ try:
302
+ raw_log = _api_raw(f"projects/{project_id}/jobs/{job['id']}/trace")
303
+ lines = raw_log.split("\n")
304
+ error_lines = [
305
+ line
306
+ for line in lines
307
+ if re.search(r"(?i)" + "|".join(_ERROR_PATTERNS), line)
308
+ ]
309
+ if not error_lines:
310
+ error_lines = lines[-30:]
311
+ result["failed_jobs"].append(
312
+ {
313
+ "name": job["name"],
314
+ "stage": job["stage"],
315
+ "log_total_lines": len(lines),
316
+ "log_errors": "\n".join(error_lines[-50:]),
317
+ }
318
+ )
319
+ except Exception as exc:
320
+ result["failed_jobs"].append(
321
+ {
322
+ "name": job["name"],
323
+ "stage": job["stage"],
324
+ "log_errors": f"(failed to fetch: {exc})",
325
+ }
326
+ )
327
+
328
+ return result
329
+
330
+ return {
331
+ "status": "timeout",
332
+ "pipeline_id": pipeline_id,
333
+ "duration_sec": round(time.time() - start, 1),
334
+ "polls": polls,
335
+ "jobs": [],
336
+ "failed_jobs": [],
337
+ }
338
+
339
+
340
+ TOOLS: dict[str, dict] = {
341
+ "list_projects": {
342
+ "fn": list_projects,
343
+ "desc": "List GitLab projects with optional search",
344
+ "params": {
345
+ "page": {"type": "int", "required": False, "desc": "page number"},
346
+ "per_page": {"type": "int", "required": False, "desc": "results per page"},
347
+ "search": {
348
+ "type": "str",
349
+ "required": False,
350
+ "desc": "search by project name",
351
+ },
352
+ },
353
+ },
354
+ "get_file": {
355
+ "fn": get_file,
356
+ "desc": "Read a file from a repository",
357
+ "params": {
358
+ "project_id": {
359
+ "type": "int",
360
+ "required": True,
361
+ "desc": "GitLab project ID",
362
+ },
363
+ "file_path": {
364
+ "type": "str",
365
+ "required": True,
366
+ "desc": "path in the repository",
367
+ },
368
+ "ref": {"type": "str", "required": False, "desc": "branch/tag name"},
369
+ },
370
+ },
371
+ "update_file": {
372
+ "fn": update_file,
373
+ "desc": "Create or update a file in a repository",
374
+ "params": {
375
+ "project_id": {
376
+ "type": "int",
377
+ "required": True,
378
+ "desc": "GitLab project ID",
379
+ },
380
+ "file_path": {
381
+ "type": "str",
382
+ "required": True,
383
+ "desc": "path in the repository",
384
+ },
385
+ "content": {"type": "str", "required": True, "desc": "file content"},
386
+ "commit_message": {
387
+ "type": "str",
388
+ "required": True,
389
+ "desc": "commit message",
390
+ },
391
+ "branch": {"type": "str", "required": False, "desc": "branch name"},
392
+ },
393
+ },
394
+ "list_project_files": {
395
+ "fn": list_project_files,
396
+ "desc": "List files in a repository directory",
397
+ "params": {
398
+ "project_id": {
399
+ "type": "int",
400
+ "required": True,
401
+ "desc": "GitLab project ID",
402
+ },
403
+ "path": {"type": "str", "required": False, "desc": "directory path"},
404
+ },
405
+ },
406
+ "list_merge_requests": {
407
+ "fn": list_merge_requests,
408
+ "desc": "List merge requests for a project",
409
+ "params": {
410
+ "project_id": {
411
+ "type": "int",
412
+ "required": True,
413
+ "desc": "GitLab project ID",
414
+ },
415
+ "state": {
416
+ "type": "str",
417
+ "required": False,
418
+ "desc": "opened / merged / closed / all",
419
+ },
420
+ },
421
+ },
422
+ "trigger_pipeline": {
423
+ "fn": trigger_pipeline,
424
+ "desc": "Trigger a new CI/CD pipeline on a branch",
425
+ "params": {
426
+ "project_id": {
427
+ "type": "int",
428
+ "required": True,
429
+ "desc": "GitLab project ID",
430
+ },
431
+ "ref": {"type": "str", "required": False, "desc": "branch/tag name"},
432
+ },
433
+ },
434
+ "get_pipeline": {
435
+ "fn": get_pipeline,
436
+ "desc": "Get pipeline status details (omitting pipeline_id returns latest)",
437
+ "params": {
438
+ "project_id": {
439
+ "type": "int",
440
+ "required": True,
441
+ "desc": "GitLab project ID",
442
+ },
443
+ "pipeline_id": {
444
+ "type": "int",
445
+ "required": False,
446
+ "desc": "pipeline ID (omit to get the latest)",
447
+ },
448
+ },
449
+ },
450
+ "list_pipelines": {
451
+ "fn": list_pipelines,
452
+ "desc": "List recent pipelines for a project",
453
+ "params": {
454
+ "project_id": {
455
+ "type": "int",
456
+ "required": True,
457
+ "desc": "GitLab project ID",
458
+ },
459
+ "per_page": {
460
+ "type": "int",
461
+ "required": False,
462
+ "desc": "results per page (max 50)",
463
+ },
464
+ "scope": {
465
+ "type": "str",
466
+ "required": False,
467
+ "desc": "filter: running/pending/finished/branches/tags",
468
+ },
469
+ },
470
+ },
471
+ "cancel_pipeline": {
472
+ "fn": cancel_pipeline,
473
+ "desc": "Cancel a running pipeline",
474
+ "params": {
475
+ "project_id": {
476
+ "type": "int",
477
+ "required": True,
478
+ "desc": "GitLab project ID",
479
+ },
480
+ "pipeline_id": {
481
+ "type": "int",
482
+ "required": True,
483
+ "desc": "pipeline ID to cancel",
484
+ },
485
+ },
486
+ },
487
+ "retry_job": {
488
+ "fn": retry_job,
489
+ "desc": "Retry a single failed job",
490
+ "params": {
491
+ "project_id": {
492
+ "type": "int",
493
+ "required": True,
494
+ "desc": "GitLab project ID",
495
+ },
496
+ "job_id": {"type": "int", "required": True, "desc": "job ID to retry"},
497
+ },
498
+ },
499
+ "get_job_log": {
500
+ "fn": get_job_log,
501
+ "desc": "Get job trace/log, smart-truncated to relevant errors",
502
+ "params": {
503
+ "project_id": {
504
+ "type": "int",
505
+ "required": True,
506
+ "desc": "GitLab project ID",
507
+ },
508
+ "job_id": {"type": "int", "required": True, "desc": "job ID"},
509
+ "tail": {
510
+ "type": "int",
511
+ "required": False,
512
+ "desc": "last N lines (0 = full log)",
513
+ },
514
+ "error_only": {
515
+ "type": "bool",
516
+ "required": False,
517
+ "desc": "only return lines matching error patterns",
518
+ },
519
+ },
520
+ },
521
+ "wait_for_pipeline": {
522
+ "fn": wait_for_pipeline,
523
+ "desc": "Poll pipeline with adaptive intervals until done. Returns structured result with job details and auto-fetched failure logs.",
524
+ "params": {
525
+ "project_id": {
526
+ "type": "int",
527
+ "required": True,
528
+ "desc": "GitLab project ID",
529
+ },
530
+ "pipeline_id": {
531
+ "type": "int",
532
+ "required": True,
533
+ "desc": "pipeline ID to poll",
534
+ },
535
+ "timeout_min": {
536
+ "type": "int",
537
+ "required": False,
538
+ "desc": "max wait time in minutes",
539
+ },
540
+ },
541
+ },
542
+ }
543
+
544
+
545
+ def serve() -> None:
546
+ """Validate credentials, then run the GitLab MCP stdio server."""
547
+ if not GITLAB_TOKEN:
548
+ print(
549
+ "ERROR: GITLAB_TOKEN not available (set env var or configure ~/.config/wlcc/token.enc)",
550
+ file=sys.stderr,
551
+ )
552
+ raise SystemExit(1)
553
+ serve_protocol(TOOLS, SERVER_NAME)