mirror of
https://fastgit.cc/github.com/HKUDS/CLI-Anything
synced 2026-04-21 05:10:42 +08:00
232 lines
7.9 KiB
Python
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()
|