"""Render local matrix skill files with resolved CLI skill paths. Installed layout (one directory per matrix, so the skill's relative links to ``references/*.md`` and ``scripts/*.py`` resolve): ~/.cli-hub/matrix//SKILL.md ~/.cli-hub/matrix//references/... ~/.cli-hub/matrix//scripts/... Skill content source lookup chain: 1. Repo checkout: ``/cli-hub-matrix//`` (via ``skill_md``). 2. Bundled package data: ``cli_hub/_matrix_data//`` (shipped in wheels/sdists built from a checkout; absent in editable installs, which hit the checkout in step 1 instead). 3. Published URL: ``https://hkuds.github.io/CLI-Anything/matrix//SKILL.md`` (SKILL.md only; references/scripts stay remote and are linked from the rendered file). 4. Generated stub. """ import shutil import subprocess from importlib import metadata from pathlib import Path import requests from cli_hub.registry import get_cli MATRIX_SKILL_DIR = Path.home() / ".cli-hub" / "matrix" # Base URL where deploy-pages.yml publishes cli-hub-matrix/ content (main only). MATRIX_CONTENT_BASE_URL = "https://hkuds.github.io/CLI-Anything/matrix" # Package data dir bundled into wheels/sdists by cli-hub/setup.py. BUNDLED_MATRIX_DATA_DIR = Path(__file__).resolve().parent / "_matrix_data" # Asset directories co-installed beside the rendered SKILL.md. MATRIX_ASSET_SUBDIRS = ("references", "scripts") _COPY_IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo") def _find_repo_root(): """Find the repository root via git, falling back to parent traversal.""" try: result = subprocess.run( ["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, timeout=5, ) if result.returncode == 0: root = Path(result.stdout.strip()) if root.is_dir(): return root except (FileNotFoundError, subprocess.TimeoutExpired): pass # Fallback: walk up from this file looking for .git current = Path(__file__).resolve().parent for parent in [current] + list(current.parents): if (parent / ".git").exists(): return parent return None def get_rendered_matrix_skill_path(name): """Return the local rendered SKILL.md path for a matrix. Prefers the per-matrix directory layout (``/SKILL.md``); falls back to the legacy flat ``.SKILL.md`` file when only that exists, so pre-existing installs keep resolving until the next re-render. """ current = MATRIX_SKILL_DIR / name / "SKILL.md" legacy = MATRIX_SKILL_DIR / f"{name}.SKILL.md" if not current.exists() and legacy.exists(): return legacy return current def resolve_local_skill_path(cli): """Resolve an installed harness CLI's local SKILL.md path if possible.""" if cli.get("_source", "harness") != "harness": return None dist_name = cli.get("dist_name") or f"cli-anything-{cli['name']}" try: dist = metadata.distribution(dist_name) except metadata.PackageNotFoundError: return _fallback_repo_skill_path(cli) for file in dist.files or []: file_str = str(file).replace("\\", "/") if file_str.endswith("/skills/SKILL.md") or file_str.endswith("skills/SKILL.md"): return str(dist.locate_file(file).resolve()) return _fallback_repo_skill_path(cli) def _fallback_repo_skill_path(cli): """Use the repo-relative skill path when available in the current checkout.""" skill_ref = cli.get("skill_md") if not skill_ref or "://" in skill_ref or skill_ref.startswith("npx "): return None repo_root = _find_repo_root() if repo_root is None: return None candidate = repo_root / skill_ref if candidate.exists(): return str(candidate.resolve()) return None def render_matrix_skill_file(matrix_item, installed=None): """Write a local matrix SKILL.md with resolved member skill paths. Renders into ``MATRIX_SKILL_DIR//SKILL.md`` and co-installs the matrix content directory's ``references/`` and ``scripts/`` beside it so the skill's relative links resolve. Re-rendering is idempotent: asset directories are replaced wholesale on each render. """ name = matrix_item["name"] output_dir = MATRIX_SKILL_DIR / name output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / "SKILL.md" template_path, content_dir = _resolve_matrix_content_source(matrix_item) base_content = _load_matrix_skill_template(matrix_item, template_path).rstrip() copied = _copy_matrix_assets(content_dir, output_dir) if content_dir else [] extra = "" if not copied: extra = ( "\n\n## Reference Modules\n\n" "No local copy of this matrix's `references/` and `scripts/` was " "found; relative links above will not resolve locally. The " "published copies live under " f"{MATRIX_CONTENT_BASE_URL}/{name}/ (e.g. " f"{MATRIX_CONTENT_BASE_URL}/{name}/SKILL.md)." ) injected_section = _render_injected_section(matrix_item, installed or {}) output_path.write_text( f"{base_content}\n\n\n\n{injected_section}{extra}\n\n\n", encoding="utf-8", ) return output_path def _resolve_matrix_content_source(matrix_item): """Locate the matrix skill source as ``(template_path, content_dir)``. Tries the repo checkout first, then bundled package data. Either element may be ``None`` when nothing local is available (the template then falls back to the published URL or a stub). """ skill_ref = matrix_item.get("skill_md") if skill_ref and "://" not in skill_ref and not skill_ref.startswith("npx "): repo_root = _find_repo_root() if repo_root is not None: candidate = repo_root / skill_ref if candidate.exists(): return candidate, candidate.parent bundled = BUNDLED_MATRIX_DATA_DIR / matrix_item["name"] / "SKILL.md" if bundled.exists(): return bundled, bundled.parent return None, None def _copy_matrix_assets(content_dir, output_dir): """Copy references/ and scripts/ beside the rendered SKILL.md. Existing asset directories are removed first so re-installs are clean and stale files do not linger. ``__pycache__`` and ``*.pyc`` are excluded. Returns the list of subdirectories that were copied. """ copied = [] for subdir in MATRIX_ASSET_SUBDIRS: source = content_dir / subdir destination = output_dir / subdir if destination.exists(): shutil.rmtree(destination) if source.is_dir(): shutil.copytree(source, destination, ignore=_COPY_IGNORE) copied.append(subdir) return copied def _load_matrix_skill_template(matrix_item, template_path=None): """Load the matrix skill template via the lookup chain. Order: local source file (checkout or bundled data) -> published URL -> generated stub. """ if template_path is None: template_path, _ = _resolve_matrix_content_source(matrix_item) if template_path is not None: return template_path.read_text(encoding="utf-8") published = _fetch_published_matrix_skill(matrix_item["name"]) if published is not None: return published title = matrix_item.get("display_name", matrix_item["name"]) description = matrix_item.get("description", "") return ( f"# {title}\n\n" f"{description}\n\n" f"Install with `cli-hub matrix install {matrix_item['name']}`.\n\n" f"Full skill content (when published): " f"{MATRIX_CONTENT_BASE_URL}/{matrix_item['name']}/SKILL.md" ) def _fetch_published_matrix_skill(name): """Fetch the published SKILL.md for a matrix, or None on any failure.""" url = f"{MATRIX_CONTENT_BASE_URL}/{name}/SKILL.md" try: resp = requests.get(url, timeout=10) except requests.RequestException: return None if resp.status_code != 200 or not resp.text.strip(): return None return resp.text def _render_injected_section(matrix_item, installed): """Render the injected skill reference section.""" lines = [ "## Installed CLI Skills", "", "Generated by `cli-hub matrix install` from the current local environment.", "", "| CLI | Entry Point | Canonical Skill | Local Skill | Status |", "|---|---|---|---|---|", ] for cli_name in matrix_item.get("clis", []): cli = get_cli(cli_name) or {"name": cli_name, "entry_point": cli_name} canonical_skill = cli.get("skill_md") or "—" local_skill = resolve_local_skill_path(cli) or "—" status = "installed" if cli_name in installed else "not installed" lines.append( f"| `{cli_name}` | `{cli.get('entry_point', '—')}` | " f"{canonical_skill} | {local_skill} | {status} |" ) capability_tooling = _render_capability_tooling(matrix_item, installed) if capability_tooling: lines.append("") lines.append(capability_tooling) stage_tooling = _render_stage_tooling(matrix_item, installed) if stage_tooling: lines.append("") lines.append(stage_tooling) discovery = _render_discovery_section(matrix_item) if discovery: lines.append("") lines.append(discovery) return "\n".join(lines) def _provider_installed(provider, installed): """Return whether a CLI provider appears installed by cli-hub name.""" if provider.get("kind") not in {"harness-cli", "public-cli"}: return False name = provider.get("name", "") aliases = {name} if name.startswith("cli-anything-"): aliases.add(name.removeprefix("cli-anything-")) return any(alias in installed for alias in aliases) def _format_requires(provider): requires = provider.get("requires") or {} parts = [] for key in ("binary", "env", "package"): values = requires.get(key) or [] if isinstance(values, str): values = [values] if values: parts.append(f"{key}: {', '.join(values)}") return "; ".join(parts) if parts else "none" def _render_capability_tooling(matrix_item, installed): """Render v2 capability/provider guidance for local matrix skills.""" capabilities = matrix_item.get("capabilities", []) if not capabilities: return "" lines = [ "## Capability Provider Overview", "", "Pick providers per capability from task constraints and preflight facts. CLI providers show cli-hub install status; non-CLI providers list their preflight requirements.", "", ] for capability in capabilities: lines.append(f"### `{capability['id']}`") if capability.get("intent"): lines.append(capability["intent"]) lines.append("") for provider in capability.get("providers", []): kind = provider.get("kind", "provider") quality = provider.get("quality_tier", "unknown") cost = provider.get("cost_tier", "unknown") offline = "offline" if provider.get("offline") else "online" status = "" if kind in {"harness-cli", "public-cli"}: status = "installed" if _provider_installed(provider, installed) else "not installed" status = f"; {status}" requires = _format_requires(provider) lines.append( f"- `{provider.get('name', '')}` ({kind}; {quality}; {cost}; {offline}{status})" ) lines.append(f" - Requires: {requires}") if provider.get("notes"): lines.append(f" - Notes: {provider['notes']}") lines.append("") recipes = matrix_item.get("recipes", []) if recipes: lines.append("## Recipes") lines.append("") for recipe in recipes: capabilities_used = ", ".join(f"`{item}`" for item in recipe.get("capabilities_used", [])) lines.append(f"- `{recipe['id']}`: {recipe.get('description', '')}") if capabilities_used: lines.append(f" - Uses: {capabilities_used}") lines.append("") known_gaps = matrix_item.get("known_gaps", []) if known_gaps: lines.append("## Known Gaps") lines.append("") for gap in known_gaps: lines.append(f"- `{gap.get('capability', 'unknown')}`: {gap.get('reason', '')}") if gap.get("workaround"): lines.append(f" - Workaround: {gap['workaround']}") lines.append("") return "\n".join(lines).rstrip() def _render_stage_tooling(matrix_item, installed): """Render per-stage tooling overview with goals and alternatives.""" stages = matrix_item.get("stages", []) has_goals = any(s.get("goal") for s in stages) if not has_goals: return "" lines = [ "## Stage Tooling Overview", "", "What is available for each stage on this system.", "", ] for stage in stages: goal = stage.get("goal") if not goal: continue lines.append(f"### {stage['name']}") lines.append(f"**Goal:** {goal}") lines.append("") for cli_name in stage.get("clis", []): marker = "installed" if cli_name in installed else "not installed" lines.append(f"- CLI: `{cli_name}` ({marker})") alts = stage.get("alternatives", {}) if alts.get("python"): lines.append(f"- Python: {', '.join(alts['python'])}") if alts.get("api"): lines.append(f"- APIs: {', '.join(alts['api'])}") if alts.get("native"): lines.append(f"- Native: {', '.join(alts['native'])}") lines.append("") return "\n".join(lines) def _render_discovery_section(matrix_item): """Registry search hints are metadata; generated skills list concrete providers.""" return ""