initial commit
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.gpg
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# CLI Tools
|
||||||
|
|
||||||
|
Scripts and tools to set up and manage the terminal environment across systems.
|
||||||
|
|
||||||
|
**Entry point:** `cli-tools`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 |
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
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]`.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## 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.
|
||||||
|
- 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.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(uv sync:*)",
|
||||||
|
"Bash(uv run pytest:*)",
|
||||||
|
"Bash(inv lint:*)",
|
||||||
|
"Bash(inv test:*)"
|
||||||
|
],
|
||||||
|
"deny": [
|
||||||
|
"Read(./.venv/**)",
|
||||||
|
"Read(./__pycache__/**)",
|
||||||
|
"Read(./.env*)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skipDangerousModePermissionPrompt": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/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"
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
aliases.sh
|
||||||
|
bash_helpers.sh
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/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'
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#!/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
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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