# knecht CLI + TUI for managing Portainer stacks from the local `services/` directory. ## Installation ```sh cd knecht go build -o knecht . # move to somewhere on your PATH, e.g.: mv knecht /usr/local/bin/knecht ``` ## Configuration `~/.config/knecht/config.toml`: ```toml url = "https://portainer.example.com" token = "ptr_..." ignore = ["portainer"] # stack names to hide (e.g. self-managed containers) # Optional endpoint = "local" # Portainer environment name, auto-detected if omitted services_path = "/path/to/services" # defaults to git root / services ``` The API token is generated in Portainer under **Account → Access tokens**. ## Commands | Command | Description | |---------|-------------| | `knecht` | Launch the TUI | | `knecht list` | Print all stacks with status and drift summary | | `knecht deploy ` | Deploy a new stack from `services//docker-compose.yml` | | `knecht update ` | Update an existing stack, preserving Portainer env vars | | `knecht restart ` | Stop then start a stack | | `knecht diff ` | Print compose and env key drift | | `knecht logs [container]` | Open Dozzle in the browser; deep-links to a specific container if given | `` always maps to a folder name under `services/`. ## TUI Run `knecht` with no arguments to launch the interactive TUI. ``` ┌─ stacks ──────────────┐ ┌─ detail ──────────────────────────────┐ │ ● traefik live │ │ rrr │ │ ● rrr live ~ │ │ │ │ ● jellyfin live │ │ status: running │ │ dummy not deploy │ │ drift: compose ~3 lines │ │ │ │ │ │ │ │ [u] update [d] diff [r] refresh │ └───────────────────────┘ └──────────────────────────────────────┘ ``` ### Keybinds | Key | Action | |-----|--------| | `↑` / `↓` | Navigate stacks | | `u` | Update selected stack (preserves Portainer env vars) | | `D` | Deploy selected stack (only available when not deployed) | | `d` | Toggle diff view | | `e` | Generate or append `.env.example` from Portainer env keys | | `r` | Refresh all stacks | | `esc` | Return to summary view | | `q` / `ctrl+c` | Quit | ## Drift Detection knecht compares the local `services//docker-compose.yml` against what is deployed in Portainer, and the keys in `services//.env.example` against the env vars set in Portainer. | Indicator | Meaning | |-----------|---------| | `~` next to stack name | Drift detected | | `! missing: KEY` | Key is in `.env.example` but not set in Portainer | | `? unknown: KEY` | Key is in Portainer but not in `.env.example` — press `e` to append | | `no .env.example` | Portainer has env vars but no example file exists — press `e` to generate | Stacks with no `.env.example` file are not penalised for env drift — absence of the file means "no opinion". ## Stack Convention Each stack is a directory under `services/` containing: ``` services// ├── docker-compose.yml # required └── .env.example # optional — documents required Portainer env vars ``` `knecht deploy ` / `knecht update ` read these files directly. Env var *values* are never stored locally — only the keys in `.env.example`, as documentation.