initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""CLI entry point."""
|
||||
|
||||
import typer
|
||||
|
||||
from cli_tools import credentials, docker, install
|
||||
|
||||
app = typer.Typer(help="Dev environment CLI tools.", no_args_is_help=True)
|
||||
app.add_typer(install.app, name="install")
|
||||
app.add_typer(docker.app, name="docker")
|
||||
app.add_typer(credentials.app, name="credentials")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Credential management commands."""
|
||||
|
||||
import typer
|
||||
|
||||
from cli_tools.helpers import run
|
||||
|
||||
app = typer.Typer(help="Encrypt and decrypt credentials.", no_args_is_help=True)
|
||||
|
||||
|
||||
@app.command("encrypt-env")
|
||||
def encrypt_env():
|
||||
"""Encrypt .env -> .env.gpg."""
|
||||
run("gpg -c .env")
|
||||
|
||||
|
||||
@app.command("decrypt-env")
|
||||
def decrypt_env():
|
||||
"""Decrypt .env.gpg -> .env."""
|
||||
run("gpg -d .env.gpg > .env")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Docker image commands."""
|
||||
|
||||
import os
|
||||
|
||||
import typer
|
||||
|
||||
from cli_tools.helpers import run
|
||||
|
||||
app = typer.Typer(help="Build and manage Docker images.", no_args_is_help=True)
|
||||
|
||||
DOCKER_IMAGE = "python-dev"
|
||||
DOCKER_DIR = "docker/python-dev"
|
||||
|
||||
|
||||
@app.command()
|
||||
def build(
|
||||
tag: str = typer.Option("latest", help="Image tag."),
|
||||
no_cache: bool = typer.Option(False, "--no-cache", help="Build without cache."),
|
||||
):
|
||||
"""Build the python-dev Docker image locally."""
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
full_tag = f"{DOCKER_IMAGE}:{tag}"
|
||||
run(f"cp ai-tools/claude/CLAUDE.md {DOCKER_DIR}/CLAUDE.md")
|
||||
run(f"cp scripts/aliases.sh {DOCKER_DIR}/aliases.sh")
|
||||
run(f"cp scripts/bash_helpers.sh {DOCKER_DIR}/bash_helpers.sh")
|
||||
run(
|
||||
f"docker build -t {full_tag} "
|
||||
f"--build-arg UID={uid} --build-arg GID={gid} "
|
||||
f"{'--no-cache ' if no_cache else ''}"
|
||||
f"--network=host {DOCKER_DIR}",
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Shared helpers for CLI commands."""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
YELLOW = "\033[33m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
BASHRC = Path("~/.bashrc").expanduser()
|
||||
SNIPPETS_DIR = Path(__file__).parent.parent.parent / "snippets"
|
||||
|
||||
|
||||
def load_snippet(name: str) -> str:
|
||||
"""Read a shell snippet from the snippets directory."""
|
||||
return (SNIPPETS_DIR / f"{name}.sh").read_text()
|
||||
|
||||
|
||||
def run(cmd: str, pty: bool = False) -> None:
|
||||
"""Print command in yellow then execute it."""
|
||||
print(f"{YELLOW}$ {cmd}{RESET}")
|
||||
subprocess.run(cmd, shell=True, check=True)
|
||||
|
||||
|
||||
def append_bashrc_section(marker: str, content: str) -> None:
|
||||
"""Idempotently add a named section to ~/.bashrc."""
|
||||
begin = f"# BEGIN {marker}"
|
||||
end = f"# END {marker}"
|
||||
section = f"\n{begin}\n{content.strip()}\n{end}\n"
|
||||
|
||||
text = BASHRC.read_text()
|
||||
text = re.sub(
|
||||
rf"\n?{re.escape(begin)}.*?{re.escape(end)}\n?", "", text, flags=re.DOTALL
|
||||
)
|
||||
BASHRC.write_text(text + section)
|
||||
@@ -0,0 +1,190 @@
|
||||
"""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",
|
||||
"tldr",
|
||||
]
|
||||
|
||||
|
||||
@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 bootstrap():
|
||||
"""Install all base tools."""
|
||||
apt_packages()
|
||||
docker()
|
||||
uv()
|
||||
claude()
|
||||
fzf()
|
||||
zoxide()
|
||||
lazygit()
|
||||
eza()
|
||||
ai_skills()
|
||||
Reference in New Issue
Block a user