Files
cli-tools/src/cli_tools/install.py
T

197 lines
5.7 KiB
Python

"""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",
]
@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 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))}")
@app.command()
def core():
"""Install essential tools."""
apt_packages()
docker()
uv()
fzf()
zoxide()
lazygit()
eza()
helpers()
@app.command()
def ai_tools():
"""Install AI tools and configure skills."""
claude()
copilot()
ai_skills()
ccusage()