Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e3440ffce | |||
| 6f8b88898f | |||
| f488af4959 | |||
| f5811ac2e4 | |||
| 02a8ff08f9 | |||
| c50caa2a77 | |||
| 944e7b8538 |
+4
-43
@@ -1,44 +1,5 @@
|
|||||||
# Environment
|
temp/
|
||||||
.env
|
*.pyc
|
||||||
.env.gpg
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
# copied files
|
|
||||||
docker/python-dev/CLAUDE.md
|
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
ENV/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
.python-version
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
stages:
|
|
||||||
- build
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache git
|
|
||||||
- docker version
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
|
||||||
- docker buildx create --name mybuilder --use
|
|
||||||
- docker buildx inspect --bootstrap
|
|
||||||
script:
|
|
||||||
- docker buildx build --platform linux/amd64,linux/arm64 -t $CI_REGISTRY_IMAGE --push ./docker/python-dev
|
|
||||||
when: manual
|
|
||||||
only:
|
|
||||||
changes:
|
|
||||||
- docker/python-dev/**/*
|
|
||||||
refs:
|
|
||||||
- main
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run cli-tools --help # run the CLI
|
|
||||||
uv sync # install dependencies
|
|
||||||
uv run ruff check --fix src/ # lint
|
|
||||||
uv run ruff format src/ # format
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Typer-based CLI for bootstrapping a dev environment.
|
|
||||||
|
|
||||||
- `cli.py` — entry point, composes sub-apps
|
|
||||||
- `install.py` — install tools (fzf, zoxide, lazygit, eza, docker, uv, claude, helpers)
|
|
||||||
- `docker.py` — build the `python-dev` Docker image locally
|
|
||||||
- `credentials.py` — GPG encrypt/decrypt `.env` files
|
|
||||||
- `helpers.py` — `run()`, `append_bashrc_section()`, `load_snippet()`
|
|
||||||
|
|
||||||
Scripts: `scripts/bash_helpers.sh`, `scripts/aliases.sh` (symlinked to `~/`).
|
|
||||||
|
|
||||||
## Adding an install command
|
|
||||||
|
|
||||||
1. Add `@app.command()` in `install.py`
|
|
||||||
2. Use `run()` for shell commands, `append_bashrc_section()` for bashrc config
|
|
||||||
3. Optionally add to `bootstrap()` for first-time setup
|
|
||||||
@@ -1,132 +1,5 @@
|
|||||||
# CLI Tools
|
# CLI tools
|
||||||
|
|
||||||
Scripts and tools to set up and manage the terminal environment across systems.
|
This folder contains scripts and tools to setup and manage terminal environment across systems
|
||||||
|
|
||||||
**Entry point:** `cli-tools`
|
Main orchestration file: `tasks.py`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Run this tool from the checked-out repo. Do **not** install it with `uv tool install`,
|
|
||||||
because `cli-tools install helpers` symlinks shell files from `scripts/`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/cli-tools
|
|
||||||
uv run cli-tools --help
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cli-tools install bootstrap # install everything (first-time setup)
|
|
||||||
cli-tools install apt-packages # git, curl, ripgrep, fd, btop, tldr, ...
|
|
||||||
cli-tools install uv # uv package manager
|
|
||||||
cli-tools install fzf # fzf + bat
|
|
||||||
cli-tools install zoxide # zoxide (z navigation)
|
|
||||||
cli-tools install eza # modern ls replacement
|
|
||||||
cli-tools install lazygit # terminal UI for git
|
|
||||||
cli-tools install docker # Docker + add user to group
|
|
||||||
cli-tools install claude # Claude Code
|
|
||||||
cli-tools install helpers # symlink bash_helpers.sh + aliases.sh into ~
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cli-tools docker build # build python-dev image
|
|
||||||
cli-tools docker build --tag v1.2 --push # build and push with tag
|
|
||||||
```
|
|
||||||
|
|
||||||
## Credentials
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cli-tools credentials encrypt-env # .env → .env.gpg
|
|
||||||
cli-tools credentials decrypt-env # .env.gpg → .env
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installed Tools
|
|
||||||
|
|
||||||
### [eza](https://github.com/eza-community/eza) — modern `ls` replacement
|
|
||||||
|
|
||||||
Fast, colorized file listing with Git integration and icons.
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|--------------------|------------------------------------------|
|
|
||||||
| `ls` | list files (aliased to `eza`) |
|
|
||||||
| `ll` | long list with git status (`eza -la --git`) |
|
|
||||||
| `eza --tree` | tree view of directory |
|
|
||||||
| `eza -la --sort=modified` | sort by modification time |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [fzf](https://github.com/junegunn/fzf) — fuzzy finder
|
|
||||||
|
|
||||||
Interactive fuzzy search for files, history, and more. Integrates with shell key bindings.
|
|
||||||
|
|
||||||
| Shortcut / Command | Description |
|
|
||||||
|-------------------------|------------------------------------------|
|
|
||||||
| `Ctrl+R` | fuzzy search shell history |
|
|
||||||
| `Ctrl+T` | fuzzy find file and insert path |
|
|
||||||
| `Alt+C` | fuzzy cd into a directory |
|
|
||||||
| `fzf` | pipe any list into interactive selector |
|
|
||||||
| `vim $(fzf)` | open fuzzy-selected file in editor |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [lazygit](https://github.com/jesseduffield/lazygit) — terminal UI for Git
|
|
||||||
|
|
||||||
Full-featured Git UI in the terminal. Run `lazygit` in any repo.
|
|
||||||
|
|
||||||
| Key (inside lazygit) | Description |
|
|
||||||
|----------------------|--------------------------------------|
|
|
||||||
| `space` | stage / unstage file |
|
|
||||||
| `c` | commit |
|
|
||||||
| `P` (cap) | push |
|
|
||||||
| `P` | pull |
|
|
||||||
| `?` | help / key bindings |
|
|
||||||
| `q` | quit |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [zoxide](https://github.com/ajeetdsouza/zoxide) — smarter `cd`
|
|
||||||
|
|
||||||
Tracks your most-visited directories and lets you jump to them with partial names.
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|----------------|--------------------------------------------------|
|
|
||||||
| `z foo` | jump to the most frecent dir matching `foo` |
|
|
||||||
| `z foo bar` | match multiple terms |
|
|
||||||
| `zi` | interactive selection with fzf |
|
|
||||||
| `z -` | jump to previous directory |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Shell aliases (`aliases.sh`)
|
|
||||||
|
|
||||||
| Alias | Expands to |
|
|
||||||
|------------------|-----------------------------------|
|
|
||||||
| `reload` | `source ~/.bashrc` |
|
|
||||||
| `venv` | `source .venv/bin/activate` |
|
|
||||||
| `uvs` | `uv sync --all-extras` |
|
|
||||||
| `cat` | `batcat` (syntax-highlighted cat) |
|
|
||||||
| `bat` | `batcat` |
|
|
||||||
| `ls` | `eza` |
|
|
||||||
| `ll` | `eza -la --git` |
|
|
||||||
| `fd` | `fdfind` |
|
|
||||||
| `clip` | `xclip -selection clipboard` |
|
|
||||||
| `open_ports` | `sudo ss -tulwn \| grep LISTEN` |
|
|
||||||
| `docker_stop_all`| stop all running containers |
|
|
||||||
| `claude-allow` | claude --dangerously-skip-permissions |
|
|
||||||
|
|
||||||
## Shell functions (`bash_helpers.sh`)
|
|
||||||
|
|
||||||
| Function | Usage |
|
|
||||||
|------------------|--------------------------------------------|
|
|
||||||
| `attach <name>` | exec into a running Docker container |
|
|
||||||
| `python-dev` | start/attach python-dev container (`-p` pull, `-n` fresh) |
|
|
||||||
| `mount_ssh <host>`| mount remote dir via SSHFS |
|
|
||||||
| `canview` | launch CAN bus viewer (uses `$CAN_CHANNEL`) |
|
|
||||||
| `repl` | connect to serial device via picocom (uses `$AMPY_PORT`) |
|
|
||||||
| `find_and_cat <file>` | find file by name and print contents |
|
|
||||||
| `record_window` | record a window to mp4 via ffmpeg |
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: implementer
|
|
||||||
description: Implement a feature by writing tests, production code, and committing changes. Delegates from /develop.
|
|
||||||
model: sonnet
|
|
||||||
tools: Read, Edit, Write, Bash, Grep, Glob
|
|
||||||
skills:
|
|
||||||
- commit
|
|
||||||
---
|
|
||||||
|
|
||||||
Read only the minimum files needed. Write tests first, then production code. Commit atomically as you go using conventional commits. Run tests after each logical step. If stuck after 3 attempts on the same issue, report `[BLOCKED: reason]`.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
## Role
|
|
||||||
Senior Python developer assistant. Optimize for simple, maintainable code and low-token responses.
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
- Act on requests by default.
|
|
||||||
- Ask questions only if ambiguity affects correctness.
|
|
||||||
- Keep scope tight; do not add unrequested features.
|
|
||||||
- Propose a short plan (≤5 bullets) only when useful.
|
|
||||||
- If a request overcomplicates things, call it out and suggest a simpler option.
|
|
||||||
- Commit only when explicitly asked.
|
|
||||||
|
|
||||||
## Principles
|
|
||||||
- KISS, YAGNI.
|
|
||||||
- Explicit > implicit; readability counts.
|
|
||||||
- Flat > nested; avoid deep abstractions.
|
|
||||||
- Sparse > dense; avoid clever one-liners.
|
|
||||||
- No speculative patterns or overengineering.
|
|
||||||
|
|
||||||
## Coding
|
|
||||||
- Python 3.10+ with type hints (PEP 604).
|
|
||||||
- Use `uv`
|
|
||||||
- Clear names; short docstrings for non-obvious parts.
|
|
||||||
- No placeholders unless immediately needed.
|
|
||||||
- Keep files ~300–500 lines when practical.
|
|
||||||
- Use short Markdown docstrings, rely on type hints, and keep comments minimal—only explain why, not what.
|
|
||||||
- Keep imports at top.
|
|
||||||
|
|
||||||
## Sub-agents
|
|
||||||
- Read minimum required files.
|
|
||||||
- Return concise summaries, not raw dumps.
|
|
||||||
- Treat as stateless; don’t pass full context unless needed.
|
|
||||||
- Don’t use for simple search or git tasks.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(uv sync:*)",
|
|
||||||
"Bash(uv run pytest:*)",
|
|
||||||
"Bash(inv lint:*)",
|
|
||||||
"Bash(inv test:*)"
|
|
||||||
],
|
|
||||||
"deny": [
|
|
||||||
"Read(./.venv/**)",
|
|
||||||
"Read(./__pycache__/**)",
|
|
||||||
"Read(./.env*)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"skipDangerousModePermissionPrompt": true
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: architect
|
|
||||||
description: Create or update docs/architecture.md from user stories and project context.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Architect
|
|
||||||
|
|
||||||
Produce `docs/architecture.md`.
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Read `docs/user_stories.md` and any existing `docs/architecture.md`.
|
|
||||||
2. Produce or update `docs/architecture.md` using the template in `assets/template.md`.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Make reasonable assumptions and flag each with `[ASSUMPTION]`.
|
|
||||||
- Ask only if a missing answer would materially change the architecture and cannot be inferred.
|
|
||||||
- Keep the document as short as reasonably possible.
|
|
||||||
- Mermaid diagrams only.
|
|
||||||
- Be specific: version numbers, concrete patterns, no "best practices" filler.
|
|
||||||
- If updating an existing document, preserve decisions that are still valid.
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Architecture: [Project Name]
|
|
||||||
|
|
||||||
## Problem and context
|
|
||||||
What problem this solves and for whom.
|
|
||||||
|
|
||||||
## Goals and non-goals
|
|
||||||
Numbered goals with measurable criteria. Explicit non-goals.
|
|
||||||
|
|
||||||
## Repository structure
|
|
||||||
Directory layout with one-line descriptions.
|
|
||||||
|
|
||||||
## System overview
|
|
||||||
One paragraph, then a Mermaid component diagram showing major components,
|
|
||||||
responsibilities, and communication paths.
|
|
||||||
|
|
||||||
## Technology stack
|
|
||||||
| Component | Technology | Version | Rationale |
|
|
||||||
|
|
||||||
## Module boundaries
|
|
||||||
For each module: what it owns, its public interface, and what it must NOT do.
|
|
||||||
Communication patterns between modules (sync/async, events, RPC).
|
|
||||||
|
|
||||||
## Key architectural decisions
|
|
||||||
For each important decision:
|
|
||||||
- **Decision:** what was chosen
|
|
||||||
- **Alternatives considered:** what else was evaluated
|
|
||||||
- **Rationale:** why this option
|
|
||||||
|
|
||||||
## Constraints and conventions
|
|
||||||
Tech stack rules, naming conventions, forbidden libraries, project-wide patterns.
|
|
||||||
|
|
||||||
## Open questions
|
|
||||||
Only unresolved items that must be decided later.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: commit
|
|
||||||
description: Stage and commit changes on the active feature branch using atomic conventional commits.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Commit
|
|
||||||
|
|
||||||
Create atomic conventional commits on the active feature branch.
|
|
||||||
|
|
||||||
## Commit format
|
|
||||||
|
|
||||||
- Format: `type(scope): description`
|
|
||||||
- Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`
|
|
||||||
- First line: imperative mood, lowercase, concise (e.g., "add login" not "added login").
|
|
||||||
- Body: only for complex changes; explain why, not what.
|
|
||||||
- Footer: `BREAKING CHANGE:` if applicable.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Scope is optional but encouraged when a module or file is the clear focus.
|
|
||||||
- If changes span multiple logical tasks, create separate commits.
|
|
||||||
- Each commit must be independently reviewable.
|
|
||||||
- Never combine unrelated changes in a single commit.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Stage specified files and create a commit.
|
|
||||||
# Usage: bash scripts/commit.sh --message "type(scope): description" [--] file1 file2 ...
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: bash scripts/commit.sh --message MESSAGE [--] FILE [FILE...]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --message MESSAGE Commit message (required)"
|
|
||||||
echo " --help Show this help"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " bash scripts/commit.sh --message \"feat(auth): add login endpoint\" src/auth.py tests/test_auth.py"
|
|
||||||
echo " bash scripts/commit.sh --message \"chore: update deps\" -- requirements.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
MESSAGE=""
|
|
||||||
FILES=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--help) usage; exit 0 ;;
|
|
||||||
--message)
|
|
||||||
[[ -z "${2:-}" ]] && { echo "Error: --message requires a value."; exit 1; }
|
|
||||||
MESSAGE="$2"; shift 2 ;;
|
|
||||||
--) shift; FILES+=("$@"); break ;;
|
|
||||||
-*) echo "Error: unknown option '$1'. Run with --help for usage."; exit 1 ;;
|
|
||||||
*) FILES+=("$1"); shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
CURRENT_BRANCH="$(git branch --show-current)"
|
|
||||||
|
|
||||||
if [[ -z "$CURRENT_BRANCH" ]]; then
|
|
||||||
echo "Error: not on a branch."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
||||||
echo "Error: refusing to commit directly on '$CURRENT_BRANCH'. Create or switch to a feature branch first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$MESSAGE" ]]; then
|
|
||||||
echo "Error: --message is required."
|
|
||||||
echo ""
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
|
||||||
echo "Error: at least one file must be specified."
|
|
||||||
echo ""
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
git add -- "${FILES[@]}"
|
|
||||||
git commit --message "$MESSAGE"
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Show branch status and full diff for commit planning.
|
|
||||||
# Usage: bash scripts/status.sh [BASE_BRANCH]
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--help" ]]; then
|
|
||||||
echo "Usage: bash scripts/status.sh [BASE_BRANCH]"
|
|
||||||
echo ""
|
|
||||||
echo "Prints the current branch, git status, and the full diff (staged and unstaged) for commit planning."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
BASE_BRANCH="${1:-main}"
|
|
||||||
CURRENT_BRANCH="$(git branch --show-current)"
|
|
||||||
|
|
||||||
echo "=== current branch ==="
|
|
||||||
echo "${CURRENT_BRANCH:-detached HEAD}"
|
|
||||||
|
|
||||||
if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1 && [[ -n "$CURRENT_BRANCH" && "$CURRENT_BRANCH" != "$BASE_BRANCH" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "=== commits ahead of $BASE_BRANCH ==="
|
|
||||||
git --no-pager log --oneline "$BASE_BRANCH..$CURRENT_BRANCH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== git status ==="
|
|
||||||
git status --short --branch
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== staged diff ==="
|
|
||||||
git diff --staged
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== unstaged diff ==="
|
|
||||||
git diff
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
---
|
|
||||||
name: develop
|
|
||||||
description: "Autopilot feature delivery: spec, implement, commit, review — stops for manual testing before merge."
|
|
||||||
---
|
|
||||||
|
|
||||||
# Develop
|
|
||||||
|
|
||||||
Deliver a feature end-to-end with minimal human intervention.
|
|
||||||
|
|
||||||
## Input
|
|
||||||
|
|
||||||
`/develop <description>` — a plain-English feature description or slug name.
|
|
||||||
|
|
||||||
If no description is provided:
|
|
||||||
1. Read `docs/feature_backlog.md` if it exists.
|
|
||||||
2. Pick the first `## NNN — title` item (lowest `NNN`).
|
|
||||||
3. Announce: "Next up: **NNN — title** — description. Starting now." then proceed.
|
|
||||||
4. If no backlog exists, ask: "What should I build?" and wait.
|
|
||||||
|
|
||||||
## Resuming an interrupted run
|
|
||||||
|
|
||||||
1. Run `git log --oneline` to see commits on the branch.
|
|
||||||
2. Check `Status` in `docs/features/<NNN-slug>.md`.
|
|
||||||
3. Skip completed phases, continue from the next one.
|
|
||||||
|
|
||||||
## Phase 0 — Setup
|
|
||||||
|
|
||||||
1. Derive a slug from the description (e.g. `user-auth`, `csv-export`).
|
|
||||||
2. Determine the numeric prefix: list files in `docs/features/` matching `[0-9][0-9][0-9]-*.md`, find the highest number, and use one higher. If no files exist, start at `001`. Format: `NNN-<slug>` (e.g. `001-user-auth`, `042-csv-export`).
|
|
||||||
3. Use the full prefixed slug everywhere: branch name (`feat/NNN-<slug>`), spec file (`docs/features/NNN-<slug>.md`), and commit messages.
|
|
||||||
4. Run `git checkout -b feat/<NNN-slug>` to create the feature branch. If it already exists, switch to it.
|
|
||||||
5. If the working tree is dirty, stash changes first with `git stash`, then pop after switching.
|
|
||||||
|
|
||||||
## Phase 1 — Spec
|
|
||||||
|
|
||||||
1. Read `docs/architecture.md` if it exists (not required).
|
|
||||||
2. Write a lightweight feature spec to `docs/features/<NNN-slug>.md` using the template in `assets/spec-template.md`.
|
|
||||||
3. Make reasonable assumptions — flag each with `[ASSUMPTION]` in the spec.
|
|
||||||
4. Ask only if a missing answer would materially change the result and cannot be inferred. Otherwise choose the simpler option and flag it.
|
|
||||||
5. Commit the spec: `docs(<NNN-slug>): add feature spec`
|
|
||||||
|
|
||||||
## Phase 2 — Implement + Commit
|
|
||||||
|
|
||||||
Delegate to the **implementer** subagent (Sonnet). Provide it with:
|
|
||||||
- The feature spec path
|
|
||||||
- The architecture doc path (if it exists)
|
|
||||||
- Instruction to write tests first, then production code
|
|
||||||
- Instruction to commit atomically as it goes (conventional commits)
|
|
||||||
- Instruction to run tests after each logical step
|
|
||||||
- Instruction to read only the minimum files needed
|
|
||||||
|
|
||||||
If the implementer reports `[BLOCKED]`, stop and report the blocker to the user.
|
|
||||||
|
|
||||||
**Copilot / single-thread mode:** run implementation directly — read the spec, write tests, implement, commit, run tests.
|
|
||||||
|
|
||||||
## Phase 3 — Review
|
|
||||||
|
|
||||||
1. Run the test suite (`inv test` or project-appropriate test command).
|
|
||||||
2. Run `git diff main...HEAD` to see all changes on the feature branch.
|
|
||||||
3. Compare against the feature spec:
|
|
||||||
- Are all acceptance criteria addressed?
|
|
||||||
- Do tests pass?
|
|
||||||
- Any obvious issues (broken imports, missing files, dead code)?
|
|
||||||
4. Decision:
|
|
||||||
- **PASS** → update spec status to `approved`. Stop and tell the user: "Feature branch `feat/<NNN-slug>` is ready. Test it, then run `/merge <NNN-slug>` to merge."
|
|
||||||
- **FIXABLE** → loop back to Phase 2 with specific fix instructions. Maximum **1 retry**.
|
|
||||||
- **BLOCKED** → update spec status to `blocked`, stop and report to user.
|
|
||||||
|
|
||||||
## When to stop
|
|
||||||
|
|
||||||
Stop and report to the user ONLY for:
|
|
||||||
- Contradictions between the feature description and existing architecture
|
|
||||||
- Test failures that can't be resolved after 3 attempts
|
|
||||||
- Merge conflicts that can't be auto-resolved
|
|
||||||
- Missing critical information that genuinely cannot be assumed
|
|
||||||
|
|
||||||
Do NOT stop for:
|
|
||||||
- Choosing between implementation approaches (pick the simpler one, flag with `[ASSUMPTION]`)
|
|
||||||
- Creating new files or modules (just do it)
|
|
||||||
- Commit message wording (use conventional commits)
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- No approval gates.
|
|
||||||
- Make assumptions and flag them.
|
|
||||||
- Atomic conventional commits throughout.
|
|
||||||
- Feature branches are temporary — created and merged automatically.
|
|
||||||
- If the project has no test infrastructure, skip test-related steps and note it in the review.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Feature: <Name>
|
|
||||||
|
|
||||||
Status: draft
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
One sentence: what this feature does and why it's needed.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- AC-01: ...
|
|
||||||
|
|
||||||
## Test Plan
|
|
||||||
|
|
||||||
- T-01 (AC-01): given ... / when ... / then ...
|
|
||||||
|
|
||||||
## Assumptions
|
|
||||||
|
|
||||||
- [ASSUMPTION]: ...
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
Optional: implementation hints, constraints, edge cases.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: merge
|
|
||||||
description: "Merge a reviewed feature branch to main and clean up."
|
|
||||||
---
|
|
||||||
|
|
||||||
# Merge
|
|
||||||
|
|
||||||
Merge a completed feature branch into `main` and delete it.
|
|
||||||
|
|
||||||
## Input
|
|
||||||
|
|
||||||
`/merge <NNN-slug>` — the prefixed feature slug (e.g. `042-csv-export`).
|
|
||||||
|
|
||||||
If no slug is provided, check the current branch (`git branch --show-current`). If it matches `feat/<NNN-slug>`, derive the slug from it. Otherwise, list feature branches and ask: "Which branch should I merge?"
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
1. Confirm the feature spec at `docs/features/<NNN-slug>.md` has status `approved`. If not, stop and report.
|
|
||||||
2. `git checkout main`
|
|
||||||
3. `git merge --no-ff feat/<NNN-slug>`
|
|
||||||
4. If merge conflicts occur, attempt auto-resolution. If that fails, stop and report the conflicting files.
|
|
||||||
5. `git branch -d feat/<NNN-slug>`
|
|
||||||
6. Report summary: feature name, number of commits merged, what was built.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
name: review
|
|
||||||
description: Audit the full codebase as a software architect. Score on 5 KPIs (Maintainability, Extensibility, Testability, Robustness, Clarity) and produce docs/review.md.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Review
|
|
||||||
|
|
||||||
Audit the codebase, score each KPI 0–10, and produce `docs/review.md`.
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Run the test suite to verify baseline health and gather coverage stats.
|
|
||||||
2. Analyze the codebase against the 5 KPIs below.
|
|
||||||
3. Produce `docs/review.md` using the template in `assets/template.md`.
|
|
||||||
|
|
||||||
## KPIs
|
|
||||||
|
|
||||||
1. **Maintainability** — How easily can the system be debugged, modified, or understood?
|
|
||||||
Metrics: modularity, cohesion, coupling, readability, simplicity.
|
|
||||||
|
|
||||||
2. **Extensibility** — How easily can new features be added without major refactoring?
|
|
||||||
Metrics: separation of concerns, dependency injection, use of interfaces/protocols.
|
|
||||||
|
|
||||||
3. **Testability** — How easily can components be tested in isolation and as a whole?
|
|
||||||
Metrics: pure functions, mockability, dependency inversion.
|
|
||||||
|
|
||||||
4. **Robustness** — How well does the system handle edge cases, errors, and real-world conditions?
|
|
||||||
Metrics: state management, predictability, fault tolerance.
|
|
||||||
|
|
||||||
5. **Clarity** — How quickly can a new developer understand the system's design and purpose?
|
|
||||||
Metrics: documentation, consistent naming, clear abstractions.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Be specific — cite `file:line` or `file:function` when pointing out issues.
|
|
||||||
- No filler — every bullet must be actionable or informative.
|
|
||||||
- Always include your model/version in the reviewer field.
|
|
||||||
- Do not modify any code. This is a read-only review.
|
|
||||||
- Keep the write-up concise.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Code Review
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| Date | YYYY-MM-DD |
|
|
||||||
| Reviewer | model name and version |
|
|
||||||
| LOC | total lines of code |
|
|
||||||
| Tests | count |
|
|
||||||
| Coverage | percentage (if available) |
|
|
||||||
|
|
||||||
## Maintainability — X/10
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## Extensibility — X/10
|
|
||||||
...
|
|
||||||
|
|
||||||
## Testability — X/10
|
|
||||||
...
|
|
||||||
|
|
||||||
## Robustness — X/10
|
|
||||||
...
|
|
||||||
|
|
||||||
## Clarity — X/10
|
|
||||||
...
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
One short paragraph on the overall state of the codebase.
|
|
||||||
|
|
||||||
## Priority recommendations
|
|
||||||
Top 5 actionable improvements, ordered by impact.
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
name: wbs
|
|
||||||
description: Break down architecture.md into a prioritized feature backlog, skipping already-built features.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Plan
|
|
||||||
|
|
||||||
Produce or update `docs/feature_backlog.md`.
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Read `docs/architecture.md`. This is the source of truth for what needs to be built.
|
|
||||||
2. Scan `docs/features/` for existing feature spec files (pattern: `[0-9][0-9][0-9]-*.md`). These are already built — do not include them in the backlog.
|
|
||||||
3. Find the highest `NNN` number across both `docs/features/` files and any existing `docs/feature_backlog.md` entries. New items start from one higher.
|
|
||||||
4. Produce `docs/feature_backlog.md` with this structure:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Feature Backlog
|
|
||||||
|
|
||||||
Completed features are tracked in `docs/features/` and removed from this list.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NNN — slug-style-title
|
|
||||||
|
|
||||||
One sentence describing what gets built.
|
|
||||||
|
|
||||||
## NNN — next-item
|
|
||||||
|
|
||||||
One sentence. Optionally:
|
|
||||||
depends: NNN
|
|
||||||
```
|
|
||||||
|
|
||||||
## What goes on the backlog
|
|
||||||
|
|
||||||
Cover everything in the architecture that is not yet implemented. Group logically — infrastructure first, then features, then integrations.
|
|
||||||
|
|
||||||
Do not add items for:
|
|
||||||
- Work already represented by a file in `docs/features/`
|
|
||||||
- Documentation, comments, or cleanup unless explicitly in the architecture
|
|
||||||
- Vague future ideas not grounded in the architecture
|
|
||||||
|
|
||||||
## Chunk sizing
|
|
||||||
|
|
||||||
Each backlog item will be handed to a coding agent that writes a spec, implements the code, and commits — all in one automated pass with no human in the loop. Size items accordingly:
|
|
||||||
|
|
||||||
- **Self-contained**: scope should contain a single functionality that can be implemented and tested.
|
|
||||||
- **Scoped**: touches at most 2 modules and adds roughly 50–200 lines of production code
|
|
||||||
- **Testable**: success is checkable by running the test suite — no manual inspection required
|
|
||||||
|
|
||||||
If an architectural feature is large, split it into ordered items (e.g. `data model`, then `API layer`, then `UI`).
|
|
||||||
If a feature is trivial (a single function or config value), merge it with a related item.
|
|
||||||
|
|
||||||
If an item depends on another, note it on a second line as `depends: NNN`.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Ask only if a trade-off materially changes scope or ordering and cannot be resolved from the architecture.
|
|
||||||
- Preserve items already in `docs/feature_backlog.md` that are not yet in `docs/features/` — only add, reorder, or remove items, do not rewrite existing descriptions without good reason.
|
|
||||||
- Each item is a `## NNN — slug-style-title` subheading followed by one sentence (and optionally a `depends:` line).
|
|
||||||
- Keep descriptions to one sentence — just enough for the coding agent to understand the scope.
|
|
||||||
- Do not mark any item as done. Completed work is tracked in `docs/features/`, not here.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
aliases.sh
|
|
||||||
bash_helpers.sh
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
FROM python:3.13
|
|
||||||
|
|
||||||
ARG USERNAME=dev
|
|
||||||
ARG UID=1000
|
|
||||||
ARG GID=1000
|
|
||||||
|
|
||||||
# Create user with sudo support
|
|
||||||
RUN groupadd --gid $GID $USERNAME \
|
|
||||||
&& useradd --uid $UID --gid $GID -m $USERNAME \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y sudo curl gnupg \
|
|
||||||
&& echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \
|
|
||||||
&& chmod 0440 /etc/sudoers.d/$USERNAME \
|
|
||||||
&& usermod -a -G dialout $USERNAME
|
|
||||||
|
|
||||||
# Install system packages
|
|
||||||
RUN apt-get install -y \
|
|
||||||
bat \
|
|
||||||
fd-find \
|
|
||||||
git \
|
|
||||||
git-lfs \
|
|
||||||
graphviz \
|
|
||||||
iputils-ping \
|
|
||||||
libgl1 \
|
|
||||||
locales \
|
|
||||||
make \
|
|
||||||
micro \
|
|
||||||
mosquitto-clients \
|
|
||||||
net-tools \
|
|
||||||
picocom \
|
|
||||||
ripgrep \
|
|
||||||
rsync \
|
|
||||||
socat \
|
|
||||||
tree \
|
|
||||||
zoxide \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install eza
|
|
||||||
RUN mkdir -p /etc/apt/keyrings \
|
|
||||||
&& wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \
|
|
||||||
| gpg --dearmor -o /etc/apt/keyrings/gierens.gpg \
|
|
||||||
&& echo 'deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main' \
|
|
||||||
> /etc/apt/sources.list.d/gierens.list \
|
|
||||||
&& chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list \
|
|
||||||
&& apt-get update && apt-get install -y eza \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install lazygit
|
|
||||||
RUN 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 \
|
|
||||||
&& install /tmp/lazygit /usr/local/bin \
|
|
||||||
&& rm /tmp/lazygit.tar.gz /tmp/lazygit
|
|
||||||
|
|
||||||
# Set locale
|
|
||||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
|
|
||||||
locale-gen
|
|
||||||
ENV LANG=en_US.UTF-8
|
|
||||||
ENV LC_ALL=en_US.UTF-8
|
|
||||||
|
|
||||||
# Create user directories with proper ownership
|
|
||||||
RUN mkdir -p /home/${USERNAME}/.vscode-server/extensions \
|
|
||||||
&& mkdir -p /home/${USERNAME}/.local/bin \
|
|
||||||
&& mkdir -p /home/${USERNAME}/.claude \
|
|
||||||
&& mkdir -p /workspace \
|
|
||||||
&& mkdir -p /workspace/.venv \
|
|
||||||
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.vscode-server \
|
|
||||||
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.local \
|
|
||||||
&& chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.claude \
|
|
||||||
&& chown -R ${USERNAME}:${USERNAME} /workspace
|
|
||||||
|
|
||||||
VOLUME /workspace/.venv
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Switch to non-root user (only once, for the rest of the build)
|
|
||||||
USER ${USERNAME}
|
|
||||||
WORKDIR /home/${USERNAME}
|
|
||||||
|
|
||||||
# Add local bin to PATH
|
|
||||||
ENV PATH="/home/${USERNAME}/.local/bin:${PATH}"
|
|
||||||
|
|
||||||
# Install uv
|
|
||||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
||||||
|
|
||||||
# Install fzf
|
|
||||||
RUN git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && ~/.fzf/install --all
|
|
||||||
|
|
||||||
# Install AI tools as user (frequently updated)
|
|
||||||
# copilot cli
|
|
||||||
RUN curl -fsSL https://gh.io/copilot-install | bash
|
|
||||||
|
|
||||||
# claude cli
|
|
||||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
||||||
|
|
||||||
# User config (most frequently changed)
|
|
||||||
COPY --chown=${USERNAME}:${USERNAME} CLAUDE.md /home/${USERNAME}/.claude/CLAUDE.md
|
|
||||||
COPY --chown=${USERNAME}:${USERNAME} aliases.sh /home/${USERNAME}/.aliases.sh
|
|
||||||
COPY --chown=${USERNAME}:${USERNAME} bash_helpers.sh /home/${USERNAME}/.bash_helpers.sh
|
|
||||||
|
|
||||||
# Customize bash prompt
|
|
||||||
RUN echo 'export PS1="${PROJECT_NAME:+\[\e[35m\][$PROJECT_NAME]\[\e[m\] }🐍 \[\e[33m\]\W\[\e[m\] \[\033[1;36m\]# \[\033[0m\]"' >> ~/.bashrc \
|
|
||||||
&& echo 'source ~/.bash_helpers.sh' >> ~/.bashrc \
|
|
||||||
&& echo 'eval "$(zoxide init bash)"' >> ~/.bashrc \
|
|
||||||
&& echo '[[ -f /workspace/init_container.sh ]] && source /workspace/init_container.sh' >> ~/.bashrc
|
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
BASHRC = "~/.bashrc"
|
||||||
|
SNIPPETS_DIR = Path(__file__).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(c, cmd: str) -> None:
|
||||||
|
"""Print command in yellow then execute it with a pty."""
|
||||||
|
print(f"{YELLOW}$ {cmd}{RESET}")
|
||||||
|
c.run(cmd, pty=True)
|
||||||
|
|
||||||
|
|
||||||
|
def append_bashrc_section(c, marker: str, content: str) -> None:
|
||||||
|
"""Idempotently add a named section to ~/.bashrc, replacing it if it already exists."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
begin = f"# BEGIN {marker}"
|
||||||
|
end = f"# END {marker}"
|
||||||
|
section = f"\n{begin}\n{content.strip()}\n{end}\n"
|
||||||
|
|
||||||
|
bashrc = Path(BASHRC).expanduser()
|
||||||
|
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)
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "cli-tools"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Dev environment CLI tools"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
dependencies = ["typer"]
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
cli-tools = "cli_tools.cli:app"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["uv_build"]
|
|
||||||
build-backend = "uv_build"
|
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"ruff>=0.15.9",
|
|
||||||
]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
alias venv='source .venv/bin/activate'
|
|
||||||
alias docker_stop_all='docker stop $(docker ps -a -q)'
|
|
||||||
alias uvs="uv sync --all-extras"
|
|
||||||
alias clip='xclip -selection clipboard'
|
|
||||||
alias open_ports='sudo ss -tulwn | grep LISTEN'
|
|
||||||
alias bat='batcat'
|
|
||||||
alias cat='batcat'
|
|
||||||
alias claude-allow="claude --dangerously-skip-permissions"
|
|
||||||
alias reload='source ~/.bashrc'
|
|
||||||
alias ls='eza'
|
|
||||||
alias ll='eza -la --git'
|
|
||||||
alias fd='fdfind'
|
|
||||||
alias lg='lazygit'
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This file contains helper functions for bash scripts.
|
|
||||||
# Source it in your bash script to use the functions.
|
|
||||||
|
|
||||||
#-------------------------------------
|
|
||||||
# Docker-related functions
|
|
||||||
|
|
||||||
# Attach to a running Docker container.
|
|
||||||
attach(){
|
|
||||||
docker exec -it "$1" bash
|
|
||||||
}
|
|
||||||
|
|
||||||
# Enter development shell in a Docker container using a Python image.
|
|
||||||
# Flags: --new/-n remove existing container first.
|
|
||||||
python-dev() {
|
|
||||||
local IMG="python-dev"
|
|
||||||
local NAME="python-dev"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--help|-h) echo "Usage: python-dev [--new|-n]"; echo " --new, -n Remove existing container before starting"; return 0 ;;
|
|
||||||
--new|-n) docker rm -f "$NAME" &>/dev/null; shift ;;
|
|
||||||
*) echo "Unknown option: $1"; return 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
local built
|
|
||||||
built="$(docker image inspect --format '{{.Created}}' "$IMG" 2>/dev/null)"
|
|
||||||
[[ -n "$built" ]] && echo "Image built: $(date -d "$built" '+%Y-%m-%d %H:%M:%S')"
|
|
||||||
|
|
||||||
local PROJECT_NAME
|
|
||||||
PROJECT_NAME="$(basename "$(pwd)")"
|
|
||||||
|
|
||||||
if docker container inspect "$NAME" &>/dev/null; then
|
|
||||||
local STATE
|
|
||||||
STATE="$(docker inspect -f '{{.State.Status}}' "$NAME")"
|
|
||||||
if [[ "$STATE" == "running" ]]; then
|
|
||||||
echo "Attaching to existing container '$NAME'"
|
|
||||||
docker exec -it -e PROJECT_NAME="$PROJECT_NAME" "$NAME" bash
|
|
||||||
else
|
|
||||||
echo "Restarting stopped container '$NAME'"
|
|
||||||
docker start -ai "$NAME"
|
|
||||||
fi
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Creating new container '$NAME'"
|
|
||||||
docker run -it \
|
|
||||||
--name="$NAME" \
|
|
||||||
--network=host \
|
|
||||||
-e PROJECT_NAME="$PROJECT_NAME" \
|
|
||||||
-v "$(pwd):/workspace" \
|
|
||||||
-v /workspace/.venv \
|
|
||||||
-v "$HOME/.ssh:/home/dev/.ssh:ro" \
|
|
||||||
-v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" \
|
|
||||||
-w /workspace "$IMG" \
|
|
||||||
bash
|
|
||||||
}
|
|
||||||
|
|
||||||
#-------------------------------------
|
|
||||||
# CAN and serial functions
|
|
||||||
|
|
||||||
# Launch CAN viewer for the given interface (default: slcan0).
|
|
||||||
canview() {
|
|
||||||
|
|
||||||
if [ -z "$CAN_CHANNEL" ]; then
|
|
||||||
echo "Warning: CAN_CHANNEL not set. Using default can0"
|
|
||||||
CAN_CHANNEL=can0
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -z "$CAN_INTERFACE" ]; then
|
|
||||||
echo "Warning: CAN_INTERFACE not set. Using default slcan0"
|
|
||||||
CAN_INTERFACE=socketcan
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 -m can.viewer -c "$CAN_CHANNEL" -i "$CAN_INTERFACE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Connect to a serial device using Picocom (default: /dev/ttyACM0).
|
|
||||||
repl(){
|
|
||||||
if [ -z "$AMPY_PORT" ]; then
|
|
||||||
echo "Warning: AMPY_PORT not set. Using default /dev/ttyACM0"
|
|
||||||
AMPY_PORT=/dev/ttyACM0
|
|
||||||
fi
|
|
||||||
echo "Connecting to $AMPY_PORT"
|
|
||||||
picocom -b 115200 $AMPY_PORT
|
|
||||||
}
|
|
||||||
|
|
||||||
#-------------------------------------
|
|
||||||
# SSHFS and network functions
|
|
||||||
|
|
||||||
# Mount a remote directory over SSHFS. Takes a remote path as an argument.
|
|
||||||
function mount_ssh() {
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "No argument provided."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Mounting $1"
|
|
||||||
|
|
||||||
mkdir -p "$1"
|
|
||||||
sshfs -o allow_other "$1:" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
#-------------------------------------
|
|
||||||
# Miscellaneous functions
|
|
||||||
|
|
||||||
# Find a file and display its contents.
|
|
||||||
find_and_cat() {
|
|
||||||
if [[ -z $1 ]]; then
|
|
||||||
echo "Usage: find_and_cat <filename>"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
find . -type f -name "$1" -print0 | while IFS= read -r -d '' file; do
|
|
||||||
echo -e "File Location: $file\n"
|
|
||||||
echo "File Contents:"
|
|
||||||
cat "$file"
|
|
||||||
echo -e "\n---\n"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# record_window
|
|
||||||
record_window() {
|
|
||||||
local outfile="recording_$(date +%F_%H-%M-%S).mp4"
|
|
||||||
|
|
||||||
echo "Click on the window you want to record..."
|
|
||||||
local wininfo=$(xwininfo)
|
|
||||||
|
|
||||||
local width=$(echo "$wininfo" | grep "Width:" | awk '{print $2}')
|
|
||||||
local height=$(echo "$wininfo" | grep "Height:" | awk '{print $2}')
|
|
||||||
local x=$(echo "$wininfo" | grep "Absolute upper-left X:" | awk '{print $4}')
|
|
||||||
local y=$(echo "$wininfo" | grep "Absolute upper-left Y:" | awk '{print $4}')
|
|
||||||
|
|
||||||
echo "Recording window at ${width}x${height}+${x},${y} to $outfile..."
|
|
||||||
ffmpeg -video_size ${width}x${height} -framerate 30 -f x11grab -i :0.0+${x},${y} \
|
|
||||||
-vcodec libx264 -preset fast -pix_fmt yuv420p -movflags +faststart "$outfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
fdcat() {
|
|
||||||
# 1. Run fdfind with all passed arguments and pipe to fzf
|
|
||||||
# Usage: fdcat -e py logging
|
|
||||||
# Usage: fdcat -e js auth
|
|
||||||
local file=$(fdfind "$@" | fzf --preview 'batcat --color=always --style=numbers {}')
|
|
||||||
|
|
||||||
# 2. Copy and print if a selection was made
|
|
||||||
if [ -n "$file" ]; then
|
|
||||||
# Adjust 'xclip -sel clip' if you are on WSL (use clip.exe) or Mac (use pbcopy)
|
|
||||||
echo -n "$file" | xclip -sel clip
|
|
||||||
echo "✔ Copied: $file"
|
|
||||||
else
|
|
||||||
echo "Exit: No file selected."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
#---- Aliases
|
|
||||||
|
|
||||||
source ~/.aliases.sh
|
|
||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Installing LXD on Debian Bookworm ==="
|
||||||
|
|
||||||
|
# Check if snapd is installed
|
||||||
|
if ! command -v snap &> /dev/null; then
|
||||||
|
echo "Installing snapd..."
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y snapd
|
||||||
|
echo "Enabling snapd service..."
|
||||||
|
sudo systemctl enable --now snapd.socket
|
||||||
|
# Wait for snapd to be ready
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo "snapd already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if LXD is already installed
|
||||||
|
if snap list lxd &> /dev/null; then
|
||||||
|
echo "LXD already installed via snap"
|
||||||
|
else
|
||||||
|
echo "Installing LXD via snap..."
|
||||||
|
sudo snap install lxd
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if LXD is initialized
|
||||||
|
if sudo lxd init --dump &> /dev/null; then
|
||||||
|
echo "LXD already initialized"
|
||||||
|
else
|
||||||
|
echo "Initializing LXD with default settings..."
|
||||||
|
sudo lxd init --auto
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Configuring LXD network for Docker compatibility..."
|
||||||
|
lxc network set lxdbr0 ipv4.firewall false 2>/dev/null || true
|
||||||
|
lxc network set lxdbr0 ipv6.firewall false 2>/dev/null || true
|
||||||
|
lxc network set lxdbr0 ipv4.nat true 2>/dev/null || true
|
||||||
|
|
||||||
|
LXD_SUBNET=$(lxc network get lxdbr0 ipv4.address)
|
||||||
|
if ! sudo iptables -t nat -C POSTROUTING -s "$LXD_SUBNET" ! -d "$LXD_SUBNET" -j MASQUERADE 2>/dev/null; then
|
||||||
|
sudo iptables -t nat -I POSTROUTING -s "$LXD_SUBNET" ! -d "$LXD_SUBNET" -j MASQUERADE
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
if ! sudo iptables -C DOCKER-USER -i lxdbr0 -j ACCEPT 2>/dev/null; then
|
||||||
|
sudo iptables -I DOCKER-USER -i lxdbr0 -j ACCEPT
|
||||||
|
fi
|
||||||
|
if ! sudo iptables -C DOCKER-USER -o lxdbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
|
||||||
|
sudo iptables -I DOCKER-USER -o lxdbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ LXD installation complete!"
|
||||||
|
echo " Default network bridge (lxdbr0) configured for internet access"
|
||||||
|
|
||||||
|
# Add user to lxd group for passwordless access
|
||||||
|
if groups "$USER" | grep -q "\blxd\b"; then
|
||||||
|
echo " User '$USER' already in lxd group"
|
||||||
|
else
|
||||||
|
echo "Adding user '$USER' to lxd group..."
|
||||||
|
sudo usermod -a -G lxd "$USER"
|
||||||
|
echo "✓ User added to lxd group"
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT: You need to log out and back in for group changes to take effect"
|
||||||
|
echo "Or run: newgrp lxd"
|
||||||
|
fi
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#export FZF_DEFAULT_OPTS='--bind ctrl-f:preview-page-down,ctrl-b:preview-page-up --preview "batcat --color=always {}"'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
eval "$(zoxide init bash)"
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
"""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")
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""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}",
|
|
||||||
)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import getpass
|
||||||
|
|
||||||
|
from invoke import task
|
||||||
|
|
||||||
|
from helpers import append_bashrc_section, load_snippet, run
|
||||||
|
|
||||||
|
APT_PACKAGES = [
|
||||||
|
"git",
|
||||||
|
"curl",
|
||||||
|
"micro",
|
||||||
|
"mc",
|
||||||
|
"detox",
|
||||||
|
"tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_claude(c):
|
||||||
|
"""Install Claude Code via the official install script."""
|
||||||
|
run(c, "curl -fsSL https://claude.ai/install.sh | bash")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_copilot(c):
|
||||||
|
"""Install GitHub Copilot via the official install script."""
|
||||||
|
run(c, "curl -fsSL https://gh.io/copilot-install | bash")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_apt_packages(c):
|
||||||
|
"""Update apt cache and install base packages."""
|
||||||
|
run(c, "sudo apt-get update")
|
||||||
|
run(c, f"sudo apt-get install -y {' '.join(APT_PACKAGES)}")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_docker(c):
|
||||||
|
"""Install Docker if not already installed and add current user to docker group."""
|
||||||
|
run(
|
||||||
|
c,
|
||||||
|
"command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sudo sh",
|
||||||
|
)
|
||||||
|
run(c, f"sudo usermod -aG docker {getpass.getuser()}")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_uv(c):
|
||||||
|
"""Install uv for the current user if not already installed."""
|
||||||
|
run(c, "test -f ~/.local/bin/uv || curl -LsSf https://astral.sh/uv/install.sh | sh")
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_fzf(c):
|
||||||
|
"""Install fzf from git, bat for preview, and configure .bashrc."""
|
||||||
|
run(c, "sudo apt-get install -y bat")
|
||||||
|
run(
|
||||||
|
c,
|
||||||
|
"test -d ~/.fzf || git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf",
|
||||||
|
)
|
||||||
|
run(c, "~/.fzf/install --all")
|
||||||
|
append_bashrc_section(c, "fzf", load_snippet("fzf"))
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def install_zoxide(c):
|
||||||
|
"""Install zoxide, configure .bashrc, """
|
||||||
|
run(c, "sudo apt install -y zoxide fzf")
|
||||||
|
append_bashrc_section(c, "zoxide", load_snippet("zoxide"))
|
||||||
|
|
||||||
|
|
||||||
|
@task(install_apt_packages, install_docker, install_uv, install_claude, install_fzf, install_zoxide)
|
||||||
|
def bootstrap(c):
|
||||||
|
"""Install all base tools."""
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def test(c):
|
||||||
|
"""Run tasks in an ephemeral LXD container."""
|
||||||
|
run(c, "pytest tests/ -v -s")
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
CONTAINER = "cli-tools-test"
|
||||||
|
IMAGE = "images:debian/bookworm"
|
||||||
|
REPO_ROOT = "/home/jev/projects/cli-tools"
|
||||||
|
|
||||||
|
|
||||||
|
def _lxc(*args: str, capture: bool = False) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
["lxc", *args],
|
||||||
|
check=True,
|
||||||
|
capture_output=capture,
|
||||||
|
text=capture,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_network(container: str, retries: int = 20, delay: float = 2.0) -> None:
|
||||||
|
print("[container] Waiting for network...", flush=True)
|
||||||
|
for _ in range(retries):
|
||||||
|
result = subprocess.run(
|
||||||
|
["lxc", "exec", container, "--", "bash", "-c", "ping -c1 8.8.8.8 &>/dev/null"],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("[container] Network ready", flush=True)
|
||||||
|
return
|
||||||
|
time.sleep(delay)
|
||||||
|
raise RuntimeError(f"Container {container!r} did not get network access")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def container() -> Generator[str, None, None]:
|
||||||
|
# Clean up any stale container from a previous run
|
||||||
|
subprocess.run(["lxc", "delete", "--force", CONTAINER], capture_output=True)
|
||||||
|
|
||||||
|
print(f"\n[container] Launching {CONTAINER}...", flush=True)
|
||||||
|
_lxc("launch", IMAGE, CONTAINER)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
_wait_for_network(CONTAINER)
|
||||||
|
|
||||||
|
print("[container] Installing sudo and python3-pip...", flush=True)
|
||||||
|
_lxc(
|
||||||
|
"exec", CONTAINER, "--",
|
||||||
|
"bash", "-c",
|
||||||
|
"""
|
||||||
|
apt-get update -qq
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sudo python3-pip
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("[container] Creating jev user...", flush=True)
|
||||||
|
_lxc(
|
||||||
|
"exec", CONTAINER, "--",
|
||||||
|
"bash", "-c",
|
||||||
|
"""
|
||||||
|
useradd -m -s /bin/bash -u 1000 jev
|
||||||
|
usermod -aG sudo jev
|
||||||
|
echo 'jev ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/jev
|
||||||
|
chmod 440 /etc/sudoers.d/jev
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("[container] Installing invoke...", flush=True)
|
||||||
|
_lxc(
|
||||||
|
"exec", CONTAINER, "--",
|
||||||
|
"bash", "-c",
|
||||||
|
"pip3 install invoke --quiet --break-system-packages",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("[container] Mounting repo...", flush=True)
|
||||||
|
_lxc("config", "device", "add", CONTAINER, "repo", "disk",
|
||||||
|
f"source={REPO_ROOT}", "path=/home/jev/cli-tools")
|
||||||
|
print("[container] Ready\n", flush=True)
|
||||||
|
|
||||||
|
yield CONTAINER
|
||||||
|
|
||||||
|
print(f"\n[container] Destroying {CONTAINER}...", flush=True)
|
||||||
|
_lxc("delete", "--force", CONTAINER)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def lxc_exec(container: str, cmd: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a command in the container as the jev user."""
|
||||||
|
return subprocess.run(
|
||||||
|
["lxc", "exec", container, "--user", "1000", "--env", "HOME=/home/jev", "--", "bash", "-lc", cmd],
|
||||||
|
check=check,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_task(container: str, task: str) -> None:
|
||||||
|
lxc_exec(container, f"cd /home/jev/cli-tools && invoke {task.replace('_', '-')}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_apt_packages(container: str) -> None:
|
||||||
|
run_task(container, "install_apt_packages")
|
||||||
|
for binary in ["git", "micro", "tree", "detox"]:
|
||||||
|
result = lxc_exec(container, f"which {binary}", check=False)
|
||||||
|
assert result.returncode == 0, f"{binary!r} not found after install_apt_packages"
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_docker(container: str) -> None:
|
||||||
|
run_task(container, "install_docker")
|
||||||
|
result = lxc_exec(container, "docker --version", check=False)
|
||||||
|
assert result.returncode == 0, "docker not found after install_docker"
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_uv(container: str) -> None:
|
||||||
|
run_task(container, "install_uv")
|
||||||
|
result = lxc_exec(container, "~/.local/bin/uv --version", check=False)
|
||||||
|
assert result.returncode == 0, "uv not found after install_uv"
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_fzf(container: str) -> None:
|
||||||
|
run_task(container, "install_fzf")
|
||||||
|
result = lxc_exec(container, "test -f ~/.fzf/bin/fzf", check=False)
|
||||||
|
assert result.returncode == 0, "fzf binary not found at ~/.fzf/bin/fzf after install_fzf"
|
||||||
|
result = lxc_exec(container, "which batcat", check=False)
|
||||||
|
assert result.returncode == 0, "batcat not found after install_fzf"
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_zoxide(container: str) -> None:
|
||||||
|
run_task(container, "install_zoxide")
|
||||||
|
result = lxc_exec(container, "which zoxide", check=False)
|
||||||
|
assert result.returncode == 0, "zoxide not found after install_zoxide"
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 3
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "annotated-doc"
|
|
||||||
version = "0.0.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cli-tools"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typer" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [{ name = "typer" }]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [{ name = "ruff", specifier = ">=0.15.9" }]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.3.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markdown-it-py"
|
|
||||||
version = "4.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "mdurl" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mdurl"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.20.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rich"
|
|
||||||
version = "14.3.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markdown-it-py" },
|
|
||||||
{ name = "pygments" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ruff"
|
|
||||||
version = "0.15.9"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shellingham"
|
|
||||||
version = "1.5.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typer"
|
|
||||||
version = "0.24.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "annotated-doc" },
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "rich" },
|
|
||||||
{ name = "shellingham" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user