Compare commits

...
12 Commits
Author SHA1 Message Date
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
stefankoelle 4c0650c43b Fix tmux: add -- separator for shell commands, kill stale sessions 2026-08-09 20:33:07 +02:00
stefankoelle e36ac3568f Add --help, use os.Executable() for panel path, improve usage text 2026-08-09 20:29:55 +02:00
stefankoelle 697017e496 Add kctl-tui doctor command for health checks 2026-08-09 20:26:55 +02:00
stefankoelle 36a7cc5e3a Windows support: psmux hint, install.ps1, updated docs 2026-08-09 20:14:54 +02:00
21 changed files with 370 additions and 44 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal
+7 -6
View File
@@ -66,13 +66,14 @@ is still open. For the full requirements, see [SPEC.md](SPEC.md).
- [ ] Paginate/scroll the secrets diff table for secrets with many fields
instead of relying on terminal wrapping.
## Phase 3 — Windows-native support (open, secondary priority)
## Phase 3 — Windows-native support (done)
- [ ] Detect OS at runtime; on native Windows (no WSL), fall back to
`wt.exe split-pane` instead of `tmux` for the status panes.
- [ ] Document/implement that `Esc`-triggered session close is **not**
available in the native Windows fallback — the panes must be closed
manually there.
- [x] Windows support via [psmux](https://github.com/marlocarlo/psmux) —
a native, tmux-compatible terminal multiplexer. kctl-tui works
without code changes; `CheckTool("tmux")` error message includes
Windows-specific install hint.
- [x] `install.ps1` — PowerShell install script for Windows.
- [x] Updated README and SPEC with Windows + psmux setup instructions.
## Phase 4 — Nice-to-haves (open, not committed)
+34 -11
View File
@@ -68,12 +68,16 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
Configuration below - they must already exist in your kubeconfig, e.g.
added via `aws eks update-kubeconfig`).
- `k9s` (used for the two status panes).
- `tmux` (used for the 3-pane layout). On Windows, this means running
kctl-tui inside **WSL**`tmux` has no native Windows port. Native
Windows Terminal has its own split-pane feature, but it cannot be
scripted from inside a pane the way `tmux` can, so the automated 3-pane
layout and the `Esc` session handling described above are only fully
supported under Linux/WSL. See SPEC.md section 3.6 for details.
- `tmux` (used for the 3-pane layout). On **Linux/macOS**, install
`tmux` via your package manager. On **Windows**, install
[psmux](https://github.com/marlocarlo/psmux) — a native,
tmux-compatible terminal multiplexer:
```powershell
scoop install psmux
# or
cargo install psmux
```
psmux provides a `tmux` command, so kctl-tui works without changes.
- `aws` CLI, configured with credentials, only needed for the secrets
workflow.
@@ -85,6 +89,15 @@ See [SPEC.md](SPEC.md) for the full requirements and design rationale, and
curl -fsSL https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.sh | bash
```
### Quick install (Windows)
```powershell
irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex
```
This downloads the latest release binary for your architecture from
GitHub Releases and installs it to your PATH.
This downloads the latest release binary for your OS/architecture from
GitHub Releases and installs it to `/usr/local/bin/kctl-tui`.
@@ -167,11 +180,19 @@ aws_sso_login_command: "aws sso login"
that way — it typically contains your organization's internal account ID,
context naming, and label names.
## WSL setup notes
## Windows notes
If `kubectx`/`kubens` or `kctl-tui` report a missing kubeconfig inside WSL,
your kubeconfig most likely only exists on the Windows side. Symlink it
into WSL:
On native Windows (without WSL), install [psmux](https://github.com/marlocarlo/psmux)
for the 3-pane layout. psmux is a native Windows terminal multiplexer
that is tmux-compatible — kctl-tui works without code changes:
```powershell
scoop install psmux
# or
cargo install psmux
```
If you prefer WSL, symlink your kubeconfig into WSL:
```bash
mkdir -p ~/.kube
@@ -182,8 +203,10 @@ ln -s /mnt/c/Users/<your-windows-username>/.kube/config ~/.kube/config
```bash
kctl-tui # start the TUI (full navigation mode)
kctl-tui --help # show all commands and flags
kctl-tui --version # print version
kctl-tui --verbose # enable debug logging to stderr
kctl-tui doctor # check if all tools, config and connections are OK
kctl-tui config check # validate ~/.kctl-tui/config.yaml
kctl-tui panel --context=... --ns=... --team=... # internal (called by tmux)
```
@@ -204,4 +227,4 @@ and untested, since it has no meaningful behavior without a live cluster.
## License
[MIT](LICENSE)
Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
+8 -9
View File
@@ -89,6 +89,8 @@ not cover: **redeploy** and **secrets diff**.
Example startup command (generic placeholders):
```
# Kill stale session first (separate command — tmux aborts on kill-session error).
tmux kill-session -t kctl
tmux new-session -d -s kctl \
"kctl-tui panel --context=$CTX_A --ns=$NS --team=$TEAM" \; \
set-option -t kctl remain-on-exit on \; \
@@ -167,18 +169,15 @@ The `context_template` replaces `{region}`, `{account_id}`, `{env}`, and
selected environment/context. The resolved value must match an existing
context in your kubeconfig (e.g. added via `aws eks update-kubeconfig`).
**Platform limitation on Windows without WSL:** `kill-session` is
tmux-specific. Windows Terminal (`wt.exe`) offers no equivalent scripting
to end the session from inside a pane. On plain Windows (without WSL),
only a simplified flow is possible: k9s panes are closed manually (`q`,
then `Ctrl+Shift+W`); automatic session termination is unavailable there.
This limitation is the main reason the primary target system is set to
Linux/WSL.
**Windows support:** On native Windows, install
[psmux](https://github.com/marlocarlo/psmux) — a native, tmux-compatible
terminal multiplexer. psmux provides a `tmux` command, so kctl-tui works
without code changes (including `Esc`-triggered session termination).
Alternatively, run kctl-tui inside WSL with standard `tmux`.
## 4. Non-functional requirements
- **Primary platform Linux/WSL**, secondary native Windows with reduced
functionality.
- **Primary platform Linux/WSL**, secondary native Windows (via psmux).
- **Single-binary distribution** without external runtime dependency (Go
provides this natively).
- **External dependencies**: `kubectl` mandatory; `tmux`, `k9s`, `aws-cli`
+34 -14
View File
@@ -1,7 +1,11 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
package main
import (
"fmt"
"os"
"os/exec"
"sort"
@@ -265,7 +269,10 @@ func (m *fullModel) loadNamespacesFor(teamValue string) tea.Cmd {
// configured envs, resolved via the context template, so both are
// visible side by side.
func (m *fullModel) startTmuxSession() tea.Cmd {
selfPath := "kctl-tui"
selfPath, err := os.Executable()
if err != nil {
selfPath = "kctl-tui" // fallback to PATH lookup
}
panelCmd := fmt.Sprintf("%s panel --context=%s --ns=%s --team=%s",
selfPath, m.selectedContext, m.selectedNamespace, m.selectedTeam)
@@ -273,28 +280,41 @@ func (m *fullModel) startTmuxSession() tea.Cmd {
ctxA := m.cfg.ResolveContext(envA, m.selectedContext)
k9sCmdA := fmt.Sprintf("k9s --context %s -n %s", ctxA, m.selectedNamespace)
args := []string{
"new-session", "-d", "-s", "kctl",
panelCmd, ";",
"set-option", "-t", "kctl", "remain-on-exit", "on", ";",
"split-window", "-v", "-t", "kctl:0.0", k9sCmdA, ";",
kubeexec.VerboseLog("[debug] selfPath=%s\n", selfPath)
kubeexec.VerboseLog("[debug] panelCmd=%s\n", panelCmd)
kubeexec.VerboseLog("[debug] k9sCmdA=%s\n", k9sCmdA)
// Kill stale session first (ignore error if none exists).
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 {
envB := m.cfg.Envs[1]
ctxB := m.cfg.ResolveContext(envB, m.selectedContext)
k9sCmdB := fmt.Sprintf("k9s --context %s -n %s", ctxB, m.selectedNamespace)
args = append(args,
"split-window", "-v", "-t", "kctl:0.1", k9sCmdB, ";",
)
kubeexec.VerboseLog("[debug] k9sCmdB=%s\n", k9sCmdB)
setup = append(setup, []string{"split-window", "-v", "-t", "kctl:0.1", k9sCmdB})
}
args = append(args,
"select-layout", "-t", "kctl", "even-vertical", ";",
"select-pane", "-t", "kctl:0.0", ";",
"attach", "-t", "kctl",
setup = append(setup,
[]string{"select-layout", "-t", "kctl", "even-vertical"},
[]string{"select-pane", "-t", "kctl:0.0"},
)
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 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
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
// simpleItem is a minimal implementation of list.Item used for all
+155
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
import (
@@ -18,6 +21,7 @@ func main() {
// Extract global flags before delegating to sub-commands.
verbose := false
showHelp := false
filtered := make([]string, 0, len(args))
for _, a := range args {
switch a {
@@ -26,6 +30,8 @@ func main() {
case "--version", "-v":
fmt.Printf("kctl-tui %s\n", version)
return
case "--help", "-h":
showHelp = true
default:
filtered = append(filtered, a)
}
@@ -34,6 +40,14 @@ func main() {
kubeexec.SetVerbose(true, os.Stderr)
}
if showHelp {
printUsage()
return
}
if len(filtered) == 0 {
// No subcommand — start the full TUI.
}
if len(filtered) > 0 {
switch filtered[0] {
case "panel":
@@ -48,6 +62,16 @@ func main() {
os.Exit(1)
}
return
case "doctor":
if err := runDoctor(); err != nil {
fmt.Fprintln(os.Stderr, "kctl-tui doctor error:", err)
os.Exit(1)
}
return
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", filtered[0])
printUsage()
os.Exit(1)
}
}
@@ -59,6 +83,28 @@ func main() {
}
}
func printUsage() {
fmt.Print(`kctl-tui — Kubernetes entry-point TUI
Usage:
kctl-tui [flags] start the TUI (full navigation mode)
kctl-tui doctor check tools, config and connections
kctl-tui config check validate ~/.kctl-tui/config.yaml
kctl-tui panel [options] control pane (called internally by tmux)
Flags:
--verbose log all kubectl/aws commands to stderr
--version print version
--help show this help
Examples:
kctl-tui # start the TUI
kctl-tui doctor # verify everything is installed
kctl-tui --verbose 2>debug.log # log commands to a file
kctl-tui config check # validate config
`)
}
func runConfig(args []string) error {
if len(args) == 0 || args[0] != "check" {
return fmt.Errorf("usage: kctl-tui config check")
@@ -110,3 +156,112 @@ func runConfig(args []string) error {
}
return nil
}
func runDoctor() error {
pass := "ok"
fail := "FAIL"
warn := "WARN"
status := pass
errs := 0
check := func(label string, err error) {
if err != nil {
fmt.Printf(" [%s] %s: %v\n", fail, label, err)
status = fail
errs++
} else {
fmt.Printf(" [%s] %s\n", pass, label)
}
}
warnCheck := func(label string, err error) {
if err != nil {
fmt.Printf(" [%s] %s: %v\n", warn, label, err)
} else {
fmt.Printf(" [%s] %s\n", pass, label)
}
}
// --- Tools ---
fmt.Println("\nTools:")
check("kubectl", kubeexec.CheckTool("kubectl"))
check("tmux/psmux", kubeexec.CheckTool("tmux"))
check("k9s", kubeexec.CheckTool("k9s"))
warnCheck("aws CLI (optional)", kubeexec.CheckTool("aws"))
// --- Config ---
fmt.Println("\nConfig:")
cfgPath, err := config.DefaultPath()
if err != nil {
fmt.Printf(" [%s] config path: %v\n", fail, err)
errs++
status = fail
} else {
cfg, err := config.Load(cfgPath)
if err != nil {
fmt.Printf(" [%s] load config: %v\n", fail, err)
errs++
status = fail
} else {
check("config file exists", nil)
if len(cfg.Contexts) == 0 {
fmt.Printf(" [%s] contexts configured\n", fail)
errs++
status = fail
} else {
fmt.Printf(" [%s] contexts configured (%d)\n", pass, len(cfg.Contexts))
}
if len(cfg.Envs) == 0 {
fmt.Printf(" [%s] envs configured\n", fail)
errs++
status = fail
} else {
fmt.Printf(" [%s] envs configured (%d)\n", pass, len(cfg.Envs))
}
if cfg.ContextTemplate != "" {
ctx := cfg.ResolveContext(cfg.Envs[0], cfg.Contexts[0])
fmt.Printf(" [%s] context_template resolves to: %s\n", pass, ctx)
} else {
fmt.Printf(" [%s] context_template is empty\n", fail)
errs++
status = fail
}
if cfg.SecretNameTemplate != "" && len(cfg.Envs) > 0 {
secret := cfg.ResolveSecretName("example-ns", cfg.Envs[0])
fmt.Printf(" [%s] secret_name_template resolves to: %s\n", pass, secret)
}
}
}
// --- Connections ---
fmt.Println("\nConnections:")
if kubeexec.CheckTool("kubectl") == nil {
err := kubeexec.CheckAWSAuth()
if err == nil {
fmt.Printf(" [%s] kubectl cluster reachable\n", pass)
} else {
// Not fatal — cluster might be unreachable from this machine
fmt.Printf(" [%s] kubectl cluster: %v\n", warn, err)
}
}
if kubeexec.CheckTool("aws") == nil {
err := kubeexec.CheckAWSAuth()
if err == nil {
fmt.Printf(" [%s] AWS credentials valid\n", pass)
} else {
fmt.Printf(" [%s] AWS credentials: %v\n", warn, err)
}
} else {
fmt.Printf(" [%s] AWS credentials (aws CLI not installed)\n", warn)
}
// --- Summary ---
fmt.Println()
if status == pass {
fmt.Println("All checks passed. kctl-tui is ready to use.")
} else {
fmt.Printf("%d error(s) found. Fix the issues above and re-run: kctl-tui doctor\n", errs)
os.Exit(1)
}
return nil
}
+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
import (
+74
View File
@@ -0,0 +1,74 @@
# Install script for kctl-tui on Windows.
# Downloads the latest GitHub release binary matching the current architecture
# and installs it to your PATH.
#
# Usage (PowerShell):
# irm https://raw.githubusercontent.com/skoelle/kctl-tui/main/install.ps1 | iex
#
# Or save and run locally:
# .\install.ps1
#
# Requires: PowerShell 5.1+, internet access.
$ErrorActionPreference = "Stop"
$Repo = "skoelle/kctl-tui"
$BinName = "kctl-tui"
# --- Detect architecture ---
$arch = $env:PROCESSOR_ARCHITECTURE
switch ($arch) {
"AMD64" { $goarch = "amd64" }
"ARM64" { $goarch = "arm64" }
default {
Write-Error "Unsupported architecture: $arch"
exit 1
}
}
# --- Determine install directory ---
$installDir = "$env:USERPROFILE\bin"
if (-not (Test-Path $installDir)) {
New-Item -ItemType Directory -Path $installDir | Out-Null
}
# Add to PATH if not already there
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($currentPath -notlike "*$installDir*") {
[Environment]::SetEnvironmentVariable("Path", "$currentPath;$installDir", "User")
$env:Path = "$env:Path;$installDir"
Write-Host "Added $installDir to your PATH."
}
# --- Query GitHub API for latest release ---
Write-Host "Detecting latest release for $Repo ..."
try {
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -UseBasicParsing
} catch {
Write-Error "Failed to reach the GitHub API (network error). Check your internet connection and try again."
exit 1
}
$tag = $release.tag_name
if (-not $tag) {
Write-Error "Could not find a published release for $Repo. No release has been tagged yet."
exit 1
}
Write-Host "Latest release: $tag"
# --- Download binary ---
$asset = "kctl-tui-windows-${goarch}.exe"
$url = "https://github.com/$Repo/releases/download/$tag/$asset"
$outFile = "$installDir\$BinName.exe"
Write-Host "Downloading $asset ($tag) ..."
try {
Invoke-WebRequest -Uri $url -OutFile $outFile -UseBasicParsing
} catch {
Write-Error "Download failed: $_"
exit 1
}
Write-Host "Installed $BinName to $outFile"
Write-Host "Done. Run '$BinName' to get started."
+2
View File
@@ -1,4 +1,6 @@
#!/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.
# Downloads the latest GitHub release binary matching the current OS/arch
# 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
// configuration (contexts, envs, templates) from a YAML file.
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
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
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
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
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
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
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
import "testing"
+9 -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.
// All functions here have side effects (they run external processes) and
// are therefore not covered by unit tests; the pure logic they depend on
@@ -182,8 +185,11 @@ func RunAWSLogin(loginCommand string) *exec.Cmd {
// Returns nil if found, or a descriptive error if not.
func CheckTool(name string) error {
_, err := exec.LookPath(name)
if err != nil {
return fmt.Errorf("%q not found in PATH — please install it first", name)
}
if err == nil {
return nil
}
if name == "tmux" {
return fmt.Errorf("%q not found in PATH — install tmux (Linux/macOS) or psmux (Windows: scoop install psmux or cargo install psmux)", name)
}
return fmt.Errorf("%q not found in PATH — please install it first", name)
}
+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
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) {
mu.Lock()
defer mu.Unlock()