feat: initial iLSP project scaffolding
- Python LSP (pylsp + pylsp_i80 plugin): i80 pypi package completions - Bicep LSP (asyncio TCP proxy → Bicep.LangServer.dll): LRU module injection - Health HTTP endpoint (:2089) for Consul/Nomad checks - Startup catalog fetch from pypi-server.i80.dk + DevOpsMCP (no volume needed) - Multi-stage Dockerfile: downloads Bicep LS at build time, dotnet-runtime-8.0 + python3.12 - Nomad job: static TCP ports 2087/2088, health check on 2089 - Gitea Actions CI: build + push + deploy pipeline - Editor configs: Helix / nvim / LSP4IJ / VS Code
This commit is contained in:
105
ilsp/bicep_lsp/modules.py
Normal file
105
ilsp/bicep_lsp/modules.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
LRU Bicep module catalog — fetched from DevOpsMCP at startup.
|
||||
|
||||
Provides completion items for LRU-internal Bicep modules with
|
||||
higher sort priority than standard Azure modules.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEVOPS_MCP_URL = os.getenv("DEVOPS_MCP_URL", "https://devops-mcp.i80.dk")
|
||||
REFRESH_INTERVAL = 3600 # 1 hour
|
||||
|
||||
|
||||
class BicepModuleCatalog:
|
||||
"""Fetches and caches LRU Bicep modules from DevOpsMCP."""
|
||||
|
||||
_modules: list[dict[str, Any]] = []
|
||||
_last_refresh: float = 0
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
@classmethod
|
||||
async def get_modules(cls) -> list[dict[str, Any]]:
|
||||
if not cls._modules or time.time() - cls._last_refresh > REFRESH_INTERVAL:
|
||||
await cls._refresh()
|
||||
return cls._modules
|
||||
|
||||
@classmethod
|
||||
async def start_background_refresh(cls) -> None:
|
||||
asyncio.create_task(cls._refresh_loop())
|
||||
|
||||
@classmethod
|
||||
async def _refresh_loop(cls) -> None:
|
||||
while True:
|
||||
await cls._refresh()
|
||||
await asyncio.sleep(REFRESH_INTERVAL)
|
||||
|
||||
@classmethod
|
||||
async def _refresh(cls) -> None:
|
||||
async with cls._lock:
|
||||
try:
|
||||
modules = await cls._fetch_from_devops_mcp()
|
||||
cls._modules = modules
|
||||
cls._last_refresh = time.time()
|
||||
logger.info("Bicep module catalog refreshed: %d modules", len(modules))
|
||||
except Exception:
|
||||
logger.exception("Failed to refresh Bicep module catalog — using stale cache")
|
||||
|
||||
@classmethod
|
||||
async def _fetch_from_devops_mcp(cls) -> list[dict[str, Any]]:
|
||||
"""Call DevOpsMCP list_bicep_modules tool via HTTP."""
|
||||
url = f"{DEVOPS_MCP_URL}/call-tool"
|
||||
payload = {"tool": "list_bicep_modules", "arguments": {}}
|
||||
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"DevOpsMCP returned {resp.status}")
|
||||
data = await resp.json()
|
||||
|
||||
modules = []
|
||||
for m in data.get("modules", []):
|
||||
path = m.get("path", "")
|
||||
versions = m.get("versions", ["latest"])
|
||||
name = path.split("/")[-1] if "/" in path else path
|
||||
modules.append({
|
||||
"name": name,
|
||||
"path": path,
|
||||
"versions": versions,
|
||||
"latest": versions[-1] if versions else "latest",
|
||||
"registry": data.get("registry", "iactemplatereg.azurecr.io"),
|
||||
})
|
||||
|
||||
return modules
|
||||
|
||||
@classmethod
|
||||
def as_completion_items(cls) -> list[dict[str, Any]]:
|
||||
items = []
|
||||
for mod in cls._modules:
|
||||
ref = f"br/modules:{mod['path']}:{mod['latest']}"
|
||||
items.append({
|
||||
"label": mod["name"],
|
||||
"kind": 9, # Module
|
||||
"detail": f"LRU Bicep module — {mod['registry']}",
|
||||
"insertText": ref,
|
||||
"sortText": f"0_lru_{mod['name']}", # sorts above standard az modules
|
||||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": (
|
||||
f"**{mod['name']}** (LRU internal)\n\n"
|
||||
f"Registry: `{mod['registry']}`\n"
|
||||
f"Versions: {', '.join(mod['versions'])}\n\n"
|
||||
f"```bicep\nmodule {mod['name'].lower()} '{ref}' = {{\n"
|
||||
f" name: '{mod['name'].lower()}'\n params: {{}}\n}}\n```"
|
||||
),
|
||||
},
|
||||
})
|
||||
return items
|
||||
Reference in New Issue
Block a user