From e59b7ee3674e804c9637681e41e010394415e33c Mon Sep 17 00:00:00 2001 From: yuhao Date: Fri, 10 Apr 2026 15:43:51 +0000 Subject: [PATCH] launch cli-anything-hub pkg manager. --- .github/scripts/generate_meta_skill.py | 37 ++- .github/workflows/deploy-pages.yml | 5 + .gitignore | 1 + README.md | 10 +- cli-hub-meta-skill/SKILL.md | 54 +++- cli-hub/README.md | 85 +++++ cli-hub/cli_hub/__init__.py | 3 + cli-hub/cli_hub/analytics.py | 111 +++++++ cli-hub/cli_hub/cli.py | 168 ++++++++++ cli-hub/cli_hub/installer.py | 107 +++++++ cli-hub/cli_hub/registry.py | 72 +++++ cli-hub/setup.py | 49 +++ cli-hub/tests/__init__.py | 0 cli-hub/tests/test_cli_hub.py | 420 +++++++++++++++++++++++++ docs/hub/SKILL.md | 141 --------- docs/hub/SKILL.txt | 141 --------- docs/hub/index.html | 116 ++++++- 17 files changed, 1199 insertions(+), 321 deletions(-) create mode 100644 cli-hub/README.md create mode 100644 cli-hub/cli_hub/__init__.py create mode 100644 cli-hub/cli_hub/analytics.py create mode 100644 cli-hub/cli_hub/cli.py create mode 100644 cli-hub/cli_hub/installer.py create mode 100644 cli-hub/cli_hub/registry.py create mode 100644 cli-hub/setup.py create mode 100644 cli-hub/tests/__init__.py create mode 100644 cli-hub/tests/test_cli_hub.py delete mode 100644 docs/hub/SKILL.md delete mode 100644 docs/hub/SKILL.txt diff --git a/.github/scripts/generate_meta_skill.py b/.github/scripts/generate_meta_skill.py index c3bba90d5..11fcfdf23 100644 --- a/.github/scripts/generate_meta_skill.py +++ b/.github/scripts/generate_meta_skill.py @@ -32,11 +32,19 @@ def main(): "## Quick Install", "", "```bash", - "# Install any CLI", - f"pip install git+{data['meta']['repo']}.git#subdirectory=/agent-harness", + "# First, install the CLI Hub package manager", + "pip install cli-anything-hub", "", - "# Example: Install GIMP CLI", - f"pip install git+{data['meta']['repo']}.git#subdirectory=gimp/agent-harness", + "# Browse available CLIs", + "cli-hub list", + "", + "# Install any CLI by name", + "cli-hub install gimp", + "cli-hub install blender", + "", + "# Search by category or keyword", + "cli-hub search image", + "cli-hub search 3d", "```", "", "## Available CLIs", @@ -53,12 +61,19 @@ def main(): for cli in sorted(clis, key=lambda x: x['name']): name = cli['display_name'] desc = cli['description'] - install = f"`{cli['install_cmd']}`" + install = f"`cli-hub install {cli['name']}`" lines.append(f"| **{name}** | {desc} | {install} |") lines.append("") lines.extend([ + "## How It Works", + "", + "`cli-hub` is a lightweight wrapper around `pip`. When you run `cli-hub install gimp`,", + "it installs a separate Python package (`cli-anything-gimp`) with its own CLI entry point", + "(`cli-anything-gimp`). Each CLI is an independent pip package โ€” `cli-hub` simply resolves", + "names from the registry and tracks installs.", + "", "## Usage Pattern", "", "All CLIs follow the same pattern:", @@ -76,15 +91,17 @@ def main(): "", "## For AI Agents", "", - "1. Install the CLI you need from the table above", - "2. Read its full SKILL.md at the repo path shown in registry.json", - "3. Always use `--json` flag for machine-readable output", - "4. Check exit codes (0=success, non-zero=error)", + "1. Install the hub: `pip install cli-anything-hub`", + "2. Install the CLI you need: `cli-hub install ` (installs `cli-anything-` pip package)", + "3. Run: `cli-anything-` for REPL, or `cli-anything- ` for one-shot", + "4. Read its full SKILL.md at the repo path shown in registry.json", + "5. Always use `--json` flag for machine-readable output", + "6. Check exit codes (0=success, non-zero=error)", "", "## More Info", "", f"- Repository: {data['meta']['repo']}", - "- Web Hub: https://hkuds.github.io/CLI-Anything/", + "- Web Hub: https://clianything.cc", f"- Last Updated: {data['meta']['updated']}", ]) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index b0e4c1984..10339ed07 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -7,6 +7,8 @@ on: paths: - '*/agent-harness/**' - 'registry.json' + - 'cli-hub/**' + - 'docs/hub/**' - '.github/workflows/deploy-pages.yml' - '.github/scripts/update_registry_dates.py' - '.github/scripts/generate_meta_skill.py' @@ -44,6 +46,9 @@ jobs: - name: Copy catalog as .txt to avoid Jekyll processing run: cp cli-hub-skill/SKILL.md docs/hub/SKILL.txt + - name: Copy registry.json to hub for cli-hub access + run: cp registry.json docs/hub/registry.json + - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 with: diff --git a/.gitignore b/.gitignore index d74f9c3f2..562fcb472 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ !/skill_generation/ !/openclaw-skill/ !/cli-hub-meta-skill/ +!/cli-hub/ # Ignore cli-hub-skill (auto-generated, not tracked) /cli-hub-skill/ diff --git a/README.md b/README.md index 57bfa2039..1b93679ff 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software

