Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 52 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
# kamal-tui

[![Go Report Card](https://goreportcard.com/badge/github.com/stawan15/kamal-tui)](https://goreportcard.com/report/github.com/stawan15/kamal-tui)
[![GitHub Release](https://img.shields.io/github/v/release/stawan15/kamal-tui)](https://github.com/stawan15/kamal-tui/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
A multi-panel, terminal UI (TUI) for managing [Kamal](https://kamal-deploy.org/) deployments effortlessly.

A `lazygit`-style Terminal UI (TUI) dashboard for [Kamal](https://kamal-deploy.org).
Built with Go and [Bubble Tea](https://github.com/charmbracelet/bubbletea), it provides a fast, interactive, and secure way to manage your Rails/Docker applications.

Instead of running `kamal deploy -d destination` blindly and waiting, `kamal-tui` provides a rich interactive interface that lets you select your destination, perform actions (deploy, rollback, logs, etc.), and watch the streamed output all on one screen.
![Demo](https://github.com/stawan15/kamal-tui/assets/placeholder.png)

![Demo](demo.gif)
## 🌟 Features

## Features
- **Lazygit-style UI:** See Destinations, Actions, and Logs all in one unified screen.
- **Secure Secrets Manager:** Store Kamal secrets (like `DATABASE_URL`) directly in your OS Keychain (like Azure Pipelines). No more plaintext `.env` or `.kamal/secrets` files hanging around!
- **Interactive Confirmations:** Built-in confirmations for all commands to prevent accidental deployments or rollbacks.
- **DB Dump & Restore:** Built-in actions to quickly run `pg_dump` and `pg_restore` (Fully customizable for your stack).
- **Keyboard & Mouse Support:** Navigate lists with mouse scroll, clicks, or keyboard (`j/k`, `tab`).

- **Multi-Panel Interface:** See your destinations, actions, and output streams simultaneously.
- **Mouse & Keyboard Support:** Click on elements or use handy keybindings (`d` for deploy, `r` for rollback, `l` for logs).
- **Fast Navigation:** Use `Tab` or `h`/`l` to jump between panels.
- **Streaming Output:** View `kamal` command output directly in the dashboard without switching context.

## Installation

### Using Homebrew (macOS / Linux)
## 🚀 Installation

### Option 1: Using Homebrew (macOS / Linux)
```bash
brew install stawan15/tap/kamal-tui
# Add the tap and install
brew tap stawan15/kamal-tui
brew install kamal-tui
```

### Using Go

### Option 2: Using Go
If you have Go 1.20+ installed, you can build and install it directly:
```bash
go install github.com/stawan15/kamal-tui@latest
```

### Binary Release

Download the pre-compiled binary for your OS/Architecture from the [Releases page](https://github.com/stawan15/kamal-tui/releases).
## 🎮 Usage

## Usage

Simply run `kamal-tui` from the root of your project where `config/deploy.yml` (and other destination configs like `config/deploy.staging.yml`) is located.
Simply run `kamal-tui` from the root of your Rails (or any Kamal-managed) project:

```bash
cd your-rails-project
kamal-tui
```

### Keybindings
### Keyboard Shortcuts

- **`Tab`, `h`, `l`**: Switch focus between panels (Destinations / Actions / Logs)
- **`d`**: Quick deploy the selected destination
- **`r`**: Quick rollback the selected destination
- **`l`**: Quick view logs for the selected destination
- **`Enter`**: Execute the highlighted action on the selected destination
- **`q`, `Ctrl+C`**: Quit
- **`Esc`**: Cancel current input / operation
| Key | Action |
|-----|--------|
| `d` | Quick Deploy |
| `r` | Quick Rollback (Prompts for version) |
| `l` | View App Logs |
| `s` | Open Secure Secrets Manager |
| `tab` | Switch between Destinations, Actions, and Logs panels |
| `q` | Quit |

## Contributing
### 🔐 Managing Secrets Securely

1. Press `s` in the TUI to open the **Secure Secrets Manager**.
2. Press `a` to add a new secret (e.g. `DATABASE_URL`).
3. Type the secret value (it will be hidden as `***`).
4. Press `Enter` to save. It is securely encrypted in your OS keychain.
5. When you run any `kamal` command from the TUI, these secrets are injected into the command's environment on the fly.
6. Press `delete` or `x` to remove a secret.

See [CONTRIBUTING.md](CONTRIBUTING.md) for how you can help!
## 💾 Customizing Database Backups (Dump/Restore)

By default, the DB Dump and Restore actions in `kamal-tui` are configured for PostgreSQL (`pg_dump` and `pg_restore`). They execute inside the Kamal app container using your `$DATABASE_URL`.

If you use a different database (e.g. MySQL, SQLite) or want to run an accessory command, you can customize this by editing `kamal.go`:

```go
// In kamal.go
args = append(args, "--", "/bin/sh", "-c", "pg_dump $DATABASE_URL -F c > /tmp/db.dump")
```

## Requirements
- `kamal` command available in your PATH (or `bundle exec kamal` in a Ruby project).

## Contributing
Bug reports and pull requests are welcome on GitHub!
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.1.2
github.com/charmbracelet/lipgloss v1.0.0
github.com/joho/godotenv v1.5.1
github.com/zalando/go-keyring v0.2.8
)

require (
Expand All @@ -16,7 +18,6 @@ require (
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
Expand All @@ -26,7 +27,6 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/zalando/go-keyring v0.2.8 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.3.8 // indirect
Expand Down
12 changes: 10 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4h
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
Expand All @@ -36,20 +38,26 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
45 changes: 32 additions & 13 deletions kamal.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,35 @@ func (a actionItem) FilterValue() string { return a.title }
func actions() []actionItem {
return []actionItem{
{
title: "Deploy",
title: "🚀 Deploy",
desc: "kamal deploy -d <destination>",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"deploy"}, dest)
},
},
{
title: "Setup",
title: "⚙️ Setup",
desc: "kamal setup -d <destination> (provision servers & deploy)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"setup"}, dest)
},
},
{
title: "Env Push",
title: "🔑 Env Push",
desc: "kamal env push -d <destination> (push .env variables to servers)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"env", "push"}, dest)
},
},
{
title: "Redeploy",
title: "♻️ Redeploy",
desc: "kamal redeploy -d <destination> (skip build cache invalidation steps)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"redeploy"}, dest)
},
},
{
title: "Rollback",
title: "Rollback",
desc: "kamal rollback <version> -d <destination>",
needsVersion: true,
buildArgs: func(dest, version string) []string {
Expand All @@ -66,35 +66,55 @@ func actions() []actionItem {
},
},
{
title: "App Details",
title: "💾 DB Dump (Backup)",
desc: "kamal app exec -i -- /bin/sh -c 'pg_dump ...'",
buildArgs: func(dest, _ string) []string {
args := []string{"app", "exec", "-i"}
args = withDest(args, dest)
args = append(args, "--", "/bin/sh", "-c", "pg_dump $DATABASE_URL -F c > /tmp/db.dump")
return args
},
},
{
title: "💿 DB Restore",
desc: "kamal app exec -i -- /bin/sh -c 'pg_restore ...'",
buildArgs: func(dest, _ string) []string {
args := []string{"app", "exec", "-i"}
args = withDest(args, dest)
args = append(args, "--", "/bin/sh", "-c", "pg_restore -d $DATABASE_URL --clean --no-owner /tmp/db.dump")
return args
},
},
{
title: "ℹ️ App Details",
desc: "kamal app details -d <destination>",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"app", "details"}, dest)
},
},
{
title: "App Logs",
title: "📝 App Logs",
desc: "kamal app logs -d <destination> (last lines, no follow)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"app", "logs"}, dest)
},
},
{
title: "App Boot",
title: "App Boot",
desc: "kamal app boot -d <destination>",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"app", "boot"}, dest)
},
},
{
title: "Audit",
title: "🕒 Audit",
desc: "kamal audit -d <destination> (recent deploy history)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"audit"}, dest)
},
},
{
title: "Remove",
title: "🗑️ Remove",
desc: "kamal remove -d <destination> (remove containers and images from servers)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"remove"}, dest)
Expand Down Expand Up @@ -172,7 +192,7 @@ func runKamal(ctx context.Context, dest string, prefixArgs []string, args []stri
full := append(append([]string{}, prefixArgs...), args...)
bin, extra, ok := kamalBinaryAvailable()
if !ok {
doneCh <- fmt.Errorf("kamal binary not found")
doneCh <- errBinaryNotFound
close(lineCh)
return
}
Expand Down Expand Up @@ -232,7 +252,7 @@ func loadEnvForDest(dest string) []string {
}
}

// Load securely stored secrets from keychain
// Load secrets from keychain last so they intentionally override file values.
keychainSecrets := loadSecrets()
for k, v := range keychainSecrets {
envMap[k] = v
Expand All @@ -255,4 +275,3 @@ func loadEnvForDest(dest string) []string {
}
return cmdEnv
}

Loading