Compare commits

..
11 Commits
Author SHA1 Message Date
stefankoelle 2911f97991 Add CHANGELOG, CONTRIBUTING, and GitHub Issue/PR templates for v0.2.0 2026-08-09 22:13:09 +02:00
stefankoelle 7db58b6fe8 Disable Bubbles list filter to prevent stuck filter state
The / filter in the list component captured keystrokes after returning
from tmux, showing random text like 'cc' and blocking cursor navigation.
Filtering is not needed for the 3-level context/team/namespace flow.
2026-08-09 22:07:49 +02:00
stefankoelle 201168e8cd Fix k9s on Windows: --namespace, --command pods
- -n flag not supported on Windows k9s, use --namespace instead
- Start k9s directly in pods view with --command pods
2026-08-09 21:57:24 +02:00
stefankoelle ffbf4341fa Remove debug output from TUI, use verbose-only logging for tmux setup
- Debug fmt.Fprintf calls were always visible in TUI
- Added VerboseLog() to kubeexec package for use by full.go
- All tmux debug output now only shows with --verbose flag
2026-08-09 21:46:25 +02:00
stefankoelle 37be0707ad Fix Windows: run tmux setup commands individually, attach via ExecProcess
psmux on Windows doesn't handle ; separators when args are passed
individually via exec.Command. Instead of chaining commands with ;,
run each setup command (new-session, set-option, split-window, etc.)
as individual exec.Command calls. Only tmux attach uses ExecProcess
so it properly takes over the terminal.
2026-08-09 21:34:46 +02:00
stefankoelle 9e6acdedc2 Fix Windows: use cmd.exe /c for tmux command chain
psmux on Windows doesn't handle ; separators when args are passed
individually via exec.Command. On Windows, build the full tmux
command string and run it through cmd.exe /c instead.
2026-08-09 21:27:37 +02:00
stefankoelle 07662dc9b7 Add debug logging to startTmuxSession for Windows troubleshooting 2026-08-09 21:20:27 +02:00
stefankoelle 38b28f64fb Fix: no-args should start TUI, not print help
The condition showHelp||len(filtered)==0 caused the tool to always
print usage and exit. Now only --help triggers usage+exit.
2026-08-09 21:00:45 +02:00
stefankoelle 6a749811c7 LICENSE 2026-08-09 20:59:04 +02:00
stefankoelle 313dffcbbf Fix Windows: remove -- separator (psmux incompatible), add unknown command error
- Remove -- separator from tmux args (psmux on Windows doesn't support it)
- Unknown subcommands now show error + usage (exit 1) instead of starting TUI
- --help now always shows usage and exits (no fallthrough to TUI)
2026-08-09 20:56:39 +02:00
stefankoelle 8546ac9bf2 Fix: kill stale tmux session as separate command, not in chain
tmux aborts the entire command chain when kill-session fails (no
existing session). This caused 'no current target' error on Linux.

The kill-session is now a separate exec.Command() call before
starting the tmux chain, ignoring any error.
2026-08-09 20:45:44 +02:00
24 changed files with 343 additions and 28 deletions
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug Report
about: Report a bug to help us improve kctl-tui
title: ""
labels: bug
assignees: ""
---
## Describe the Bug
A clear description of what the bug is.
## Steps to Reproduce
1. Run `kctl-tui ...`
2. Select '...'
3. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Environment
- OS: [e.g. Windows 11, Ubuntu 24.04, macOS 15]
- Go version: [e.g. 1.22.5]
- kctl-tui version: [e.g. v0.2.0]
- kubectl version: [e.g. v1.30.0]
- tmux/psmux version: [e.g. tmux 3.4, psmux latest]
- Terminal: [e.g. Windows Terminal, iTerm2, GNOME Terminal]
## Additional Context
Config file (without secrets), error logs, or screenshots.
+23
View File
@@ -0,0 +1,23 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: ""
labels: enhancement
assignees: ""
---
## Problem
What problem does this feature solve?
## Proposed Solution
Describe the solution you'd like.
## Alternatives Considered
Any alternative solutions or workarounds you considered.
## Additional Context
Any mockups, examples, or references.
+23
View File
@@ -0,0 +1,23 @@
## Summary
Brief description of what this PR does.
## Changes
- ...
## Related Issues
Closes #
## Testing
- [ ] `go vet ./...` passes
- [ ] `go test ./...` passes
- [ ] Manually tested on [OS]
## Checklist
- [ ] Code follows existing style
- [ ] No new comments added (unless necessary)
- [ ] CHANGELOG.md updated (if applicable)
+59
View File
@@ -0,0 +1,59 @@
# Changelog
All notable changes to kctl-tui will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
## [0.2.0] - 2026-08-09
### Added
- **Windows support** via [psmux](https://github.com/marlocarlo/psmux) as tmux-compatible multiplexer
- `install.ps1` PowerShell install script for Windows
- `--help` flag with full usage documentation
- `--version` / `-v` flag (set via `-ldflags` at build time)
- `kctl-tui doctor` command to verify tools, config and cluster connectivity
- `kctl-tui config check` command to validate `~/.kctl-tui/config.yaml`
- `--verbose` flag for debug logging of all kubectl/aws commands to stderr
- Binary secret value detection (`IsBinary`) — base64 or non-UTF-8 content is flagged
- ExternalSecret name validation before force-sync
- `CheckTool()` and `CheckAWSAuth()` helpers for pre-flight checks
- Diff table scroll support (j/k, up/down arrows)
- `--command pods` flag for k9s to start directly in pod view
- `--namespace` flag for k9s (Windows compatibility)
### Fixed
- Tmux session cleanup: stale sessions are killed before creating new ones
- Panel path: uses `os.Executable()` instead of PATH lookup for correct binary
- k9s on Windows: `--namespace` instead of `-n`, direct pods view
- `--help` always shows usage and exits (no fallthrough to TUI)
- Unknown subcommands show error message + usage (exit 1)
- Panel quit: properly closes tmux session and exits
- Bubbles list filter disabled to prevent stuck filter state after tmux return
### Changed
- Config uses `k8s_secret_name_template` for Kubernetes secret names (separate from AWS `secret_name_template`)
- Contexts resolved via template (`context_template`) instead of live kubectl discovery
- Panel redesigned: env-first menu (quit/beta/prod) with secrets sync + redeploy
- Full-mode navigation starts at team selection with default context
- `tea.ExecProcess` only used for `tmux attach` — setup commands run synchronously
### Removed
- Context-pair logic (superseded by env-template based context resolution)
- Live kubectl discovery (superseded by config templates)
## [0.1.0] - 2026-07-XX
### Added
- Initial release with Bubble Tea TUI
- 3-pane tmux orchestration (control panel + 2x k9s)
- Team/namespace navigation with kubectl context switching
- Panel mode with redeploy and secrets diff/force-sync
- AWS SSO integration with interactive login prompt
- CI/CD pipeline with Go build and release
- Linux/macOS install script
[Unreleased]: https://github.com/skoelle/kctl-tui/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/skoelle/kctl-tui/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/skoelle/kctl-tui/releases/tag/v0.1.0
+95
View File
@@ -0,0 +1,95 @@
# Contributing to kctl-tui
Thanks for your interest in contributing! This document explains how to get
started.
## Development Setup
```bash
git clone https://github.com/skoelle/kctl-tui.git
cd kctl-tui
go mod download
```
### Prerequisites
- Go 1.22+
- kubectl, k9s, tmux (or psmux on Windows)
- An active Kubernetes cluster for integration testing
### Running Locally
```bash
go run ./cmd/kctl-tui
```
### Building
```bash
go build -o kctl-tui ./cmd/kctl-tui
```
### With Version Tag
```bash
go build -ldflags "-X main.version=v0.2.0" -o kctl-tui ./cmd/kctl-tui
```
## Project Structure
```
cmd/kctl-tui/ Entry points (main, full mode, panel mode)
internal/config/ YAML config loading and template resolution
internal/kctl/ Pure logic (secret diffing, template engine)
internal/kubeexec/ kubectl/aws/tmux wrappers (side effects only)
```
### Architecture Rules
- **Pure logic** goes into `internal/kctl` or `internal/config` — no exec, no I/O.
- **Side effects** (running kubectl, aws, tmux) go into `internal/kubeexec`.
- **UI** lives in `cmd/kctl-tui/` — Bubble Tea models, views, handlers.
- Unit tests cover pure logic only. Side-effect packages are tested via
integration/manual tests.
## Testing
```bash
go vet ./... # static analysis
go test ./... # unit tests
```
There are no integration tests yet. Manual testing against a real cluster is
expected for UI and kubeexec changes.
## Code Style
- Standard Go formatting (`gofmt`).
- No comments unless the logic is non-obvious.
- Error messages should be actionable — tell the user what to fix.
- Log commands with `kubeexec.VerboseLog()` when `--verbose` is active.
## Commits
- One logical change per commit.
- Imperative mood in commit messages ("Add ...", "Fix ...", "Remove ...").
- No co-authors in commits.
## Pull Requests
1. Fork the repo and create a feature branch.
2. Make your changes following the style guide above.
3. Run `go vet` and `go test`.
4. Open a PR against `main` with a clear description of what changed and why.
5. Reference any related issues.
## Issues
- Use the provided issue templates.
- Include your OS, Go version, and kctl-tui version.
- For bugs: steps to reproduce, expected vs actual behavior.
## License
By contributing, you agree that your contributions will be licensed under the
MIT License.
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 Stefan Koelle Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -227,4 +227,4 @@ and untested, since it has no meaningful behavior without a live cluster.
## License ## License
[MIT](LICENSE) Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
+7 -6
View File
@@ -75,9 +75,9 @@ Once start navigation is complete, the tool opens a tmux session with
| -> runs the kctl-tui binary in "panel" mode | | -> runs the kctl-tui binary in "panel" mode |
| -> menu: Redeploy, secrets diff | | -> menu: Redeploy, secrets diff |
+--------------------------------------------------+ +--------------------------------------------------+
| Pane 1 (middle): k9s --context <context-a> -n <ns>| | Pane 1 (middle): k9s --context <context-a> --namespace <ns> --command pods|
+--------------------------------------------------+ +--------------------------------------------------+
| Pane 2 (bottom): k9s --context <context-b> -n <ns>| | Pane 2 (bottom): k9s --context <context-b> --namespace <ns> --command pods|
+--------------------------------------------------+ +--------------------------------------------------+
``` ```
@@ -89,12 +89,13 @@ not cover: **redeploy** and **secrets diff**.
Example startup command (generic placeholders): Example startup command (generic placeholders):
``` ```
tmux kill-session -t kctl \; \ # Kill stale session first (separate command — tmux aborts on kill-session error).
tmux kill-session -t kctl
tmux new-session -d -s kctl \ tmux new-session -d -s kctl \
-- "kctl-tui panel --context=$CTX_A --ns=$NS --team=$TEAM" \; \ "kctl-tui panel --context=$CTX_A --ns=$NS --team=$TEAM" \; \
set-option -t kctl remain-on-exit on \; \ set-option -t kctl remain-on-exit on \; \
split-window -v -t kctl:0.0 -- "k9s --context $CTX_A -n $NS" \; \ split-window -v -t kctl:0.0 "k9s --context $CTX_A --namespace $NS --command pods" \; \
split-window -v -t kctl:0.1 -- "k9s --context $CTX_B -n $NS" \; \ split-window -v -t kctl:0.1 "k9s --context $CTX_B --namespace $NS --command pods" \; \
select-layout -t kctl even-vertical \; \ select-layout -t kctl even-vertical \; \
select-pane -t kctl:0.0 \; \ select-pane -t kctl:0.0 \; \
attach -t kctl attach -t kctl
+32 -16
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main package main
import ( import (
@@ -47,6 +50,7 @@ func newFullModel() *fullModel {
l := list.New(nil, list.NewDefaultDelegate(), 0, 0) l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
l.Title = "kctl-tui" l.Title = "kctl-tui"
l.SetShowStatusBar(false) l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
return &fullModel{list: l} return &fullModel{list: l}
} }
@@ -275,31 +279,43 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
envA := m.cfg.Envs[0] envA := m.cfg.Envs[0]
ctxA := m.cfg.ResolveContext(envA, m.selectedContext) ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
k9sCmdA := fmt.Sprintf("k9s --context %s -n %s", ctxA, m.selectedNamespace) k9sCmdA := fmt.Sprintf("k9s --context %s --namespace %s --command pods", ctxA, m.selectedNamespace)
args := []string{ kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath)
"kill-session", "-t", "kctl", ";", // ignore error if no session exists kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd)
"new-session", "-d", "-s", "kctl", kubeexec.VerboseLog("[debug] k9sCmdA=%s\n", k9sCmdA)
"--", panelCmd, ";",
"set-option", "-t", "kctl", "remain-on-exit", "on", ";", // Kill stale session first (ignore error if none exists).
"split-window", "-v", "-t", "kctl:0.0", "--", k9sCmdA, ";", exec.Command("tmux", "kill-session", "-t", "kctl").Run()
// Run setup commands individually — this avoids the ; separator
// issue on Windows where psmux doesn't handle chained args.
setup := [][]string{
{"new-session", "-d", "-s", "kctl", panelCmd},
{"set-option", "-t", "kctl", "remain-on-exit", "on"},
{"split-window", "-v", "-t", "kctl:0.0", k9sCmdA},
} }
if len(m.cfg.Envs) > 1 { if len(m.cfg.Envs) > 1 {
envB := m.cfg.Envs[1] envB := m.cfg.Envs[1]
ctxB := m.cfg.ResolveContext(envB, m.selectedContext) ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
k9sCmdB := fmt.Sprintf("k9s --context %s -n %s", ctxB, m.selectedNamespace) k9sCmdB := fmt.Sprintf("k9s --context %s --namespace %s --command pods", ctxB, m.selectedNamespace)
args = append(args, kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB)
"split-window", "-v", "-t", "kctl:0.1", "--", k9sCmdB, ";", setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB})
)
} }
args = append(args, setup = append(setup,
"select-layout", "-t", "kctl", "even-vertical", ";", []string{"select-layout", "-t", "kctl", "even-vertical"},
"select-pane", "-t", "kctl:0.0", ";", []string{"select-pane", "-t", "kctl:0.0"},
"attach", "-t", "kctl",
) )
c := exec.Command("tmux", args...) for _, args := range setup {
if out, err := exec.Command("tmux", args...).CombinedOutput(); err != nil {
kubeexec.VerboseLog("[debug] tmux %s failed: %v\n%s\n", args[0], err, out)
return func() tea.Msg { return tmuxDoneMsg{err: fmt.Errorf("tmux %s: %w", args[0], err)} }
}
}
// Only attach uses tea.ExecProcess so it takes over the terminal.
c := exec.Command("tmux", "attach", "-t", "kctl")
return tea.ExecProcess(c, func(err error) tea.Msg { return tea.ExecProcess(c, func(err error) tea.Msg {
return tmuxDoneMsg{err: err} return tmuxDoneMsg{err: err}
}) })
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main package main
import "github.com/skoelle/kctl-tui/internal/kctl" import "github.com/skoelle/kctl-tui/internal/kctl"
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main package main
// simpleItem is a minimal implementation of list.Item used for all // simpleItem is a minimal implementation of list.Item used for all
+12 -4
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main package main
import ( import (
@@ -37,11 +40,12 @@ func main() {
kubeexec.SetVerbose(true, os.Stderr) kubeexec.SetVerbose(true, os.Stderr)
} }
if showHelp || len(filtered) == 0 { if showHelp {
printUsage() printUsage()
if showHelp && len(filtered) == 0 { return
return }
} if len(filtered) == 0 {
// No subcommand — start the full TUI.
} }
if len(filtered) > 0 { if len(filtered) > 0 {
@@ -64,6 +68,10 @@ func main() {
os.Exit(1) os.Exit(1)
} }
return return
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0])
printUsage()
os.Exit(1)
} }
} }
+4
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main package main
import ( import (
@@ -81,6 +84,7 @@ func newPanelModel(context, ns, team string) *panelModel {
l := list.New(nil, list.NewDefaultDelegate(), 0, 0) l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
l.SetShowStatusBar(false) l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
m := &panelModel{context: context, ns: ns, team: team, cfg: cfg, step: stepEnvMenu, list: l, input: ti} m := &panelModel{context: context, ns: ns, team: team, cfg: cfg, step: stepEnvMenu, list: l, input: ti}
if loadErr != nil { if loadErr != nil {
+2
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
# Licensed under the MIT License. See LICENSE file in project root for details.
# Install script for kctl-tui. # Install script for kctl-tui.
# Downloads the latest GitHub release binary matching the current OS/arch # Downloads the latest GitHub release binary matching the current OS/arch
# and installs it to /usr/local/bin (or $INSTALL_DIR if set). # and installs it to /usr/local/bin (or $INSTALL_DIR if set).
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
// Package config loads the user-specific, non-versioned kctl-tui // Package config loads the user-specific, non-versioned kctl-tui
// configuration (contexts, envs, templates) from a YAML file. // configuration (contexts, envs, templates) from a YAML file.
package config package config
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package config package config
import ( import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kctl package kctl
import "sort" import "sort"
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kctl package kctl
import "testing" import "testing"
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kctl package kctl
import "sort" import "sort"
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kctl package kctl
import ( import (
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kctl package kctl
import "strings" import "strings"
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kctl package kctl
import "testing" import "testing"
+3
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
// Package kubeexec wraps kubectl/aws-cli invocations used by kctl-tui. // Package kubeexec wraps kubectl/aws-cli invocations used by kctl-tui.
// All functions here have side effects (they run external processes) and // All functions here have side effects (they run external processes) and
// are therefore not covered by unit tests; the pure logic they depend on // are therefore not covered by unit tests; the pure logic they depend on
+13
View File
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package kubeexec package kubeexec
import ( import (
@@ -26,6 +29,16 @@ func SetVerbose(enabled bool, w io.Writer) {
} }
} }
// VerboseLog writes a message to the verbose log if enabled.
func VerboseLog(format string, args ...interface{}) {
mu.Lock()
defer mu.Unlock()
if !verbose {
return
}
fmt.Fprintf(logOut, format, args...)
}
func logCmd(name string, args ...string) { func logCmd(name string, args ...string) {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()