Last Updated: March 2026 (CLI subcommands) | For: pvetui - Proxmox TUI
- Initial Setup
- Development Workflow
- Quick Reference
- Code Quality Standards
- Style Guidelines
- Documentation Requirements
- Commit Standards
- Security and Performance
- Tools and Environment
- Testing Strategy
- Project Context
- Common Pitfalls
- Troubleshooting
- Maintaining This Document
The following conventions must be followed for any changes in this repository.
- Run development setup:
make dev-setup(installs required tools and validates environment). - The embedded noVNC client lives in a git subtree; sync it with upstream when needed via
make update-novnc. - For enhanced development experience (optional):
- Install direnv:
sudo pacman -S direnvor equivalent - Copy
.envrc.exampleto.envrcand configure as needed - Install pre-commit hooks:
pre-commit install
- Install direnv:
- Confirm the application builds with
make build. - Run comprehensive code quality checks with
make code-quality(includesgo vetandgolangci-lint). - Execute all tests with
make test. - For integration tests:
make test-integration(requires Proxmox environment). - Keep the working tree clean before finishing.
| Task | Command |
|---|---|
| Build | make build |
| Run all checks | make code-quality && make test |
| Fast iteration | make test-quick |
| Integration tests | PVETUI_INTEGRATION_TEST=true make test-integration |
| Install hooks | pre-commit install |
| View logs | tail -f ~/.cache/pvetui/pvetui.log |
| Clean build | make clean && make build |
| Development setup | make dev-setup |
- All code must pass
make code-qualitywithout errors (includes go vet and golangci-lint). - Maintain test coverage; add tests for new functionality.
- Use table-driven tests where appropriate.
- Mock external dependencies in unit tests.
-
Follow idiomatic Go and clean architecture practices.
-
Apply Clean Architecture: handlers → services → repositories → domain models.
-
Prefer small, focused interfaces and dependency injection via constructors.
-
Use interface-driven development; public functions should accept interfaces, not concrete types.
-
Document all exported identifiers with comprehensive GoDoc comments including:
- Package-level documentation explaining purpose and usage patterns
- Function documentation with parameter descriptions and examples
- Type documentation with use cases and thread safety considerations
-
Handle errors explicitly; wrap errors with context using
fmt.Errorf("context: %w", err).Example:
// Good if err := client.StartVM(vmid); err != nil { return fmt.Errorf("failed to start VM %d: %w", vmid, err) } // Bad - no context if err := client.StartVM(vmid); err != nil { return err }
-
Use context propagation for request-scoped values, deadlines, and cancellations.
- Update
CHANGELOG.mdunder [Unreleased] section with user-visible changes.- Important: Only document changes from the previous release, not intermediate development steps
- Example: If you change a timeout from 5min → 10min → 3min during development, only document the final change (5min → 3min)
- Focus on user-visible bug fixes, features, and breaking changes
- Avoid documenting internal refactorings or temporary fixes made during development
- Add GoDoc examples for complex public APIs.
- Update relevant documentation files when changing behavior.
- Write concise commit messages (imperative mood, present tense).
- Use simple, descriptive messages stating what was done.
- Include relevant emojis when appropriate.
- Ask about committing after successfully implementing features.
- Validate and sanitize all external inputs (especially VM names, IDs, and user commands).
- Never log credentials: Passwords, API tokens, or CSRF tokens should never appear in logs.
- Implement proper error handling and timeouts for external calls (default: 30s).
- Use secure defaults for authentication and configuration.
- File permissions: Config files with secrets must be 0o600, cache directories 0o700.
- Profile and benchmark performance-critical code paths.
- Always validate array indices and map keys before access.
- Use prepared statements or proper escaping for any dynamic command construction.
- Go version is pinned in
.go-versionfile for consistency. - Use
golangci-lintfor comprehensive linting (config auto-migrated to v2 format). - Environment variables can be configured via
.envfile (see.env.example). - Consider using direnv for automatic environment loading (
.envrc.exampleprovided). - Pre-commit hooks available for automated code quality checks.
- Unit tests: Fast, isolated, mocked dependencies
- Integration tests: Real system interactions (separate from unit tests)
- Set environment variable:
PVETUI_INTEGRATION_TEST=true - Configure test Proxmox instance in
.env.test(see.env.example) - Alternatively, use mock Proxmox server from
test/testutils/integration_helpers.go
- Set environment variable:
- Use
make test-quickfor fast feedback during development - Ensure tests are deterministic and can run in parallel
- Table-driven tests are preferred for testing multiple scenarios
- Mock external dependencies using interfaces
pvetui is a Terminal User Interface (TUI) for Proxmox Virtual Environment, written in Go. It provides a fast, keyboard-driven interface for managing VMs, containers, nodes, and clusters without requiring the web UI.
-
pkg/api/- Proxmox API client with authentication, caching, and HTTP communicationclient.go- Main API client with methods for all Proxmox operationsauth.go- Authentication manager supporting both password and API token authhttp.go- HTTP client with retry logic and timeout handlingguest_agent_exec.go- QEMU guest agent command execution with polling- Constants:
DefaultAPITimeout = 30s,DefaultRetryCount = 3 - Important: Proxmox returns boolean-like fields as integers (0/1), not JSON booleans - parse as
float64and convert
-
internal/cache/- Caching layer with BadgerDB and in-memory implementationsbadger_cache.go- Persistent cache using BadgerDB with proper goroutine cleanupcache.go- In-memory FileCache with LRU eviction (usingcontainer/list)- CacheItem stores data as
json.RawMessageto avoid double marshaling - Supports configurable size limits for memory management
-
internal/cli/- Non-interactive CLI subcommands (nodes, guests, tasks)cli_helpers.go-cliSessionabstraction, SSH helpers, output utilitiesguests.go- Guest list/show/lifecycle/exec (QEMU guest agent + LXC pct exec)nodes.go- Node list/showtasks.go- Task list- All subcommands work with single-profile and aggregate group profiles transparently
-
internal/ui/- TUI components using tview library- Main interface with tabbed navigation (Nodes, Guests, Tasks)
- Context menus, dialogs, forms, and detail panels
- VNC integration with embedded noVNC client
-
internal/logger/- Unified logging system- All components log to single file in cache directory
- Debug/Info/Warn/Error levels
- Clean Architecture: Dependency injection via constructors, interfaces over concrete types
- Adapter Pattern: Config and logger adapters bridge internal and pkg interfaces
- Thread Safety: All cache operations protected with RWMutex, auth manager uses mutex for token access
- LRU Eviction: FileCache uses doubly-linked list for efficient cache management
Note: For changes older than 6 months, see CHANGELOG.md.
Comprehensive code review resulted in these fixes (Oct 2025):
- Security: Removed password logging, fixed race condition in auth token retrieval, improved file permissions
- Performance: Eliminated double JSON marshaling, implemented LRU cache with size limits
- Reliability: Added HTTP timeouts, fixed BadgerDB goroutine leak, configurable retry count
- Lock File Handling: Proper PID validation prevents stale lock file issues
cmd/pvetui/main.go- Entry point, CLI setup with Cobrainternal/config/config.go- Configuration management with SOPS/age encryption supportpkg/api/interfaces/interfaces.go- Core interfaces for Logger, Cache, Configpkg/api/guest_agent_exec.go- QEMU guest agent command executiontest/testutils/integration_helpers.go- Integration test utilities with mock Proxmox server
- Supports both password-based (tickets/CSRF) and API token authentication
- AuthManager handles token caching, expiration, and automatic refresh
- Thread-safe token access with proper locking patterns
- BadgerDB for persistent cache (background GC with proper cleanup)
- FileCache for in-memory with optional persistence
- LRU eviction when cache exceeds maxSize (0 = unlimited)
- Namespaced caches for plugins (separate storage per plugin)
- Unit tests with mocked dependencies
- Integration tests require Proxmox environment (controlled by
PVETUI_INTEGRATION_TEST=true) - Mock Proxmox server in test utilities for offline testing
- Pre-commit hooks ensure code quality (go vet, golangci-lint, formatting)
- Recently added pluggable architecture for UI extensions
- Plugins disabled by default, opt-in via
plugins.enabledconfig - Community Scripts extracted to plugin
- Community Scripts metadata is no longer repo-backed JSON; the current live source is the public PocketBase API at
https://db.community-scripts.org/api/collections/script_scripts/records, while install scripts still come fromcommunity-scripts/ProxmoxVEand script paths are derived from PocketBasetype+slug - Ansible Toolkit plugin (
ansible) provides inventory generation + playbook execution flows from node/guest context menus - Namespaced cache support for plugin isolation
When developing new plugins:
- Location: Place plugins in
internal/plugins/<plugin-name>/(core logic) andinternal/ui/plugins/<plugin-name>/(UI integration) - Interface: Implement the plugin interface defined in
internal/ui/components/plugins.go - Registration: Register plugin in
internal/ui/plugins/loader.gofactory map - Caching: Use namespaced cache:
cache.GetNamespaced("plugin:<plugin-name>") - Documentation: Document plugin configuration in
README.mdand.env.example - Testing: Test plugin independently with mocked dependencies
- Graceful degradation: Plugins must gracefully handle being disabled
- Configuration: All plugin settings should have reasonable defaults
- Error handling: Return errors rather than panicking; let the host handle display
- Modal Pages: Implement
ModalPageNames() []stringto declare plugin modal pages for proper keyboard event handling - UI Display: Use color tags for keyboard shortcuts in UI text:
[primary]ESC[-]not[[ESC]] - Input Handling: Set
SetInputCapture()on the focused element (not container) to properly consume events - Global Actions: For plugin entries in the Global Menu, implement
components.GlobalActionPluginand keep core menu code plugin-agnostic.
Key architectural decisions and rationale:
- BadgerDB over BoltDB: Chosen for better concurrent read performance and automatic garbage collection
- tview over bubbletea: More mature widget system for complex TUI layouts with better documentation
- Clean Architecture enforcement: Enables easier testing and future API changes without affecting business logic
- Plugin opt-in model: Disabled by default to maintain security and performance baselines; users explicitly enable
- Interface-driven design: All public APIs accept interfaces for maximum testability and flexibility
- Namespaced plugin caching: Prevents cache key collisions and allows per-plugin cache management
- Plugin modal page registration: Plugins declare modal pages via
ModalPageNames()method instead of modifying core keyboard handler; maintains separation of concerns and enables self-contained plugins - CLI subcommands via
cliSession: Non-interactive CLI shares all auth/config/group logic with the TUI through acliSessionstruct that wraps either*api.Client(single profile) or*api.GroupClientManager(group/aggregate). Subcommand handlers call methods oncliSessionwithout knowing which mode they're in.BootstrapOptions.Quiet = truesuppresses TUI banners;SilenceUsage: trueon the Cobra root prevents runtime errors from printing the full help menu. - LXC exec via SSH + pct exec: LXC containers have no API exec equivalent (unlike QEMU guest agent). Use
SSHClientImpl.ExecuteContainerCommandDetailedfrom the command-runner package to runpct exec <ctid> -- <cmd>over SSH to the Proxmox node. SSH credentials are resolved per-VM viavm.SourceProfile→ global config fallback. - Agent skill packaging: Skills for the skills.sh ecosystem live in
skills/<skill-name>/SKILL.mdat the repo root. Install vianpx skills add owner/repo. Theskills/path is auto-discovered;docs/skills/is not.
- Do not push without explicit approval: Always ask for confirmation before pushing to any remote, even after committing locally.
- BadgerDB requires proper cleanup channel to prevent goroutine leaks
- Test files should use 0o600 permissions for sensitive data
- All API methods should have timeouts to prevent indefinite hangs
- Lock file validation must check PID to avoid corruption from stale locks
- Never log sensitive information (passwords, tokens, API keys)
- Always defer Close() calls immediately after successful resource acquisition
- Use context.WithTimeout() for all external calls, not context.Background()
- Proxmox API responses: Boolean-like fields return as integers (0/1), not JSON booleans - parse as
float64and convert tobool - Proxmox task completion: Use the
EndTimefield to detect completion (EndTime > 0 means done), not status string matching - status can be empty while running - Plugin modals: Always implement
ModalPageNames()in plugins to prevent global keybindings from firing in plugin modals - Keyboard events: Use
SetInputCapture()on focused elements (e.g., input fields, text views) not flex containers to properly consume events - UI text: Use tview color tags
[primary]text[-]for colored text; brackets need escaping as[[or use color tags instead - Form label readability: Use
newStandardForm()(ininternal/ui/components/form_helpers.go) instead of rawtview.NewForm()so form labels consistently usetheme.Colors.HeaderText; avoid relying on default secondary text for labels. For plugin code outsideinternal/ui/components, usecomponents.NewStandardForm()(exported wrapper). - tview QueueUpdateDraw deadlocks: Never call functions that use
QueueUpdateDrawfrom within anotherQueueUpdateDrawcallback - this creates nested calls that deadlock tview. Always separate UI updates into sequential, non-nested calls. - UI callback re-entrancy: Treat modal/button callbacks,
SetDoneFunc, and input handlers as UI-thread contexts. Prefergo func() { ... }for background work and keep callback bodies non-blocking. - TaskManager/UI notify path: If a background manager notifies UI code that uses
QueueUpdateDraw, dispatch the notifier asynchronously (e.g.go notify()) to avoid blocking UI event handlers. - Long-running plugin commands: If a plugin starts external processes (e.g., ansible), wire modal cancel actions to
context.CancelFunc; hiding the modal alone does not stop the process. - Pre-PR deadlock scan: Before merging UI changes, scan for risky call sites with
rg -n "QueueUpdateDraw|SetDoneFunc|SetInputCapture" internal/ui/components internal/taskmanagerand verify no nestedQueueUpdateDrawchains were introduced. - Pending state and refreshes: Always clear pending state BEFORE calling refresh functions (
manualRefresh,refreshVMData), as these functions check for pending operations and will block if any exist - Guest advanced filter persistence: When checking whether Guests filters are active, use
SearchState.HasActiveVMFilter()(not onlySearchState.Filter != "") so structured filters (status/type/node/tag) persist across manual refresh, auto-refresh, and VM refresh paths. - Header loading animation: Multiple overlapping
ShowLoadingcalls can spawn concurrent animations and make the spinner appear too fast; ensure loading state is serialized (single animation, cancelable ticker). - Context menu anchoring: Anchor node/guest context menus to their source list primitives (
nodeList/vmList), not the currently focused pane; focus can be in details and produce inconsistent placement. - Context menu geometry: Compute/clamp modal placement against the
pagescontainer rect (notmainLayout) to avoid low/clipped menus on smaller terminals. - Template guests are not ordinary stopped guests: Proxmox templates often surface with
status=stopped, but lifecycle UX must treatVM.Templateas authoritative: label templates explicitly in guest list/details, do not offer start actions, and exclude them from batch lifecycle actions. - Keybind unsetting semantics: Treat empty string keybinds (for example
global_menu: "") as explicit unbinds; do not silently reapply default keys. - Back navigation consistency: For non-input views, support both
EscandBackspacefor back navigation; avoid addingBackspacehandlers on input-focused views where it must remain text editing. - Cobra
SilenceUsage: SetSilenceUsage: trueon the root command so runtime errors (API failures, not-found, wrong guest type) don't print the full help menu. Without it, anyRunEerror dumps usage, which is confusing for CLI consumers. - CLI group mode detection: Check
result.InitialGroup != ""(frombootstrap.Bootstrap) to determine group mode; build aGroupClientManagerwith oneapi.Clientper profile usingcfg.GetProfileNamesInGroup(). Don't rely on single-client code paths when group mode is active — they only see one node's data. - CLI SSH credential resolution: For operations that need SSH to a node (e.g., LXC exec), resolve credentials with: source profile from
vm.SourceProfile→ active profile → global configSSHUser/SSHJumpHost. The same pattern is ininternal/ui/plugins/commandrunner/plugin.go:resolveSSHUser. - Shell quoting for pct exec: When building
pct exec <ctid> -- <cmd>as a shell string forsession.Run(), shell-quote each argument with single quotes and escape embedded single quotes as'\''. Passing unquoted arguments silently mis-parses commands with spaces or special characters.
Symptoms: Application fails to start with "lock file already exists" error
Root cause: Lock file PID validation failing or previous unclean shutdown
Fix:
- Check if pvetui is actually running:
ps aux | grep pvetui - If not running, manually remove:
rm ~/.cache/pvetui/pvetui.lock - If recurring, ensure
LockFile.Unlock()is properly deferred in all code paths
Symptoms: Increasing goroutine count, memory growth over time
Root cause: Missing cleanup channel or improper shutdown sequence
Fix:
- Ensure
badgerCache.Close()is deferred in all initialization code paths - Check that cleanup channel is being listened to and properly closed
- Use
deferimmediately after successful cache initialization
Symptoms: TUI freezes immediately after confirming an action (for example Start/Stop/Shutdown), keyboard input stops responding, and no further redraw occurs
Root cause: Nested or re-entrant QueueUpdateDraw usage, or synchronous UI notification from code running in a UI callback path
Fix:
- Check recent changes for nested update paths:
rg -n "QueueUpdateDraw|SetDoneFunc|SetInputCapture" internal/ui/components internal/taskmanager
- Ensure UI callbacks do not perform blocking work; move long-running work into goroutines.
- If a manager/background worker triggers UI refreshes, call notifier callbacks asynchronously (
go notify()), then useQueueUpdateDrawinside the notifier. - Re-run
make test-quickafter the fix and manually verify the action flow that froze.
Symptoms: Context menu appears too low/clipped, or appears on the right when opened while details pane has focus
Root cause: Menu placement anchored to current focus instead of source list, or placement math using the wrong container rect
Fix:
- Ensure node menus pass
a.nodeListand guest menus passa.vmListas explicit anchors. - Keep global menu centered; only context menus should use anchored placement.
- Calculate and clamp coordinates using the
pagesrect, then verify on smaller terminal sizes.
Symptoms: Startup keybinding error suggests interactive editor for keybind issues, or back keys behave inconsistently across screens
Root cause: Messaging assumes editor can fix keybinds; navigation handlers diverge across non-input views
Fix:
- For keybinding validation errors, direct users to manual config edits (interactive editor does not edit keybinds).
- Keep
Escas the reserved global fallback. - Standardize non-input "back" handling to accept both
EscandBackspace.
Symptoms: Tests hang indefinitely or timeout after long wait
Root cause: Missing context deadline in API calls
Fix:
- Always pass context with timeout (use
DefaultAPITimeoutconstant) - Example:
ctx, cancel := context.WithTimeout(context.Background(), api.DefaultAPITimeout) - Don't forget to
defer cancel()
Symptoms: Integration tests fail with connection errors
Root cause: Missing or incorrect Proxmox test environment setup
Fix:
- Ensure
PVETUI_INTEGRATION_TEST=trueis set - Configure
.env.testwith valid Proxmox credentials - Alternatively, use the mock server: check
test/testutils/integration_helpers.go
Symptoms: make code-quality reports linting errors
Root cause: Code doesn't meet golangci-lint standards
Fix:
- Run
golangci-lint runto see detailed errors - Many issues can be auto-fixed:
golangci-lint run --fix - For persistent issues, check
.golangci.ymlconfiguration - Ensure your editor is using the project's Go version (see
.go-version)
Symptoms: Application crashes with permission denied on cache operations
Root cause: Incorrect file permissions on cache directory or files
Fix:
- Cache directory should be 0o700:
chmod 700 ~/.cache/pvetui - Sensitive config files should be 0o600:
chmod 600 ~/.config/pvetui/config.yaml - Check that the application is creating files with correct permissions in the first place
When to Update: After completing significant work on the codebase, update this document with lessons learned and patterns discovered.
What to Add:
- New patterns discovered in the codebase
- Common pitfalls encountered during development
- Architectural decisions and their rationale
- Troubleshooting steps for new classes of issues
- Updates to development workflows or tooling
Format:
- Add concrete, actionable information
- Include examples where helpful
- Keep entries concise but complete
- Update the "Last Updated" date at the top
- Update the Table of Contents if adding new sections
For Future Agents: After completing your work session, review your changes and add relevant learnings to the appropriate sections above. This ensures institutional knowledge is preserved and future development is more efficient.