Files
2026-04-16 06:52:28 +00:00

232 lines
7.9 KiB
Python

"""cli-hub — CLI entry point."""
import os
import shutil
import click
from cli_hub import __version__
from cli_hub.registry import fetch_all_clis, 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 and public CLIs."""
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())
def _source_tag(cli):
"""Return a styled source indicator for display."""
source = cli.get("_source", "harness")
if source == "public":
manager = cli.get("package_manager") or cli.get("install_strategy") or "public"
return click.style(f" {manager}", fg="yellow")
return ""
@main.command()
@click.argument("name")
def install(name):
"""Install a CLI 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")
if cli:
click.echo(f" Run it with: {cli['entry_point']}")
click.echo(f" Or launch: cli-hub launch {cli['name']}")
if cli.get("_source") == "public" and cli.get("npx_cmd"):
click.echo(f" Or use npx: {cli['npx_cmd']}")
else:
click.secho(f"{msg}", fg="red", err=True)
raise SystemExit(1)
@main.command()
@click.argument("name")
def uninstall(name):
"""Uninstall a CLI 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 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("--source", "-s", default=None, type=click.Choice(["harness", "public", "npm", "all"], case_sensitive=False),
help="Filter by source (harness, public, or all). 'npm' is kept as an alias for public.")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
def list_clis(category, source, as_json):
"""List all available CLIs."""
try:
all_clis = fetch_all_clis()
except Exception as e:
click.secho(f"Failed to fetch registry: {e}", fg="red", err=True)
raise SystemExit(1)
clis = all_clis
if category:
clis = [c for c in clis if c.get("category", "").lower() == category.lower()]
if source == "npm":
source = "public"
if source and source != "all":
clis = [c for c in clis if c.get("_source", "harness") == source]
installed = get_installed()
if as_json:
import json as json_mod
click.echo(json_mod.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"][:55]
tag = _source_tag(cli)
click.echo(f" {marker} {name}{tag} {desc}")
total = len(clis)
inst = sum(1 for c in clis if c["name"] in installed)
harness_count = sum(1 for c in clis if c.get("_source") == "harness")
public_count = sum(1 for c in clis if c.get("_source") == "public")
click.echo(f"\n {total} CLIs available ({harness_count} harness, {public_count} public), {inst} installed")
cats = list_categories()
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 as json_mod
click.echo(json_mod.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")
tag = _source_tag(cli)
click.echo(f" {marker} {name} {cat}{tag}{cli['description'][:65]}")
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
source = cli.get("_source", "harness")
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" Source: {source}")
if source == "public":
click.echo(f" Install via: {cli.get('package_manager') or cli.get('install_strategy') or 'public'}")
if cli.get("npm_package"):
click.echo(f" npm package: {cli['npm_package']}")
if cli.get("npx_cmd"):
click.echo(f" npx command: {cli['npx_cmd']}")
if cli.get("install_cmd"):
click.echo(f" Install cmd: {cli['install_cmd']}")
if cli.get("install_notes"):
click.echo(f" Notes: {cli['install_notes']}")
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')}")
contributors = cli.get("contributors", [])
if contributors:
names = ", ".join(ct["name"] for ct in contributors)
click.echo(f" Contributors: {names}")
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()
@main.command()
@click.argument("name")
@click.argument("args", nargs=-1)
def launch(name, args):
"""Launch an installed CLI, passing through any extra arguments."""
cli = get_cli(name)
if not cli:
click.secho(f"CLI '{name}' not found in registry.", fg="red", err=True)
raise SystemExit(1)
entry = cli["entry_point"]
if not shutil.which(entry):
click.secho(
f"'{entry}' not found on PATH. Install it first: cli-hub install {name}",
fg="red",
err=True,
)
raise SystemExit(1)
os.execvp(entry, [entry] + list(args))
if __name__ == "__main__":
main()