-**๐ŸŒ [CLI-Hub](https://hkuds.github.io/CLI-Anything/)**: Explore all community-built CLIs and install with one command at the **[CLI-Hub](https://hkuds.github.io/CLI-Anything/)**. Want to add your own? [Open a PR](https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md) โ€” the hub updates instantly. +**๐ŸŒ [CLI-Hub](https://hkuds.github.io/CLI-Anything/)**: `pip install cli-anything-hub` then `cli-hub install ` โ€” browse, install, and manage all community-built CLIs. Want to add your own? [Open a PR](https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md) โ€” the hub updates instantly. **๐ŸŽฌ [See Demos](#-real-world-demos)**: Watch AI agents use generated CLIs to produce real artifacts โ€” diagrams, gameplay, subtitles, and more. @@ -45,7 +45,13 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software Thanks to all invaluable efforts from the community! More updates continuously on the way everyday.. -- **2026-04-08** ๐ŸŽฌ **Openscreen CLI** merged (#183) โ€” screen recording editor harness with fused manual + auto-generated tests (101 tests). +- **2026-04-10** ๐Ÿ“ฆ **CLI-Hub package manager** launched โ€” `pip install cli-anything-hub` to browse, install, and manage all CLIs. Download tracking via Umami analytics. Hub frontend updated with new install flow and "Empower yourself" toolkit card. + +- **2026-04-09** ๐Ÿงน Cleanup and docs pass (#200) โ€” fixed Openscreen test subtotals, added Openscreen to Chinese README and project structure. + +- **2026-04-08** ๐ŸŽฌ **Openscreen CLI** merged (#183) โ€” screen recording editor harness with 101 tests. โ˜๏ธ **CloudAnalyzer CLI** merged (#181) โ€” cloud cost analysis harness with 27 commands. ๐ŸŒŠ **SeaClip / PM2 / ChromaDB** harnesses merged (#129). + +- **2026-04-07** ๐Ÿ”„ **Dify Workflow CLI** merged (#191) โ€” workflow automation wrapper. ๐Ÿ”ง **Inkscape** auto-save fix (#193, fixes #182). ๐Ÿ›ก๏ธ **DomShell security hardening** (#156) โ€” URL validation and DOM sanitization for browser CLI. ๐Ÿฅง **Pi Coding Agent extension** merged (#178). - **2026-04-06** ๐Ÿ” **Exa CLI** merged (#172) โ€” AI-powered web search and answers harness. ๐ŸŽฎ **Godot CLI** merged (#140) โ€” game engine harness with full demo-game E2E pipeline. CLI-Hub frontend improvements. diff --git a/cli-hub-meta-skill/SKILL.md b/cli-hub-meta-skill/SKILL.md index 802ac6c12..25fe7e427 100644 --- a/cli-hub-meta-skill/SKILL.md +++ b/cli-hub-meta-skill/SKILL.md @@ -9,13 +9,33 @@ description: >- CLI-Hub is a marketplace of agent-native command-line interfaces that make professional software accessible to AI agents. +## Quick Start + +```bash +# Install the CLI Hub package manager +pip install cli-anything-hub + +# Browse all available CLIs +cli-hub list + +# Search by category or keyword +cli-hub search image +cli-hub search "3d modeling" + +# Install a CLI +cli-hub install gimp + +# Show details for a CLI +cli-hub info gimp +``` + ## Live Catalog -**URL**: [`https://hkuds.github.io/CLI-Anything/SKILL.txt`](https://hkuds.github.io/CLI-Anything/SKILL.txt) +**URL**: [`https://clianything.cc/SKILL.txt`](https://clianything.cc/SKILL.txt) The catalog is auto-updated and provides: - Full list of available CLIs organized by category -- One-line `pip install` commands for each tool +- One-line `cli-hub install` commands for each tool - Complete descriptions and usage patterns **Note**: The file is served as `.txt` but contains markdown formatting for easy parsing. @@ -33,26 +53,36 @@ CLI-Hub covers a broad range of software and codebases, empowering agents to con Each CLI provides stateful operations, JSON output for agents, REPL mode, and integrates with real software backends. +## How It Works + +`cli-hub` is a lightweight wrapper around `pip`. When you run `cli-hub install gimp`, it installs a separate Python package (`cli-anything-gimp`) with its own CLI entry point (`cli-anything-gimp`). Each CLI is an independent pip package โ€” `cli-hub` simply resolves names from the registry and tracks installs. + ## How to Use -1. **Read the catalog**: Fetch `https://hkuds.github.io/CLI-Anything/SKILL.txt` (markdown format) -2. **Find your tool**: Browse by category to discover the CLI you need -3. **Install**: Use the provided `pip install` command -4. **Execute**: All CLIs support `--json` flag for machine-readable output +1. **Install cli-hub**: `pip install cli-anything-hub` +2. **Find your tool**: `cli-hub search ` or `cli-hub list -c ` +3. **Install**: `cli-hub install ` (installs the `cli-anything-` pip package) +4. **Run**: `cli-anything-` for REPL, or `cli-anything- ` for one-shot +5. **JSON output**: All CLIs support `--json` flag for machine-readable output ## Example Workflow ```bash -# Fetch the catalog to find available tools -# Install the CLI you need -pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=/agent-harness +# Install the hub +pip install cli-anything-hub + +# Find what you need +cli-hub search video + +# Install it +cli-hub install kdenlive # Use it with JSON output -cli-anything- --json [options] +cli-anything-kdenlive --json project create --name my-project ``` ## More Info -- Live Catalog: https://hkuds.github.io/CLI-Anything/SKILL.txt -- Web Hub: https://hkuds.github.io/CLI-Anything/ +- Live Catalog: https://clianything.cc/SKILL.txt +- Web Hub: https://clianything.cc - Repository: https://github.com/HKUDS/CLI-Anything diff --git a/cli-hub/README.md b/cli-hub/README.md new file mode 100644 index 000000000..0830e6b9b --- /dev/null +++ b/cli-hub/README.md @@ -0,0 +1,85 @@ +# cli-hub + +Package manager for [CLI-Anything](https://github.com/HKUDS/CLI-Anything) โ€” a framework that auto-generates stateful CLI interfaces for GUI applications, making them agent-native. + +Browse, install, and manage 40+ CLI harnesses for software like GIMP, Blender, Inkscape, LibreOffice, Audacity, OBS Studio, and more โ€” all from your terminal. + +**Web Hub**: [clianything.cc](https://clianything.cc) + +## Install + +```bash +pip install cli-anything-hub +``` + +## Usage + +```bash +# Browse all available CLIs, grouped by category +cli-hub list + +# Filter by category (image, 3d, video, audio, office, ai, ...) +cli-hub list -c image + +# Search by name, description, or category +cli-hub search "3d modeling" + +# Show details for a CLI +cli-hub info gimp + +# Install a CLI harness +cli-hub install gimp + +# Update a CLI to the latest version +cli-hub update gimp + +# Uninstall a CLI +cli-hub uninstall gimp +``` + +## What gets installed + +Each CLI harness is a standalone Python package that wraps a real application (GIMP, Blender, etc.) with a stateful command-line interface. Every harness supports: + +- **REPL mode**: `cli-anything-gimp` launches an interactive session +- **One-shot commands**: `cli-anything-gimp project create --name my-project` +- **JSON output**: `cli-anything-gimp --json project list` for machine-readable output +- **Undo/redo**: Stateful project management with full operation history + +## For AI agents + +cli-hub is designed to be agent-friendly. AI coding agents can: + +1. `pip install cli-anything-hub` to get the package manager +2. `cli-hub search ` or `cli-hub list --json` to discover tools +3. `cli-hub install ` to install what they need +4. Use `--json` output for structured data parsing + +## Available categories + +3D, AI, Audio, Communication, Database, Design, DevOps, Diagrams, Game, GameDev, Generation, Graphics, Image, Music, Network, Office, OSINT, Project Management, Search, Streaming, Testing, Video, Web + +## JSON output + +All listing commands support `--json` for machine-readable output: + +```bash +cli-hub list --json +cli-hub search blender --json +``` + +## Analytics + +cli-hub sends anonymous install/uninstall events to help track adoption (via [Umami](https://umami.is)). No personal data is collected. + +Opt out: + +```bash +export CLI_HUB_NO_ANALYTICS=1 +``` + +## Links + +- **Web Hub**: [clianything.cc](https://clianything.cc) +- **Repository**: [github.com/HKUDS/CLI-Anything](https://github.com/HKUDS/CLI-Anything) +- **Live Catalog**: [clianything.cc/SKILL.txt](https://clianything.cc/SKILL.txt) diff --git a/cli-hub/cli_hub/__init__.py b/cli-hub/cli_hub/__init__.py new file mode 100644 index 000000000..7f360542e --- /dev/null +++ b/cli-hub/cli_hub/__init__.py @@ -0,0 +1,3 @@ +"""cli-hub โ€” Download, manage, and browse CLI-Anything harnesses.""" + +__version__ = "0.1.0" diff --git a/cli-hub/cli_hub/analytics.py b/cli-hub/cli_hub/analytics.py new file mode 100644 index 000000000..379f0bd1d --- /dev/null +++ b/cli-hub/cli_hub/analytics.py @@ -0,0 +1,111 @@ +"""Lightweight, opt-out-able download event tracking via Umami.""" + +import os +import platform +import threading + +import requests + +from cli_hub import __version__ + +UMAMI_URL = "https://cloud.umami.is/api/send" +WEBSITE_ID = "a076c661-bed1-405c-a522-813794e688b4" +HOSTNAME = "clianything.cc" +USER_AGENT = f"Mozilla/5.0 (compatible; cli-anything-hub/{__version__})" + + +def _is_enabled(): + return os.environ.get("CLI_HUB_NO_ANALYTICS", "").strip() not in ("1", "true", "yes") + + +def _send_event(payload): + """Send a single event payload. Blocking โ€” callers should use threads.""" + try: + return requests.post( + UMAMI_URL, json=payload, timeout=5, + headers={"User-Agent": USER_AGENT}, + ) + except Exception: + return None # analytics must never break the user's workflow + + +def track_event(event_name, url="/cli-anything-hub", data=None): + """Fire-and-forget event to Umami. Non-blocking, never raises.""" + if not _is_enabled(): + return + + payload = { + "type": "event", + "payload": { + "website": WEBSITE_ID, + "hostname": HOSTNAME, + "url": url, + "name": event_name, + "data": data or {}, + }, + } + + threading.Thread(target=_send_event, args=(payload,), daemon=True).start() + + +def track_install(cli_name, version): + """Track a CLI install event โ€” event name includes the CLI for dashboard visibility.""" + track_event(f"cli-install:{cli_name}", url=f"/cli-anything-hub/install/{cli_name}", data={ + "cli": cli_name, + "version": version, + "platform": platform.system().lower(), + }) + + +def track_uninstall(cli_name): + """Track a CLI uninstall event.""" + track_event(f"cli-uninstall:{cli_name}", url=f"/cli-anything-hub/uninstall/{cli_name}", data={ + "cli": cli_name, + }) + + +def track_visit(is_agent=False): + """Track a visit-human or visit-agent event, matching the hub website's convention.""" + event_name = "visit-agent" if is_agent else "visit-human" + track_event(event_name, url="/cli-anything-hub", data={ + "source": "cli-anything-hub", + "platform": platform.system().lower(), + }) + + +def track_first_run(): + """Send a one-time 'cli-hub-installed' event on first invocation.""" + from pathlib import Path + marker = Path.home() / ".cli-hub" / ".first_run_sent" + if marker.exists(): + return + track_event("cli-anything-hub-installed", url="/cli-anything-hub/installed", data={ + "version": __version__, + "platform": platform.system().lower(), + }) + try: + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text(__version__) + except Exception: + pass + + +def _detect_is_agent(): + """Detect if cli-hub is likely being invoked by an AI agent.""" + indicators = [ + "CLAUDE_CODE", # Claude Code + "CODEX", # OpenAI Codex + "CURSOR_SESSION", # Cursor + "CLINE_SESSION", # Cline + "COPILOT", # GitHub Copilot + "AIDER", # Aider + "CONTINUE_SESSION", # Continue.dev + ] + for var in indicators: + if os.environ.get(var): + return True + # Check if stdin is not a terminal (piped / scripted) + import sys + if not sys.stdin.isatty(): + return True + return False diff --git a/cli-hub/cli_hub/cli.py b/cli-hub/cli_hub/cli.py new file mode 100644 index 000000000..65557cfa9 --- /dev/null +++ b/cli-hub/cli_hub/cli.py @@ -0,0 +1,168 @@ +"""cli-hub โ€” CLI entry point.""" + +import click + +from cli_hub import __version__ +from cli_hub.registry import fetch_registry, get_cli, search_clis, list_categories +from cli_hub.installer import install_cli, uninstall_cli, get_installed, update_cli +from cli_hub.analytics import track_install, track_uninstall, track_visit, track_first_run, _detect_is_agent + + +@click.group(invoke_without_command=True) +@click.option("--version", is_flag=True, help="Show version.") +@click.pass_context +def main(ctx, version): + """cli-hub โ€” Download and manage CLI-Anything harnesses.""" + track_first_run() + track_visit(is_agent=_detect_is_agent()) + if version: + click.echo(f"cli-hub {__version__}") + return + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + + +@main.command() +@click.argument("name") +def install(name): + """Install a CLI harness by name.""" + click.echo(f"Installing {name}...") + success, msg = install_cli(name) + if success: + cli = get_cli(name) + track_install(name, cli["version"] if cli else "unknown") + click.secho(f"โœ“ {msg}", fg="green") + click.echo(f" Run it with: {cli['entry_point']}" if cli else "") + else: + click.secho(f"โœ— {msg}", fg="red", err=True) + raise SystemExit(1) + + +@main.command() +@click.argument("name") +def uninstall(name): + """Uninstall a CLI harness by name.""" + success, msg = uninstall_cli(name) + if success: + track_uninstall(name) + click.secho(f"โœ“ {msg}", fg="green") + else: + click.secho(f"โœ— {msg}", fg="red", err=True) + raise SystemExit(1) + + +@main.command() +@click.argument("name") +def update(name): + """Update a CLI harness to the latest version.""" + click.echo(f"Updating {name}...") + success, msg = update_cli(name) + if success: + cli = get_cli(name) + track_install(name, cli["version"] if cli else "unknown") + click.secho(f"โœ“ {msg}", fg="green") + else: + click.secho(f"โœ— {msg}", fg="red", err=True) + raise SystemExit(1) + + +@main.command("list") +@click.option("--category", "-c", default=None, help="Filter by category.") +@click.option("--json", "as_json", is_flag=True, help="Output as JSON.") +def list_clis(category, as_json): + """List all available CLI harnesses.""" + try: + registry = fetch_registry() + except Exception as e: + click.secho(f"Failed to fetch registry: {e}", fg="red", err=True) + raise SystemExit(1) + + clis = registry["clis"] + if category: + clis = [c for c in clis if c.get("category", "").lower() == category.lower()] + + installed = get_installed() + + if as_json: + import json + click.echo(json.dumps(clis, indent=2)) + return + + if not clis: + click.echo("No CLIs found." + (f" Category '{category}' may not exist." if category else "")) + return + + # Group by category + by_cat = {} + for cli in clis: + cat = cli.get("category", "uncategorized") + by_cat.setdefault(cat, []).append(cli) + + for cat in sorted(by_cat): + click.secho(f"\n {cat.upper()}", fg="blue", bold=True) + for cli in sorted(by_cat[cat], key=lambda c: c["name"]): + marker = click.style(" โ—", fg="green") if cli["name"] in installed else " " + name = click.style(f"{cli['name']:20s}", bold=True) + desc = cli["description"][:60] + click.echo(f" {marker} {name} {desc}") + + total = len(clis) + inst = sum(1 for c in clis if c["name"] in installed) + click.echo(f"\n {total} CLIs available, {inst} installed") + cats = list_categories(registry) + click.echo(f" Categories: {', '.join(cats)}") + + +@main.command() +@click.argument("query") +@click.option("--json", "as_json", is_flag=True, help="Output as JSON.") +def search(query, as_json): + """Search CLIs by name, description, or category.""" + results = search_clis(query) + + if as_json: + import json + click.echo(json.dumps(results, indent=2)) + return + + if not results: + click.echo(f"No CLIs matching '{query}'.") + return + + installed = get_installed() + for cli in results: + marker = click.style("โ—", fg="green") if cli["name"] in installed else " " + name = click.style(cli["name"], bold=True) + cat = click.style(f"[{cli.get('category', '')}]", fg="blue") + click.echo(f" {marker} {name} {cat} โ€” {cli['description'][:70]}") + click.echo(f" Install: cli-hub install {cli['name']}") + + +@main.command() +@click.argument("name") +def info(name): + """Show details for a specific CLI.""" + cli = get_cli(name) + if not cli: + click.secho(f"CLI '{name}' not found.", fg="red", err=True) + raise SystemExit(1) + + installed = get_installed() + is_installed = cli["name"] in installed + + click.secho(f"\n {cli['display_name']}", bold=True) + click.echo(f" {cli['description']}") + click.echo(f" Category: {cli.get('category', 'N/A')}") + click.echo(f" Version: {cli['version']}") + click.echo(f" Requires: {cli.get('requires') or 'nothing'}") + click.echo(f" Entry point: {cli['entry_point']}") + click.echo(f" Homepage: {cli.get('homepage', 'N/A')}") + click.echo(f" Contributor: {cli.get('contributor', 'N/A')}") + status = click.style("installed", fg="green") if is_installed else "not installed" + click.echo(f" Status: {status}") + click.echo(f"\n Install: cli-hub install {cli['name']}") + click.echo() + + +if __name__ == "__main__": + main() diff --git a/cli-hub/cli_hub/installer.py b/cli-hub/cli_hub/installer.py new file mode 100644 index 000000000..a8f761fcd --- /dev/null +++ b/cli-hub/cli_hub/installer.py @@ -0,0 +1,107 @@ +"""Install, uninstall, and manage CLI-Anything harnesses via pip.""" + +import json +import subprocess +import sys +from pathlib import Path + +from cli_hub.registry import get_cli, fetch_registry + +INSTALLED_FILE = Path.home() / ".cli-hub" / "installed.json" + + +def _load_installed(): + if INSTALLED_FILE.exists(): + try: + return json.loads(INSTALLED_FILE.read_text()) + except json.JSONDecodeError: + pass + return {} + + +def _save_installed(data): + INSTALLED_FILE.parent.mkdir(parents=True, exist_ok=True) + INSTALLED_FILE.write_text(json.dumps(data, indent=2)) + + +def install_cli(name): + """Install a CLI harness by name. Returns (success, message).""" + cli = get_cli(name) + if cli is None: + return False, f"CLI '{name}' not found in registry. Use 'cli-hub list' to see available CLIs." + + install_cmd = cli["install_cmd"] + result = subprocess.run( + [sys.executable, "-m", "pip", "install"] + install_cmd.replace("pip install ", "").split(), + capture_output=True, text=True + ) + + if result.returncode == 0: + installed = _load_installed() + installed[cli["name"]] = { + "version": cli["version"], + "entry_point": cli["entry_point"], + "install_cmd": install_cmd, + } + _save_installed(installed) + return True, f"Installed {cli['display_name']} ({cli['entry_point']})" + else: + return False, f"pip install failed:\n{result.stderr}" + + +def uninstall_cli(name): + """Uninstall a CLI harness by name. Returns (success, message).""" + cli = get_cli(name) + if cli is None: + return False, f"CLI '{name}' not found in registry." + + # The pip package name follows the pattern: cli-anything- with underscores + # but we derive it from the install_cmd's subdirectory + # The namespace package is cli_anything., entry point is cli-anything- + # pip package name in subdirectory installs is the name from setup.py + # We'll uninstall by the entry_point pattern + pkg_name = f"cli-anything-{cli['name']}" + + result = subprocess.run( + [sys.executable, "-m", "pip", "uninstall", "-y", pkg_name], + capture_output=True, text=True + ) + + if result.returncode == 0: + installed = _load_installed() + installed.pop(cli["name"], None) + _save_installed(installed) + return True, f"Uninstalled {cli['display_name']}" + else: + return False, f"pip uninstall failed:\n{result.stderr}" + + +def get_installed(): + """Return dict of installed CLIs.""" + return _load_installed() + + +def update_cli(name): + """Update a CLI by reinstalling from the latest source.""" + cli = get_cli(name, fetch_registry(force_refresh=True)) + if cli is None: + return False, f"CLI '{name}' not found in registry." + + install_cmd = cli["install_cmd"] + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "--upgrade", "--force-reinstall"] + + install_cmd.replace("pip install ", "").split(), + capture_output=True, text=True + ) + + if result.returncode == 0: + installed = _load_installed() + installed[cli["name"]] = { + "version": cli["version"], + "entry_point": cli["entry_point"], + "install_cmd": install_cmd, + } + _save_installed(installed) + return True, f"Updated {cli['display_name']} to {cli['version']}" + else: + return False, f"Update failed:\n{result.stderr}" diff --git a/cli-hub/cli_hub/registry.py b/cli-hub/cli_hub/registry.py new file mode 100644 index 000000000..8b6a3b99c --- /dev/null +++ b/cli-hub/cli_hub/registry.py @@ -0,0 +1,72 @@ +"""Fetch and cache the CLI-Anything registry.""" + +import json +import os +import time +from pathlib import Path + +import requests + +REGISTRY_URL = "https://hkuds.github.io/CLI-Anything/registry.json" +CACHE_DIR = Path.home() / ".cli-hub" +CACHE_FILE = CACHE_DIR / "registry_cache.json" +CACHE_TTL = 3600 # 1 hour + + +def _ensure_cache_dir(): + CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +def fetch_registry(force_refresh=False): + """Fetch registry.json, using a local cache with TTL.""" + _ensure_cache_dir() + + if not force_refresh and CACHE_FILE.exists(): + try: + cached = json.loads(CACHE_FILE.read_text()) + if time.time() - cached.get("_cached_at", 0) < CACHE_TTL: + return cached["data"] + except (json.JSONDecodeError, KeyError): + pass + + resp = requests.get(REGISTRY_URL, timeout=15) + resp.raise_for_status() + data = resp.json() + + cache_payload = {"_cached_at": time.time(), "data": data} + CACHE_FILE.write_text(json.dumps(cache_payload, indent=2)) + + return data + + +def get_cli(name, registry=None): + """Look up a CLI entry by name (case-insensitive).""" + if registry is None: + registry = fetch_registry() + name_lower = name.lower() + for cli in registry["clis"]: + if cli["name"].lower() == name_lower: + return cli + return None + + +def search_clis(query, registry=None): + """Search CLIs by name, description, or category.""" + if registry is None: + registry = fetch_registry() + query_lower = query.lower() + results = [] + for cli in registry["clis"]: + if (query_lower in cli["name"].lower() + or query_lower in cli["description"].lower() + or query_lower in cli.get("category", "").lower() + or query_lower in cli.get("display_name", "").lower()): + results.append(cli) + return results + + +def list_categories(registry=None): + """Return sorted list of unique categories.""" + if registry is None: + registry = fetch_registry() + return sorted(set(cli.get("category", "uncategorized") for cli in registry["clis"])) diff --git a/cli-hub/setup.py b/cli-hub/setup.py new file mode 100644 index 000000000..1c2f89532 --- /dev/null +++ b/cli-hub/setup.py @@ -0,0 +1,49 @@ +"""cli-hub โ€” package manager for CLI-Anything harnesses.""" + +from setuptools import setup, find_packages + +setup( + name="cli-anything-hub", + version="0.1.0", + description="Package manager for CLI-Anything โ€” browse, install, and manage 40+ agent-native CLI interfaces for GUI applications", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="HKUDS", + author_email="hkuds@connect.hku.hk", + url="https://github.com/HKUDS/CLI-Anything", + project_urls={ + "Homepage": "https://clianything.cc", + "Repository": "https://github.com/HKUDS/CLI-Anything", + "Bug Tracker": "https://github.com/HKUDS/CLI-Anything/issues", + "Catalog": "https://clianything.cc/SKILL.txt", + }, + license="MIT", + packages=find_packages(exclude=["tests", "tests.*"]), + python_requires=">=3.10", + install_requires=[ + "click>=8.0", + "requests>=2.28", + ], + entry_points={ + "console_scripts": [ + "cli-hub=cli_hub.cli:main", + ], + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: System :: Installation/Setup", + "Topic :: Utilities", + ], + keywords="cli, agent, gui, automation, package-manager, cli-anything", +) diff --git a/cli-hub/tests/__init__.py b/cli-hub/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cli-hub/tests/test_cli_hub.py b/cli-hub/tests/test_cli_hub.py new file mode 100644 index 000000000..37f85deaf --- /dev/null +++ b/cli-hub/tests/test_cli_hub.py @@ -0,0 +1,420 @@ +"""Tests for cli-hub โ€” registry, installer, analytics, and CLI.""" + +import json +import os +import tempfile +from pathlib import Path +from unittest.mock import patch, MagicMock + +import pytest +import click.testing + +from cli_hub import __version__ +from cli_hub.registry import fetch_registry, get_cli, search_clis, list_categories +from cli_hub.installer import install_cli, uninstall_cli, get_installed, _load_installed, _save_installed +from cli_hub.analytics import _is_enabled, track_event, track_install, track_uninstall as analytics_track_uninstall, track_visit, track_first_run, _detect_is_agent +from cli_hub.cli import main + + +# โ”€โ”€โ”€ Sample registry data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +SAMPLE_REGISTRY = { + "meta": {"repo": "https://github.com/HKUDS/CLI-Anything", "description": "test"}, + "clis": [ + { + "name": "gimp", + "display_name": "GIMP", + "version": "1.0.0", + "description": "Image editing via GIMP", + "requires": "gimp", + "homepage": "https://gimp.org", + "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness", + "entry_point": "cli-anything-gimp", + "skill_md": None, + "category": "image", + "contributor": "test-user", + "contributor_url": "https://github.com/test-user", + }, + { + "name": "blender", + "display_name": "Blender", + "version": "1.0.0", + "description": "3D modeling via Blender", + "requires": "blender", + "homepage": "https://blender.org", + "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness", + "entry_point": "cli-anything-blender", + "skill_md": None, + "category": "3d", + "contributor": "test-user", + "contributor_url": "https://github.com/test-user", + }, + { + "name": "audacity", + "display_name": "Audacity", + "version": "1.0.0", + "description": "Audio editing and processing via sox", + "requires": "sox", + "homepage": "https://audacityteam.org", + "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness", + "entry_point": "cli-anything-audacity", + "skill_md": None, + "category": "audio", + "contributor": "test-user", + "contributor_url": "https://github.com/test-user", + }, + ], +} + + +# โ”€โ”€โ”€ Registry tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +class TestRegistry: + """Tests for registry.py โ€” fetch, cache, search, and lookup.""" + + @patch("cli_hub.registry.requests.get") + @patch("cli_hub.registry.CACHE_FILE", Path(tempfile.mktemp())) + def test_fetch_registry_from_remote(self, mock_get): + mock_resp = MagicMock() + mock_resp.json.return_value = SAMPLE_REGISTRY + mock_resp.raise_for_status = MagicMock() + mock_get.return_value = mock_resp + + result = fetch_registry(force_refresh=True) + assert result["clis"][0]["name"] == "gimp" + mock_get.assert_called_once() + + def test_get_cli_found(self): + cli = get_cli("gimp", SAMPLE_REGISTRY) + assert cli is not None + assert cli["display_name"] == "GIMP" + + def test_get_cli_case_insensitive(self): + cli = get_cli("GIMP", SAMPLE_REGISTRY) + assert cli is not None + assert cli["name"] == "gimp" + + def test_get_cli_not_found(self): + cli = get_cli("nonexistent", SAMPLE_REGISTRY) + assert cli is None + + def test_search_by_name(self): + results = search_clis("gimp", SAMPLE_REGISTRY) + assert len(results) == 1 + assert results[0]["name"] == "gimp" + + def test_search_by_category(self): + results = search_clis("3d", SAMPLE_REGISTRY) + assert len(results) == 1 + assert results[0]["name"] == "blender" + + def test_search_by_description(self): + results = search_clis("audio", SAMPLE_REGISTRY) + assert len(results) == 1 + assert results[0]["name"] == "audacity" + + def test_search_no_results(self): + results = search_clis("nonexistent_xyz", SAMPLE_REGISTRY) + assert len(results) == 0 + + def test_list_categories(self): + cats = list_categories(SAMPLE_REGISTRY) + assert cats == ["3d", "audio", "image"] + + +# โ”€โ”€โ”€ Installer tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +class TestInstaller: + """Tests for installer.py โ€” install, uninstall, tracking.""" + + def test_load_installed_empty(self, tmp_path): + with patch("cli_hub.installer.INSTALLED_FILE", tmp_path / "installed.json"): + assert _load_installed() == {} + + def test_save_and_load_installed(self, tmp_path): + installed_file = tmp_path / "installed.json" + with patch("cli_hub.installer.INSTALLED_FILE", installed_file): + _save_installed({"gimp": {"version": "1.0.0"}}) + data = _load_installed() + assert data["gimp"]["version"] == "1.0.0" + + @patch("cli_hub.installer.subprocess.run") + @patch("cli_hub.installer.get_cli") + @patch("cli_hub.installer.INSTALLED_FILE", Path(tempfile.mktemp())) + def test_install_success(self, mock_get_cli, mock_run): + mock_get_cli.return_value = SAMPLE_REGISTRY["clis"][0] + mock_run.return_value = MagicMock(returncode=0) + + success, msg = install_cli("gimp") + assert success + assert "GIMP" in msg + + @patch("cli_hub.installer.get_cli") + def test_install_not_found(self, mock_get_cli): + mock_get_cli.return_value = None + success, msg = install_cli("nonexistent") + assert not success + assert "not found" in msg + + @patch("cli_hub.installer.subprocess.run") + @patch("cli_hub.installer.get_cli") + @patch("cli_hub.installer.INSTALLED_FILE", Path(tempfile.mktemp())) + def test_install_pip_failure(self, mock_get_cli, mock_run): + mock_get_cli.return_value = SAMPLE_REGISTRY["clis"][0] + mock_run.return_value = MagicMock(returncode=1, stderr="some error") + + success, msg = install_cli("gimp") + assert not success + assert "failed" in msg + + @patch("cli_hub.installer.subprocess.run") + @patch("cli_hub.installer.get_cli") + @patch("cli_hub.installer.INSTALLED_FILE", Path(tempfile.mktemp())) + def test_uninstall_success(self, mock_get_cli, mock_run): + mock_get_cli.return_value = SAMPLE_REGISTRY["clis"][0] + mock_run.return_value = MagicMock(returncode=0) + + success, msg = uninstall_cli("gimp") + assert success + assert "GIMP" in msg + + +# โ”€โ”€โ”€ Analytics tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +class TestAnalytics: + """Tests for analytics.py โ€” opt-out, event firing, event names.""" + + def test_analytics_enabled_by_default(self): + with patch.dict(os.environ, {}, clear=True): + assert _is_enabled() + + def test_analytics_disabled_by_env(self): + with patch.dict(os.environ, {"CLI_HUB_NO_ANALYTICS": "1"}): + assert not _is_enabled() + + def test_analytics_disabled_by_true(self): + with patch.dict(os.environ, {"CLI_HUB_NO_ANALYTICS": "true"}): + assert not _is_enabled() + + @patch("cli_hub.analytics._send_event") + def test_track_event_sends_request(self, mock_send): + with patch.dict(os.environ, {}, clear=True): + track_event("test-event", data={"key": "value"}) + import time + time.sleep(0.2) + mock_send.assert_called_once() + payload = mock_send.call_args[0][0] + assert payload["payload"]["name"] == "test-event" + assert payload["payload"]["hostname"] == "clianything.cc" + + @patch("cli_hub.analytics._send_event") + def test_track_event_noop_when_disabled(self, mock_send): + with patch.dict(os.environ, {"CLI_HUB_NO_ANALYTICS": "1"}): + track_event("test-event") + import time + time.sleep(0.2) + mock_send.assert_not_called() + + @patch("cli_hub.analytics._send_event") + def test_track_install_event_name_includes_cli(self, mock_send): + """cli-install event name must include CLI name for dashboard visibility.""" + with patch.dict(os.environ, {}, clear=True): + track_install("gimp", "1.0.0") + import time + time.sleep(0.2) + mock_send.assert_called_once() + payload = mock_send.call_args[0][0] + assert payload["payload"]["name"] == "cli-install:gimp" + assert payload["payload"]["url"] == "/cli-anything-hub/install/gimp" + assert payload["payload"]["data"]["cli"] == "gimp" + assert payload["payload"]["data"]["version"] == "1.0.0" + assert "platform" in payload["payload"]["data"] + + @patch("cli_hub.analytics._send_event") + def test_track_uninstall_event_name_includes_cli(self, mock_send): + """cli-uninstall event name must include CLI name for dashboard visibility.""" + with patch.dict(os.environ, {}, clear=True): + analytics_track_uninstall("blender") + import time + time.sleep(0.2) + mock_send.assert_called_once() + payload = mock_send.call_args[0][0] + assert payload["payload"]["name"] == "cli-uninstall:blender" + assert payload["payload"]["url"] == "/cli-anything-hub/uninstall/blender" + assert payload["payload"]["data"]["cli"] == "blender" + + @patch("cli_hub.analytics._send_event") + def test_track_visit_human(self, mock_send): + """visit-human event sent when not detected as agent.""" + with patch.dict(os.environ, {}, clear=True): + track_visit(is_agent=False) + import time + time.sleep(0.2) + mock_send.assert_called_once() + payload = mock_send.call_args[0][0] + assert payload["payload"]["name"] == "visit-human" + assert payload["payload"]["url"] == "/cli-anything-hub" + assert payload["payload"]["data"]["source"] == "cli-anything-hub" + + @patch("cli_hub.analytics._send_event") + def test_track_visit_agent(self, mock_send): + """visit-agent event sent when agent environment detected.""" + with patch.dict(os.environ, {}, clear=True): + track_visit(is_agent=True) + import time + time.sleep(0.2) + mock_send.assert_called_once() + payload = mock_send.call_args[0][0] + assert payload["payload"]["name"] == "visit-agent" + + def test_detect_agent_claude_code(self): + with patch.dict(os.environ, {"CLAUDE_CODE": "1"}): + assert _detect_is_agent() is True + + def test_detect_agent_codex(self): + with patch.dict(os.environ, {"CODEX": "1"}): + assert _detect_is_agent() is True + + def test_detect_not_agent_clean_env(self): + """Clean env with a tty should not detect as agent.""" + with patch.dict(os.environ, {}, clear=True): + with patch("sys.stdin") as mock_stdin: + mock_stdin.isatty.return_value = True + assert _detect_is_agent() is False + + @patch("cli_hub.analytics._send_event") + def test_first_run_sends_event(self, mock_send, tmp_path): + """First invocation sends cli-hub-installed event.""" + with patch.dict(os.environ, {"HOME": str(tmp_path)}, clear=False): + track_first_run() + import time + time.sleep(0.2) + mock_send.assert_called_once() + payload = mock_send.call_args[0][0] + assert payload["payload"]["name"] == "cli-anything-hub-installed" + assert payload["payload"]["url"] == "/cli-anything-hub/installed" + # Marker file should now exist + assert (tmp_path / ".cli-hub" / ".first_run_sent").exists() + + @patch("cli_hub.analytics._send_event") + def test_first_run_skips_if_marker_exists(self, mock_send, tmp_path): + """Second invocation does NOT send cli-hub-installed event.""" + cli_hub_dir = tmp_path / ".cli-hub" + cli_hub_dir.mkdir() + (cli_hub_dir / ".first_run_sent").write_text("0.1.0") + with patch.dict(os.environ, {"HOME": str(tmp_path)}, clear=False): + track_first_run() + import time + time.sleep(0.2) + mock_send.assert_not_called() + + +# โ”€โ”€โ”€ CLI tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +class TestCLI: + """Tests for the Click CLI interface.""" + + def setup_method(self): + self.runner = click.testing.CliRunner() + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + def test_version(self, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["--version"]) + assert __version__ in result.output + assert result.exit_code == 0 + mock_visit.assert_called_once_with(is_agent=False) + mock_first_run.assert_called_once() + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + def test_help(self, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["--help"]) + assert "cli-hub" in result.output + assert result.exit_code == 0 + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.fetch_registry", return_value=SAMPLE_REGISTRY) + @patch("cli_hub.cli.get_installed", return_value={}) + def test_list_command(self, mock_installed, mock_fetch, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["list"]) + assert "gimp" in result.output + assert "blender" in result.output + assert result.exit_code == 0 + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.fetch_registry", return_value=SAMPLE_REGISTRY) + @patch("cli_hub.cli.get_installed", return_value={}) + def test_list_with_category(self, mock_installed, mock_fetch, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["list", "-c", "image"]) + assert "gimp" in result.output + assert "blender" not in result.output + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.search_clis", return_value=[SAMPLE_REGISTRY["clis"][0]]) + @patch("cli_hub.cli.get_installed", return_value={}) + def test_search_command(self, mock_installed, mock_search, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["search", "gimp"]) + assert "gimp" in result.output + assert result.exit_code == 0 + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.get_cli", return_value=SAMPLE_REGISTRY["clis"][0]) + @patch("cli_hub.cli.get_installed", return_value={}) + def test_info_command(self, mock_installed, mock_get, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["info", "gimp"]) + assert "GIMP" in result.output + assert "image" in result.output + assert result.exit_code == 0 + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.get_cli", return_value=None) + def test_info_not_found(self, mock_get, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["info", "nonexistent"]) + assert result.exit_code == 1 + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.track_install") + @patch("cli_hub.cli.install_cli", return_value=(True, "Installed GIMP (cli-anything-gimp)")) + @patch("cli_hub.cli.get_cli", return_value=SAMPLE_REGISTRY["clis"][0]) + def test_install_command(self, mock_get, mock_install, mock_track, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["install", "gimp"]) + assert result.exit_code == 0 + assert "Installed" in result.output + mock_track.assert_called_once() + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=False) + @patch("cli_hub.cli.track_uninstall") + @patch("cli_hub.cli.uninstall_cli", return_value=(True, "Uninstalled GIMP")) + def test_uninstall_command(self, mock_uninstall, mock_track, mock_detect, mock_visit, mock_first_run): + result = self.runner.invoke(main, ["uninstall", "gimp"]) + assert result.exit_code == 0 + mock_track.assert_called_once() + + @patch("cli_hub.cli.track_first_run") + @patch("cli_hub.cli.track_visit") + @patch("cli_hub.cli._detect_is_agent", return_value=True) + def test_visit_agent_on_invocation(self, mock_detect, mock_visit, mock_first_run): + """When agent env detected, track_visit is called with is_agent=True.""" + result = self.runner.invoke(main, ["--version"]) + mock_visit.assert_called_once_with(is_agent=True) diff --git a/docs/hub/SKILL.md b/docs/hub/SKILL.md deleted file mode 100644 index 95e8caa70..000000000 --- a/docs/hub/SKILL.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: cli-anything-hub -description: >- - Browse and install 21+ agent-native CLI tools for GUI software. - Covers image editing, 3D, video, audio, office, diagrams, AI, and more. ---- - -# CLI-Anything Hub - -Agent-native stateful CLI interfaces for 21 applications. All CLIs support `--json` output, REPL mode, and undo/redo. - -## Quick Install - -```bash -# Install any CLI -pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=/agent-harness - -# Example: Install GIMP CLI -pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness -``` - -## Available CLIs - -### 3D - -| Name | Description | Install | -|------|-------------|---------| -| **Blender** | 3D modeling, animation, and rendering via blender --background --python | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness` | - -### Ai - -| Name | Description | Install | -|------|-------------|---------| -| **ComfyUI** | AI image generation workflow management via ComfyUI REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=comfyui/agent-harness` | -| **NotebookLM** | Experimental NotebookLM harness scaffold wrapping the installed notebooklm CLI for notebook, source, chat, artifact, download, and sharing workflows | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=notebooklm/agent-harness` | -| **Novita** | Access AI models via Novita's OpenAI-compatible API (DeepSeek, GLM, MiniMax) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=novita/agent-harness` | -| **Ollama** | Local LLM inference and model management via Ollama REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=ollama/agent-harness` | - -### Audio - -| Name | Description | Install | -|------|-------------|---------| -| **Audacity** | Audio editing and processing via sox | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness` | - -### Communication - -| Name | Description | Install | -|------|-------------|---------| -| **Zoom** | Meeting management via Zoom REST API (OAuth2) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=zoom/agent-harness` | - -### Design - -| Name | Description | Install | -|------|-------------|---------| -| **Sketch** | Generate Sketch design files (.sketch) from JSON design specifications via sketch-constructor | `cd sketch/agent-harness && npm install && npm link` | - -### Diagrams - -| Name | Description | Install | -|------|-------------|---------| -| **Draw.io** | Diagram creation and export via draw.io CLI | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=drawio/agent-harness` | -| **Mermaid** | Mermaid Live Editor state files and renderer URLs | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mermaid/agent-harness` | - -### Generation - -| Name | Description | Install | -|------|-------------|---------| -| **AnyGen** | Generate docs, slides, websites and more via AnyGen cloud API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=anygen/agent-harness` | - -### Image - -| Name | Description | Install | -|------|-------------|---------| -| **GIMP** | Raster image processing via gimp -i -b (batch mode) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness` | -| **Inkscape** | SVG vector graphics with export via inkscape --export-filename | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=inkscape/agent-harness` | - -### Music - -| Name | Description | Install | -|------|-------------|---------| -| **MuseScore** | CLI for music notation โ€” transpose, export PDF/audio/MIDI, extract parts, manage instruments | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=musescore/agent-harness` | - -### Network - -| Name | Description | Install | -|------|-------------|---------| -| **AdGuardHome** | DNS ad-blocking and network infrastructure management via AdGuardHome REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=adguardhome/agent-harness` | - -### Office - -| Name | Description | Install | -|------|-------------|---------| -| **LibreOffice** | Create and manipulate ODF documents, export to PDF/DOCX/XLSX/PPTX via headless mode | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=libreoffice/agent-harness` | -| **Mubu** | Knowledge management and outlining via local Mubu desktop data | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mubu/agent-harness` | - -### Streaming - -| Name | Description | Install | -|------|-------------|---------| -| **OBS Studio** | Create and manage streaming/recording scenes via command line | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=obs-studio/agent-harness` | - -### Video - -| Name | Description | Install | -|------|-------------|---------| -| **Kdenlive** | Video editing and rendering via melt | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=kdenlive/agent-harness` | -| **Shotcut** | Video editing and rendering via melt/ffmpeg | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=shotcut/agent-harness` | - -### Web - -| Name | Description | Install | -|------|-------------|---------| -| **Browser** | Browser automation via DOMShell MCP server. Maps Chrome's Accessibility Tree to a virtual filesystem for agent-native navigation. | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=browser/agent-harness` | - -## Usage Pattern - -All CLIs follow the same pattern: - -```bash -# Interactive REPL -cli-anything- - -# One-shot command -cli-anything- [options] - -# JSON output for agents -cli-anything- --json -``` - -## For AI Agents - -1. Install the CLI you need from the table above -2. Read its full SKILL.md at the repo path shown in registry.json -3. Always use `--json` flag for machine-readable output -4. Check exit codes (0=success, non-zero=error) - -## More Info - -- Repository: https://github.com/HKUDS/CLI-Anything -- Web Hub: https://hkuds.github.io/CLI-Anything/ -- Last Updated: 2026-03-18 diff --git a/docs/hub/SKILL.txt b/docs/hub/SKILL.txt deleted file mode 100644 index 95e8caa70..000000000 --- a/docs/hub/SKILL.txt +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: cli-anything-hub -description: >- - Browse and install 21+ agent-native CLI tools for GUI software. - Covers image editing, 3D, video, audio, office, diagrams, AI, and more. ---- - -# CLI-Anything Hub - -Agent-native stateful CLI interfaces for 21 applications. All CLIs support `--json` output, REPL mode, and undo/redo. - -## Quick Install - -```bash -# Install any CLI -pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=/agent-harness - -# Example: Install GIMP CLI -pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness -``` - -## Available CLIs - -### 3D - -| Name | Description | Install | -|------|-------------|---------| -| **Blender** | 3D modeling, animation, and rendering via blender --background --python | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness` | - -### Ai - -| Name | Description | Install | -|------|-------------|---------| -| **ComfyUI** | AI image generation workflow management via ComfyUI REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=comfyui/agent-harness` | -| **NotebookLM** | Experimental NotebookLM harness scaffold wrapping the installed notebooklm CLI for notebook, source, chat, artifact, download, and sharing workflows | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=notebooklm/agent-harness` | -| **Novita** | Access AI models via Novita's OpenAI-compatible API (DeepSeek, GLM, MiniMax) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=novita/agent-harness` | -| **Ollama** | Local LLM inference and model management via Ollama REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=ollama/agent-harness` | - -### Audio - -| Name | Description | Install | -|------|-------------|---------| -| **Audacity** | Audio editing and processing via sox | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness` | - -### Communication - -| Name | Description | Install | -|------|-------------|---------| -| **Zoom** | Meeting management via Zoom REST API (OAuth2) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=zoom/agent-harness` | - -### Design - -| Name | Description | Install | -|------|-------------|---------| -| **Sketch** | Generate Sketch design files (.sketch) from JSON design specifications via sketch-constructor | `cd sketch/agent-harness && npm install && npm link` | - -### Diagrams - -| Name | Description | Install | -|------|-------------|---------| -| **Draw.io** | Diagram creation and export via draw.io CLI | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=drawio/agent-harness` | -| **Mermaid** | Mermaid Live Editor state files and renderer URLs | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mermaid/agent-harness` | - -### Generation - -| Name | Description | Install | -|------|-------------|---------| -| **AnyGen** | Generate docs, slides, websites and more via AnyGen cloud API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=anygen/agent-harness` | - -### Image - -| Name | Description | Install | -|------|-------------|---------| -| **GIMP** | Raster image processing via gimp -i -b (batch mode) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness` | -| **Inkscape** | SVG vector graphics with export via inkscape --export-filename | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=inkscape/agent-harness` | - -### Music - -| Name | Description | Install | -|------|-------------|---------| -| **MuseScore** | CLI for music notation โ€” transpose, export PDF/audio/MIDI, extract parts, manage instruments | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=musescore/agent-harness` | - -### Network - -| Name | Description | Install | -|------|-------------|---------| -| **AdGuardHome** | DNS ad-blocking and network infrastructure management via AdGuardHome REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=adguardhome/agent-harness` | - -### Office - -| Name | Description | Install | -|------|-------------|---------| -| **LibreOffice** | Create and manipulate ODF documents, export to PDF/DOCX/XLSX/PPTX via headless mode | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=libreoffice/agent-harness` | -| **Mubu** | Knowledge management and outlining via local Mubu desktop data | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mubu/agent-harness` | - -### Streaming - -| Name | Description | Install | -|------|-------------|---------| -| **OBS Studio** | Create and manage streaming/recording scenes via command line | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=obs-studio/agent-harness` | - -### Video - -| Name | Description | Install | -|------|-------------|---------| -| **Kdenlive** | Video editing and rendering via melt | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=kdenlive/agent-harness` | -| **Shotcut** | Video editing and rendering via melt/ffmpeg | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=shotcut/agent-harness` | - -### Web - -| Name | Description | Install | -|------|-------------|---------| -| **Browser** | Browser automation via DOMShell MCP server. Maps Chrome's Accessibility Tree to a virtual filesystem for agent-native navigation. | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=browser/agent-harness` | - -## Usage Pattern - -All CLIs follow the same pattern: - -```bash -# Interactive REPL -cli-anything- - -# One-shot command -cli-anything- [options] - -# JSON output for agents -cli-anything- --json -``` - -## For AI Agents - -1. Install the CLI you need from the table above -2. Read its full SKILL.md at the repo path shown in registry.json -3. Always use `--json` flag for machine-readable output -4. Check exit codes (0=success, non-zero=error) - -## More Info - -- Repository: https://github.com/HKUDS/CLI-Anything -- Web Hub: https://hkuds.github.io/CLI-Anything/ -- Last Updated: 2026-03-18 diff --git a/docs/hub/index.html b/docs/hub/index.html index 1499b77e2..5bc4fa80e 100644 --- a/docs/hub/index.html +++ b/docs/hub/index.html @@ -161,9 +161,18 @@ .hero-tagline strong { color: var(--text); font-weight: 600; } /* โ”€โ”€ Empower card โ”€โ”€ */ - .empower-card { - max-width: 560px; + .empower-row { + display: flex; + gap: 1rem; + max-width: 1100px; margin: 0 auto 1.5rem; + align-items: stretch; + } + + .empower-card { + flex: 1; + min-width: 0; + margin: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; @@ -227,6 +236,35 @@ .empower-prompt strong { color: var(--text-secondary); font-weight: 600; } .empower-prompt em { color: var(--accent); font-style: italic; } + .empower-card.empower-self::before { background: var(--green); } + .empower-card.empower-self h3 span { color: var(--green); } + .empower-self .cmd-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.4rem; + } + .empower-self .cmd-item { + display: flex; + align-items: center; + gap: 0.4rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.4rem 0.65rem; + font-size: 0.76rem; + } + .empower-self .cmd-item:hover { border-color: var(--text-tertiary); } + .empower-self .cmd-item .cmd-name { + color: var(--green); + font-family: 'JetBrains Mono', monospace; + font-weight: 500; + white-space: nowrap; + } + .empower-self .cmd-item .cmd-desc { + color: var(--text-tertiary); + font-size: 0.72rem; + } + .hero-actions { display: flex; justify-content: center; @@ -547,10 +585,11 @@ font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--text-secondary); - white-space: nowrap; + white-space: pre-line; overflow: hidden; text-overflow: ellipsis; flex: 1; + line-height: 1.6; } .card-copy-btn { @@ -732,7 +771,9 @@ .filter-row { padding: 0.75rem 1.25rem 0; } .nav { padding: 0.75rem 1.25rem; } .footer { padding: 1.5rem 1.25rem; flex-direction: column; align-items: flex-start; } - .empower-card { margin: 0 auto 1.25rem; padding: 1.25rem; } + .empower-row { flex-direction: column; } + .empower-card { margin: 0 0 1rem; padding: 1.25rem; } + .empower-self .cmd-grid { grid-template-columns: 1fr; } } @media (max-width: 400px) { @@ -776,6 +817,7 @@

CLI-Anything Hub

Any software. Any codebase. Any Web API. Generate an agent-native CLI and let AI agents operate it — install with a single pip command.

+

Empower your agents — install in one command

@@ -793,6 +835,23 @@
+
+

Empower yourself — your CLI toolkit

+
+ pip install cli-anything-hub + +
+
+
cli-hub listBrowse all CLIs
+
cli-hub searchFind by keyword
+
cli-hub installInstall a CLI
+
cli-hub infoCLI details
+
cli-hub updateUpdate a CLI
+
cli-hub uninstallRemove a CLI
+
+
+
+
Agent SKILL (SKILL.txt) @@ -1031,8 +1090,8 @@ ${dateHtml} ${requiresHtml}
- ${esc(c.install_cmd)} - + pip install cli-anything-hub\ncli-hub install ${esc(c.name)} +