initial commit

This commit is contained in:
Jev Kuznetsov
2026-04-16 11:36:48 +02:00
commit 60710fab20
30 changed files with 1460 additions and 0 deletions
View File
+13
View File
@@ -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()
+19
View File
@@ -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")
+32
View File
@@ -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}",
)
+35
View File
@@ -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)
+190
View File
@@ -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()