{"id":50277804,"url":"https://github.com/gevgev/esp-tool","last_synced_at":"2026-05-27T22:00:54.876Z","repository":{"id":351596185,"uuid":"1211667367","full_name":"gevgev/esp-tool","owner":"gevgev","description":"Go CLI that auto-discovers ESPHome devices from YAML configs and runs OTA firmware upgrades, version checks, and boot-log diagnostics in parallel.","archived":false,"fork":false,"pushed_at":"2026-05-26T01:52:42.000Z","size":112,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-26T03:20:10.959Z","etag":null,"topics":["cli","esp32","esphome","firmware","go","home-automation","ota"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gevgev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-15T16:15:33.000Z","updated_at":"2026-05-26T01:52:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gevgev/esp-tool","commit_stats":null,"previous_names":["gevgev/esp-tool"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/gevgev/esp-tool","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gevgev%2Fesp-tool","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gevgev%2Fesp-tool/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gevgev%2Fesp-tool/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gevgev%2Fesp-tool/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gevgev","download_url":"https://codeload.github.com/gevgev/esp-tool/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gevgev%2Fesp-tool/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33585203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-27T02:00:06.184Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","esp32","esphome","firmware","go","home-automation","ota"],"created_at":"2026-05-27T22:00:50.085Z","updated_at":"2026-05-27T22:00:54.867Z","avatar_url":"https://github.com/gevgev.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# esp-tool\n\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Go 1.21+](https://img.shields.io/badge/go-1.21%2B-00ADD8.svg?logo=go\u0026logoColor=white)](https://go.dev/)\n[![ESPHome](https://img.shields.io/badge/ESPHome-compatible-blue.svg)](https://esphome.io/)\n\nA Go CLI for managing [ESPHome](https://esphome.io) devices. It auto-discovers devices from your ESPHome YAML configuration directory, runs OTA firmware upgrades in parallel with retry logic, and checks running firmware versions — all without maintaining a manual device list.\n\n**Replaces** the handwritten `upgrade-esp-devices.sh` and `check-esp-versions.sh` shell scripts. Adding a new device YAML to your ESPHome repo is all that's needed for it to be picked up automatically.\n\n---\n\n## Screenshots\n\n### `upgrade`\n\n**TUI — compilation phase** (14 devices, 4 parallel jobs, dependency resolution in progress):\n\n![TUI startup — compilation phase](docs/screenshots/tui-startup.png)\n\n**TUI — upload phase** (progress bar advancing, completed devices marked ✓, active uploads in right panel):\n\n![TUI mid-run — firmware upload phase](docs/screenshots/tui-mid-run.png)\n\n**Post-upgrade summary** (all 14 devices upgraded, retry badges where applicable, per-device timing):\n\n![Upgrade summary](docs/screenshots/upgrade-summary.png)\n\n**TUI — device unreachable, retry pending** (step-motor-2 offline; `↺ in 4s` countdown in device list, last error line in Active Jobs panel, Output panel showing the DNS timeout):\n\n![TUI device retrying](docs/screenshots/tui-device-retrying.png)\n\n**TUI — upgrade complete with one failure** (step-motor-2 exhausted all retries; Errors panel populated with snippets, failed device marked ✗, progress bar full):\n\n![TUI device failed](docs/screenshots/tui-device-failed.png)\n\n### `versions`\n\n**TUI — mid-run** (9/14 devices resolved with ✓ and firmware version, remaining still spinning):\n\n![Versions TUI mid-run](docs/screenshots/versions-tui-mid-run.png)\n\n### `diagnostics`\n\n**TUI — startup** (all 14 devices spinning, log collection in progress):\n\n![Diagnostics TUI startup](docs/screenshots/diagnostics-tui-startup.png)\n\n**Post-diagnostics summary** (all 14 devices reachable and healthy, firmware versions listed):\n\n![Diagnostics summary](docs/screenshots/diagnostics-summary.png)\n\n---\n\n## How it works\n\n1. **Discovers devices** by scanning `*.yaml` files in the target directory using a case-insensitive extension match (`.yaml`, `.YAML`, `.Yaml` are all accepted — handles files saved by Windows tools like Notepad). Skips `secrets.yaml` and subdirectories like `archive/`.\n2. **Parses `esphome.name`** from each YAML, resolving ESPHome substitution variables (`${name}`, `$hostname`, etc.) via the `substitutions:` block.\n3. **Derives the OTA hostname** as `\u003cname\u003e.local`.\n4. **Runs `esphome` commands** in parallel, bounded by a configurable concurrency limit.\n\n---\n\n## Prerequisites\n\n\u003e **`esphome` CLI is required.** esp-tool is a wrapper around the ESPHome command-line tool — it calls `esphome run`, `esphome logs`, and `esphome config` under the hood. If `esphome` is not installed and on your `PATH`, every command that talks to devices will fail.\n\u003e\n\u003e Install it with: `pip3 install esphome` (requires Python 3.9+).  \n\u003e Full instructions: [ESPHome — Getting Started with the CLI](https://esphome.io/guides/getting_started_command_line).\n\n| Platform | ESPHome availability |\n|---|---|\n| **macOS / Linux** | Install via `pip3 install esphome` — works natively |\n| **Windows (WSL2)** | Run esp-tool's **Linux** binary inside WSL2 where ESPHome is installed — recommended |\n| **Windows (native)** | Install ESPHome via `pip3 install esphome` in a Windows Python environment, then use esp-tool's `.exe` — ESPHome's Windows support is limited; most users prefer WSL2 |\n\n- [Go 1.21+](https://go.dev/dl/) — only needed if building from source (Option B)\n\n---\n\n## Installation\n\n### Option A — download a pre-built binary (recommended)\n\nPre-built binaries for macOS, Linux, and Windows are published with every tagged release on\n[GitHub Releases](https://github.com/gevgev/esp-tool/releases).\n\n**macOS / Linux** — one-liner install (auto-detects the latest version):\n\n```bash\n# macOS (Apple Silicon)\nVERSION=$(curl -s https://api.github.com/repos/gevgev/esp-tool/releases/latest | grep '\"tag_name\"' | cut -d'\"' -f4 | sed 's/^v//')\ncurl -sL \"https://github.com/gevgev/esp-tool/releases/download/v${VERSION}/esp-tool_${VERSION}_darwin_arm64.tar.gz\" | tar -xz\nsudo mv esp-tool /usr/local/bin/\n\n# macOS (Intel)\nVERSION=$(curl -s https://api.github.com/repos/gevgev/esp-tool/releases/latest | grep '\"tag_name\"' | cut -d'\"' -f4 | sed 's/^v//')\ncurl -sL \"https://github.com/gevgev/esp-tool/releases/download/v${VERSION}/esp-tool_${VERSION}_darwin_amd64.tar.gz\" | tar -xz\nsudo mv esp-tool /usr/local/bin/\n\n# Linux (amd64)\nVERSION=$(curl -s https://api.github.com/repos/gevgev/esp-tool/releases/latest | grep '\"tag_name\"' | cut -d'\"' -f4 | sed 's/^v//')\ncurl -sL \"https://github.com/gevgev/esp-tool/releases/download/v${VERSION}/esp-tool_${VERSION}_linux_amd64.tar.gz\" | tar -xz\nsudo mv esp-tool /usr/local/bin/\n\n# Linux (arm64 — Raspberry Pi 64-bit, etc.)\nVERSION=$(curl -s https://api.github.com/repos/gevgev/esp-tool/releases/latest | grep '\"tag_name\"' | cut -d'\"' -f4 | sed 's/^v//')\ncurl -sL \"https://github.com/gevgev/esp-tool/releases/download/v${VERSION}/esp-tool_${VERSION}_linux_arm64.tar.gz\" | tar -xz\nsudo mv esp-tool /usr/local/bin/\n```\n\n**Windows (PowerShell)** — auto-detects the latest version:\n\n```powershell\n$VERSION = (Invoke-RestMethod https://api.github.com/repos/gevgev/esp-tool/releases/latest).tag_name.TrimStart('v')\nInvoke-WebRequest `\n  -Uri \"https://github.com/gevgev/esp-tool/releases/download/v$VERSION/esp-tool_${VERSION}_windows_amd64.zip\" `\n  -OutFile esp-tool.zip\nExpand-Archive esp-tool.zip -DestinationPath .\n# Move the binary to any folder on your PATH, for example:\nMove-Item .\\esp-tool.exe \"$env:USERPROFILE\\AppData\\Local\\Microsoft\\WindowsApps\\\"\n```\n\n\u003e **Windows + ESPHome:** `esphome` has limited native Windows support. Most Windows users run esp-tool's **Linux binary inside WSL2** (where ESPHome is pip-installed) rather than the native `.exe`. The Windows binary is most useful for scripting or environments where ESPHome is explicitly installed in the Windows Python environment.\n\nEach release archive also contains the `completions/_esp-tool` zsh script and `README.md`.\n\n### Option B — build from source\n\nRequires [Go 1.21+](https://go.dev/dl/).\n\n```bash\ngit clone https://github.com/gevgev/esp-tool.git\ncd esp-tool\nmake build              # produces bin/esp-tool  (macOS / Linux)\nmake build-windows      # produces bin/esp-tool.exe  (cross-compile for Windows)\n```\n\nInstall the binary into your ESPHome YAML directory (so you can run it from there):\n\n```bash\nmake install                                        # installs to ../esphome/esphome/ by default\nmake install ESPHOME_DIR=/path/to/your/esphome/dir  # or specify a custom path\n```\n\nThe installed binary is a build artifact — add it to `.gitignore` in your ESPHome repo:\n\n```\n/esp-tool\n```\n\n### Shell completion (zsh)\n\n**Option 1 — system-wide (requires sudo):**\n\n```bash\nsudo make install-completions\n```\n\n**Option 2 — user-local, no sudo.** Add to `~/.zshrc`:\n\n```zsh\nfpath=(/path/to/esp-tool/completions $fpath)\nautoload -Uz compinit \u0026\u0026 compinit\n```\n\nThen `source ~/.zshrc` (or open a new shell).\n\nCompletion covers all subcommands, flags with descriptions, directory path completion for `--dir`, and device name completion for `--filter` (scanned from YAML files in the active `--dir`).\n\n---\n\n## Configuration file\n\nesp-tool looks for `.esp-tool.yaml` in the current directory first, then in `~/.esp-tool.yaml`. This lets you persist your usual settings so you don't need to repeat them on every invocation. Command-line flags always override config file values.\n\n**Supported keys:**\n\n| Key | Type | Description |\n|---|---|---|\n| `dir` | string | Directory containing ESPHome YAML files |\n| `jobs` | int | Maximum simultaneous `esphome` processes |\n| `retries` | int | Retry attempts after the first upgrade failure |\n| `retry-delay` | duration | Wait between retry attempts (e.g. `5s`, `15s`) |\n| `timeout` | duration | Per-attempt timeout; `0` means no limit |\n| `filter` | string | Comma-separated device names (all if omitted) |\n\n**Example — place this in your ESPHome directory as `.esp-tool.yaml`:**\n\n```yaml\n# .esp-tool.yaml\ndir: ~/git/esp32/esphome/esphome   # default --dir for all commands\njobs: 4\nretries: 3\nretry-delay: 10s\n```\n\nA fully annotated example is in [`docs/esp-tool.yaml.example`](docs/esp-tool.yaml.example).\n\n\u003e Run with `--verbose` to see which config file was loaded.\n\n---\n\n## Commands\n\n### `upgrade`\n\nRebuilds firmware and OTA-flashes all discovered devices. Runs:\n\n```\nesphome run \u003cfile\u003e --no-logs --device \u003cname\u003e.local\n```\n\nDevices are processed in parallel (default: 4 at a time, since compilation is CPU/RAM intensive). On failure, each device is retried up to `--retries` additional times before being marked as failed. A colored summary table is printed when all devices finish.\n\n#### TUI vs plain-text output\n\nWhen stdout is a TTY at least 80 × 24 characters, the `upgrade` command shows an interactive terminal UI (TUI):\n\n- **Header** — jobs, retries, progress bar (`█░`), elapsed time\n- **Left panel** — all devices with live status icons (⠋ running, ✓ success, ✗ failed, ↺ retrying, ◷ queued), retry badge, and final duration\n- **Right panels** — active jobs with last output line (top) and error snippets (bottom)\n- **Bottom panel** — scrollable tail of the most recent output lines from all devices (or press `?` for help)\n\nWhen the TUI exits, the same colored summary table as plain mode is printed to stdout.\n\nThe TUI is **not** shown when:\n- `--plain` or `--no-tui` is passed\n- stdout is not a TTY (piped, redirected, CI environment)\n- the terminal is smaller than 80 × 24\n- (use `--verbose` to see which condition triggered the fallback)\n\n#### TUI keyboard shortcuts\n\n| Key | Action |\n|---|---|\n| `q` / `ctrl+c` | Quit (auto-exits on all-success; waits for `q` on failure) |\n| `?` | Toggle keyboard-shortcut help overlay |\n| `↑` / `k` | Scroll device list up |\n| `↓` / `j` | Scroll device list down |\n| `Home` / `g` | Jump to top of device list |\n| `End` / `G` | Jump to bottom of device list |\n\n**Flags:**\n\n| Flag | Short | Default | Description |\n|---|---|---|---|\n| `--dir` | `-d` | `.` (cwd) | Directory containing ESPHome YAML files |\n| `--jobs` | `-j` | `4` | Maximum simultaneous `esphome` processes |\n| `--retries` | `-r` | `2` | Retry attempts after the first failure |\n| `--retry-delay` | | `5s` | Wait time between retry attempts |\n| `--timeout` | | `0` (none) | Per-attempt timeout; kills the process if exceeded (e.g. `10m`) |\n| `--filter` | | | Comma-separated device names to upgrade (all if omitted) |\n| `--retry-failed` | | `false` | Re-run only devices that failed in the previous upgrade run |\n| `--dry-run` | | `false` | Print commands without executing them |\n| `--prefix` | | `true` | Prefix live output lines with `[device-name]` (plain mode) |\n| `--plain` | | `false` | Disable TUI; use plain-text output |\n| `--no-tui` | | `false` | Alias for `--plain` |\n| `--log-file` | | | Append all device output to a file (streamed line-by-line) |\n| `--verbose` | `-v` | `false` | Print diagnostic logs to stderr (retries, timing, TUI fallback reason) |\n\nAfter every run (whether all devices succeeded or not) a small JSON state file is saved to `.esp-tool-last-run.json` in the `--dir` directory. `--retry-failed` reads this file and filters the device list to only the ones that failed last time. It is an error to combine `--retry-failed` with `--filter`.\n\n**Examples:**\n\n```bash\n# Upgrade all devices from the current directory (esphome repo root)\n./esp-tool upgrade\n\n# Upgrade from a specific directory\nesp-tool upgrade --dir ~/git/esp32/esphome/esphome\n\n# Increase parallelism and retries\nesp-tool upgrade --jobs 6 --retries 3\n\n# Use a longer pause between retries (e.g. device is slow to reboot)\nesp-tool upgrade --retry-delay 15s\n\n# Upgrade only specific devices (comma-separated)\nesp-tool upgrade --filter lux-living-christmas\nesp-tool upgrade --filter step-motor-1,step-motor-2\n\n# Re-run only the devices that failed last time (reads .esp-tool-last-run.json)\nesp-tool upgrade --retry-failed\n\n# Dry-run: verify device discovery and see exact commands without flashing\nesp-tool upgrade --dry-run\n\n# Force plain-text output (no TUI) — useful in scripts or over SSH\nesp-tool upgrade --plain\n\n# Stream all output to a file while TUI is active\nesp-tool upgrade --log-file /tmp/upgrade-$(date +%Y%m%d).log\n\n# Show why TUI was not activated (runs in plain mode in this example)\nesp-tool upgrade --verbose 2\u003e\u00261 | head -3\n\n# Combine: dry-run a filtered set from a specific directory\nesp-tool upgrade --dir ~/git/esp32/esphome/esphome --filter ocamera --dry-run\n\n# Kill any attempt that takes longer than 10 minutes (prevents stuck OTA from blocking a slot)\nesp-tool upgrade --timeout 10m\n```\n\n**Sample output:** see the [Screenshots](#screenshots) section at the top of this README.\n\n---\n\n### `validate`\n\nRuns `esphome config \u003cfile\u003e` for every discovered device in parallel. Reports which configs are valid and which have errors, **without compiling or flashing anything**. Use this as a pre-flight check after editing YAML before a batch upgrade.\n\nCompile errors detected here also cause `upgrade` to skip retries automatically — if a device's config is broken, `upgrade` reports `Upgrade failed (compile error)` and moves on immediately rather than retrying.\n\n**Flags:**\n\n| Flag | Short | Default | Description |\n|---|---|---|---|\n| `--dir` | `-d` | `.` (cwd) | Directory containing ESPHome YAML files |\n| `--jobs` | `-j` | `4` | Maximum simultaneous `esphome` processes |\n| `--timeout` | | `30s` | Per-device timeout for config validation |\n| `--filter` | | | Comma-separated device names to check (all if omitted) |\n| `--dry-run` | | `false` | Print commands without executing them |\n| `--verbose` | `-v` | `false` | Print diagnostic logs to stderr |\n\n**Examples:**\n\n```bash\n# Validate all devices in the current directory\nesp-tool validate\n\n# Validate from a specific directory\nesp-tool validate --dir ~/git/esp32/esphome/esphome\n\n# Validate a single device\nesp-tool validate --filter lux-living-christmas\n\n# Dry-run: see what would be checked\nesp-tool validate --dry-run\n```\n\n---\n\n### `versions`\n\nConnects to each device's live log stream in parallel, grabs the first `ESPHome version` line, and exits. Prints a colored summary. Times out per device after `--timeout` (default 12 s).\n\nWhen stdout is a TTY ≥ 80×24, shows a **live TUI** with a spinner per device that updates in real time as each result arrives. The TUI auto-quits after all devices report and the summary is printed to stdout. Use `--plain` to suppress the TUI.\n\n**Flags:**\n\n| Flag | Short | Default | Description |\n|---|---|---|---|\n| `--dir` | `-d` | `.` (cwd) | Directory containing ESPHome YAML files |\n| `--timeout` | | `12s` | Per-device timeout before marking unreachable |\n| `--filter` | | | Comma-separated device names to check (all if omitted) |\n| `--plain` | | `false` | Disable TUI; use plain-text output |\n| `--verbose` | `-v` | `false` | Print diagnostic logs to stderr |\n\n**Examples:**\n\n```bash\n# Check all devices from the current directory\n./esp-tool versions\n\n# Check from a specific directory\nesp-tool versions --dir ~/git/esp32/esphome/esphome\n\n# Allow more time for slow devices\nesp-tool versions --timeout 20s\n\n# Check only a subset of devices\nesp-tool versions --filter ocamera,widecamera,widecamera-2\n\n# Force plain-text output (no TUI)\nesp-tool versions --plain\n```\n\n**Sample output:**\n\n```\nChecking firmware versions for 14 devices...\n\nESPHome device firmware versions.\nSummary:\n\n  - air-quality-external:          v2024.11.0\n  - air-quality-internal:          v2024.11.0\n  - aram-display:                  v2024.11.0\n  - bluetooth-proxy-2:             v2024.11.0\n  - bluetooth-proxy-9c866c:        Unreachable\n  ...\n\n13 reachable, 1 unreachable\nElapsed time: 12s\n```\n\n---\n\n### `diagnostics`\n\nConnects to each device's live log stream in parallel, collects the initial boot dump, and prints a per-device health table. Times out per device after `--timeout` (default 15 s).\n\nWhen stdout is a TTY ≥ 80×24, shows a **live TUI** with a spinner per device that resolves as each result arrives: ✓ Healthy, ⚠ N warnings (with detail lines), or ✗ crash. Use `--plain` to suppress the TUI.\n\nDetects:\n- Crash on previous boot (hardware WDT, exception, etc.)\n- Bootloader too old for OTA rollback (needs one-time USB flash)\n- Bootloader supports SRAM1 (+40 KB IRAM, opt-in flag available)\n- Chip rev ≥ 3.0 (binary size can be reduced with `minimum_chip_revision`)\n- GPIO strapping pin in use\n- Multiple OTA platform configs merged\n\n**Flags:**\n\n| Flag | Short | Default | Description |\n|---|---|---|---|\n| `--dir` | `-d` | `.` (cwd) | Directory containing ESPHome YAML files |\n| `--timeout` | | `15s` | Per-device timeout for log collection |\n| `--filter` | | | Comma-separated device names to check (all if omitted) |\n| `--plain` | | `false` | Disable TUI; use plain-text output |\n| `--reboot` | `-r` | `false` | Soft-reboot each device before capturing logs |\n| `--reboot-wait` | | `12s` | Time to wait after rebooting before collecting logs |\n| `--verbose` | `-v` | `false` | Print diagnostic logs to stderr |\n\n**Examples:**\n\n```bash\n# Check all devices from the current directory\n./esp-tool diagnostics\n\n# Check from a specific directory\nesp-tool diagnostics --dir ~/git/esp32/esphome/esphome\n\n# Check a subset with verbose output\nesp-tool diagnostics --filter espvibration1,lux-living-christmas --verbose\n\n# Reboot each device first to capture a fresh boot log\nesp-tool diagnostics --reboot\n\n# Force plain-text output (no TUI)\nesp-tool diagnostics --plain\n```\n\n---\n\n## Typical workflow\n\nAfter a new ESPHome version is released:\n\n```bash\n# 1. Upgrade the esphome tool itself\npip3 install esphome --upgrade\n\n# 2. (Optional) Validate all device configs before touching any hardware\n./esp-tool validate\n\n# 3. (Optional) Verify all devices are currently reachable\n./esp-tool versions\n\n# 4. Upgrade all devices\n./esp-tool upgrade\n\n# 5. If any failed, retry just those (reads .esp-tool-last-run.json automatically)\n./esp-tool upgrade --retry-failed\n\n# 6. Or target a specific device by name\n./esp-tool upgrade --filter bluetooth-proxy-9c866c\n```\n\n---\n\n## Project structure\n\n```\nesp-tool/\n├── cmd/esp-tool/main.go          # CLI entry point (cobra commands, flag wiring)\n├── internal/\n│   ├── diagnostics/              # Boot log collection and health analysis\n│   ├── discovery/scanner.go      # YAML glob, esphome.name parsing, substitution resolution\n│   ├── output/                   # OutputWriter abstraction (PlainWriter, MutexWriter, ShouldUseTUI)\n│   ├── tui/                      # Bubbletea TUI — model, panel renderers, TUIWriter\n│   ├── upgrader/runner.go        # Parallel esphome execution, semaphore, retry logic\n│   └── report/printer.go        # Colored ANSI summary table\n├── completions/\n│   └── _esp-tool                 # Zsh completion script\n├── go.mod\n├── Makefile\n└── README.md\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgevgev%2Fesp-tool","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgevgev%2Fesp-tool","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgevgev%2Fesp-tool/lists"}