"""Install commands.""" import shlex from pathlib import Path import typer from cli_tools.helpers import append_bashrc_section, run app = typer.Typer( help="Install tools and configure the environment.", no_args_is_help=True ) APT_PACKAGES = [ "git", "curl", "micro", "mc", "detox", "tree", "ripgrep", "fd-find", "btop", ] PYTHON_TOOLS = [ "cruft", "invoke", ] @app.command() def core(): """Install essential tools.""" apt_packages() docker() uv() uv_tools() fzf() zoxide() lazygit() eza() helpers() @app.command() def ai_tools(): """Install AI tools and configure skills.""" claude() copilot() ai_skills() ccusage() @app.command() def claude(): """Install Claude Code via the official install script.""" run("curl -fsSL https://claude.ai/install.sh | bash") @app.command() def copilot(): """Install GitHub Copilot via the official install script.""" run("curl -fsSL https://gh.io/copilot-install | bash") @app.command("apt-packages") def apt_packages(): """Update apt cache and install base packages.""" run("sudo apt-get update") run(f"sudo apt-get install -y {' '.join(APT_PACKAGES)}") @app.command() def docker(): """Install Docker and add current user to docker group.""" import getpass run( "command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sudo sh" ) run(f"sudo usermod -aG docker {getpass.getuser()}") @app.command() def uv(): """Install uv for the current user.""" run("test -f ~/.local/bin/uv || curl -LsSf https://astral.sh/uv/install.sh | sh") @app.command() def uv_tools(): """Install Python tools using uv.""" for tool in PYTHON_TOOLS: run(f"uv tool install --force {tool}") @app.command() def ccusage(): """Install ccusage for monitoring code complexity.""" run("npm install -g ccusage") @app.command() def fzf(): """Install fzf from git and bat for preview.""" run("sudo apt-get remove -y fzf 2>/dev/null || true") run("sudo apt-get install -y bat") run( "test -d ~/.fzf || git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf" ) run("~/.fzf/install --all") @app.command() def zoxide(): """Install zoxide and configure .bashrc.""" run("sudo apt install -y zoxide") append_bashrc_section("zoxide", 'eval "$(zoxide init bash)"') @app.command() def helpers(): """Symlink bash_helpers.sh and aliases.sh into home directory.""" repo = Path(__file__).resolve().parents[2] bash_helpers = repo / "scripts" / "bash_helpers.sh" aliases = repo / "scripts" / "aliases.sh" if not bash_helpers.is_file() or not aliases.is_file(): typer.secho( "This command must be run from the checked-out cli-tools repo " "(for example with `uv run cli-tools install helpers`).", fg=typer.colors.RED, err=True, ) raise typer.Exit(code=1) run(f"ln -sf {shlex.quote(str(bash_helpers))} ~/.bash_helpers.sh") run(f"ln -sf {shlex.quote(str(aliases))} ~/.aliases.sh") append_bashrc_section("bash_helpers", "source ~/.bash_helpers.sh") @app.command() def lazygit(): """Install lazygit (terminal UI for git).""" run( "command -v lazygit >/dev/null 2>&1 || (" "LAZYGIT_VERSION=$(curl -s https://api.github.com/repos/jesseduffield/lazygit/releases/latest" " | grep '\"tag_name\"' | cut -d'\"' -f4 | sed 's/v//') && " "curl -Lo /tmp/lazygit.tar.gz" " https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz && " "tar -xf /tmp/lazygit.tar.gz -C /tmp lazygit && " "sudo install /tmp/lazygit /usr/local/bin)" ) @app.command() def eza(): """Install eza (modern ls replacement) via official deb repo.""" run("sudo mkdir -p /etc/apt/keyrings") run( "wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc" " | sudo gpg --dearmor -o /etc/apt/keyrings/gierens.gpg" ) run( "echo 'deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main'" " | sudo tee /etc/apt/sources.list.d/gierens.list" ) run( "sudo chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list" ) run("sudo apt update && sudo apt install -y eza") @app.command("ai-skills") def ai_skills(): """Symlink Claude Code config, skills, agents, and Copilot skills globally.""" repo = Path(__file__).resolve().parents[2] claude_src = repo / "ai-tools" / "claude" skills_src = repo / "ai-tools" / "skills" agents_src = repo / "ai-tools" / "agents" claude_home = Path.home() / ".claude" claude_skills = Path.home() / ".claude" / "skills" claude_agents = Path.home() / ".claude" / "agents" github_skills = Path.home() / ".github" / "skills" claude_home.mkdir(parents=True, exist_ok=True) for src in sorted(claude_src.iterdir()): dest = claude_home / src.name run(f"ln -sfn {shlex.quote(str(src))} {shlex.quote(str(dest))}") claude_skills.mkdir(parents=True, exist_ok=True) for skill_dir in sorted(skills_src.iterdir()): if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists(): dest = claude_skills / skill_dir.name run(f"ln -sfn {shlex.quote(str(skill_dir))} {shlex.quote(str(dest))}") claude_agents.mkdir(parents=True, exist_ok=True) for agent_file in sorted(agents_src.glob("*.md")): dest = claude_agents / agent_file.name run(f"ln -sf {shlex.quote(str(agent_file))} {shlex.quote(str(dest))}") github_skills.mkdir(parents=True, exist_ok=True) for skill_dir in sorted(claude_skills.iterdir()): if skill_dir.is_dir(): dest = github_skills / skill_dir.name run(f"ln -sfn {shlex.quote(str(skill_dir))} {shlex.quote(str(dest))}")