A terminal dashboard for a fleet of git repositories — what's dirty, what's unpushed, and what's worth releasing, across every checkout you own, live.
coldstorage is the Go reimplementation of
drydock, Andy Miller's Rust
dashboard for a fleet of git repos. drydock's ideas — the dashboard grammar,
the git semantics, the honest reporting — are the foundation this project is
built on; GO-PORT-SPEC.md is drydock's behavioral specification, adapted
for the port, and this repository follows it feature for feature. If you use
coldstorage and like it, go star
drydock — it is the original.
Why a rewrite exists, and why Go, is its own story:
REWRITE-FROM-DRYDOCK.md.
- Live fleet dashboard — every checkout on one full-width table: group, repo, branch, state, release verdict, changes, ahead/behind, tag, and age, with fleet totals in the header and an operation widget that narrates background work.
- Fast at 500–1,000+ repos — a fingerprint gate skips unchanged checkouts without spawning a single process, and per-repo filesystem watching re-probes the moment something moves; storm gates keep a burst of changes from stampeding the background.
- Org sync that never lies — register GitHub, GitLab, or Gitea/Forgejo
orgs and sync them through the provider's own CLI: clones what's missing,
updates with
pull --ff-only, reports orphans, skips diverged/dirty/ detached with the reason shown, and never merges, rewrites, or deletes. - Repo sync from anywhere —
ppulls the selected repo,Ppulls every discovered repo, with the same safety semantics and a progress widget. - Detail view — one repo's complete story: state, release placement, tags, changelog verdict, the full branch table with per-upstream ahead/behind, and its commit history as you scroll.
- Hand-offs —
topens a git TUI (lazygit, gitui, tig),oa TUI file manager (superfile, yazi, ranger, nnn), andTa shell in the repo's directory; the terminal is released to the child and coldstorage resumes and re-probes when it exits. - Group colors — paint a group's rows from the config or the
Goverlay, persisted to the toml on the spot. - Org filtering —
Ocycles the table down to a single registered organization's checkouts. - Themes that follow your shell — Omarchy's active theme, the terminal's own background (OSC 11), or the macOS system appearance; the verdict grammar reads on all of them.
- Filters that compose — dirty, unpushed, needs-release, conflicts, never-fetched and more, with any/all matching, group and org filters, age presets, sort cycling, and fuzzy search.
- Honest reporting —
?means nobody checked; an unfetched repo says so instead of inventing an ahead/behind count.
Homebrew (macOS and Linux):
brew install crueber/tap/coldstorageLinux / anywhere with a shell — installs to ~/.local/bin:
curl -fsSL https://raw.githubusercontent.com/crueber/coldstorage/main/install.sh | shSet COLDSTORAGE_INSTALL_DIR to install somewhere else. Every release also
ships a tarball per platform (zip on Windows) with the binary, the docs, and
a checksums.txt on the
releases page.
Or let Go build it:
go install github.com/crueber/coldstorage/cmd/coldstorage@latestOr from a clone:
git clone git@github.com:crueber/coldstorage.git
cd coldstorage && go install ./cmd/coldstorageRun coldstorage — there are no subcommands. First launch scans
~/Projects (the default root), and the config below is created at
~/.config/coldstorage/config.toml on Linux,
~/Library/Application Support/coldstorage/config.toml on macOS.
A minimal config is two lines:
roots = ["~/Projects", "~/dev/github.com"]A full reference (every key, every default) lives in GO-PORT-SPEC.md §4.
The shape:
roots = ["~/Projects", "~/dev/github.com"]
max_depth = 4 # how deep discovery descends
exclude = ["**/archived/**"] # doublestar globs; `x/**` excludes x
prune = ["node_modules", "vendor"] # directory names never descended
[refresh]
interval = "5m" # background resweep cadence
debounce = "1s" # watcher quiet window
watch = true # filesystem watching (per-repo, selective)
[remote]
fetch = true # fetch before computing ahead/behind
concurrency = 4
timeout = "20s"
[release]
tag_pattern = "*[0-9]*" # which tags count as releases
max_subjects = 30
[ui]
default_filters = [] # e.g. ["dirty", "unpushed"]
default_sort = "activity"
theme = "auto" # auto | dark | light — see Theming
git_client_command = [] # t — empty = auto-detect (lazygit, gitui, tig)
file_manager_command = [] # o — empty = auto-detect (spf, yazi, ranger, nnn)
terminal_command = [] # T — empty = $SHELL; {path} is the repo root
[ui.group_colors]
decisiv = "#1f2d3d" # any group name; hex or ANSI names (red, blue, bright-cyan, …)
crueber = "blue"
[[orgs]]
provider = "github" # github | gitlab | gitea
host = "github.com"
owner = "acme"
path = "~/dev/github.com/acme" # where checkouts live; created on first sync
# login = "work" # gitea/tea only: which login to use
# protocol = "https" # ssh (default) | https
# enabled = true # false = registered but never synced
# include_forks = false
# include_archived = false
# include_subgroups = false # gitlab only
# exclude = ["archived-*"]Durations are suffix-rich (500ms, 30s, 5m, 2h, 7d, 2w, 3mo,
1y; bare numbers are seconds).
Columns, left to right: GROUP · REPO · BRANCH · STATE · RELEASE · CHANGES · AHEAD · BEHIND · TAG · +TAG · AGE, with VISIBILITY, STASHES, and FETCHED toggleable. The table always spans the full terminal width — REPO and BRANCH are flexible and absorb the space — and column widths never reflow while you scroll or move the cursor.
Keys (table): j/k move · ctrl-d/ctrl-u half page · pgup/pgdn ·
home/end · ⏎ detail · d u r N b c i x e n filters (dirty, unpushed,
released, needs-release, bare, conflicts, needs-attention, excluded,
errors, never-fetched) · & any/all · a clear all · [ ] cycle group ·
O cycle org filter · 0-4 age presets · s cycle sort · S reverse ·
/ fuzzy search · p sync repo · P sync all · C column picker ·
A org manager · G group colors · R rescan · ? help · q quit.
The chrome: line one is the fleet — total repos, dirty, unpushed,
needs-release, and never-fetched counts — with the operation widget on the
right edge whenever background work is running, saying what the queue is
doing right now. Line two is the status line: the active filter summary,
the search prompt, or the last operation's result. The fleet also stays
live on its own: a sweep runs on the [refresh] interval, and per-repo
filesystem watching re-probes a checkout the moment something touches it —
storm gates keep a burst of changes from stampeding the background.
Hand-offs (t, o, T — on the table and in the detail view):
t opens a git TUI on the selected repo (detects lazygit, gitui, lg,
tig), o opens a TUI file manager there (spf, yazi, ranger, nnn),
and T hands the whole terminal to your shell in the repo's directory —
when the shell exits, coldstorage comes back and re-probes whatever the
shell left behind. The terminal is released to the child; coldstorage is
suspended, not competing. [ui] git_client_command,
file_manager_command, and terminal_command override detection, with
{path} standing in for the repo root.
Detail view (⏎): everything about one repo — state, release placement,
visibility, activity and its source, head, remote, fetch age, tags,
changelog verdict, the full branch table with ahead/behind per upstream, and
the repo's recent commit history (titles, paged in as you scroll).
j/k scrolls, esc/⏎ returns.
Org manager (A): a add · e edit · x x remove · s sync the
selected org · S sync every enabled one · esc close. The add form
probes gh/glab/tea auth first (off the UI thread), cycles only
through providers you're logged into, and auto-fills the checkout path.
Sync clones missing checkouts, updates with pull --ff-only, reports
orphans (checkouts the org no longer lists), and skips divergent, dirty,
or detached repos with the reason shown — nothing is ever merged or
deleted. Adding an org over a directory another registration covers
replaces the older registration, and interrupted-clone debris (a .git
with no HEAD) is cleared and re-cloned rather than wedging the repo.
Progress streams into the operation widget; a full sweep afterward makes
fresh clones appear immediately.
Colors follow your shell, not a palette this tool invented. With theme = "auto" (the default) coldstorage checks, in order:
- Omarchy's active theme —
~/.config/omarchy/current/theme, read from itsalacritty.tomlorkitty.conf— for your exact accents - the terminal's own background color (works in Alacritty, kitty, iTerm2, Terminal.app, and anything that answers OSC 11; on macOS the system appearance refines the verdict)
COLORFGBG, then a dark default
Set [ui] theme = "dark"|"light" to pin it. The verdict grammar — yellow
dirty, cyan unpushed, magenta needs-release, red conflicts, dim clean — is
the same in every theme.
Any group can carry a row background: [ui.group_colors] maps a group
name (case-insensitive — groups are org logins) to a hex color or an ANSI
name (red, blue, bright-cyan, …). Rows in that group render with
that background across the full table width; the selection highlight
outranks it, and the verdict colors stay foreground-only so they read on
any background. The selection highlight always wins.
Set them from the TUI: G opens the group colors overlay — pick a
group, enter/l cycles forward through a muted palette, h cycles
back, x clears. Every change is written to the config on the spot, so
the toml and the screen never disagree.
p pulls the selected repo, P pulls every discovered repo — on the table
and in the detail view. The semantics are the org sync's, unchanged:
pull --ff-only only, and every refusal (diverged, dirty, detached, no
upstream) is a skip with the reason shown, never a merge. Progress streams
into the header's operation widget; the tally lands on the status line and
a sweep afterward re-probes exactly the repos that moved. [remote]
timeout and concurrency bound the pass.
gofmt -l . # must print nothing
go vet ./...
go test ./... # network-free: tempdirs and fixtures onlyGO-PORT-SPEC.md is the behavioral contract — the code follows the spec,
and when they disagree the spec wins or is amended deliberately. AGENTS.md
carries the invariants (each with the incident that produced it) and the
dependency policy: tiny, no cgo, every new dependency justified in its
commit.
Releases are automated: pushing a v* tag runs the gates and publishes
the release package (six cross-compiled binaries — Linux, macOS, Windows on
x86_64 and arm64 — as archives with checksums and a changelog) via
GoReleaser. The config is .goreleaser.yaml; the pipeline is
.github/workflows/release.yml. Ordinary pushes get the same gates on CI
(.github/workflows/ci.yml), and every green main build is tagged and
released automatically by the release train
(.github/workflows/release-train.yml) — the Homebrew formula in
crueber/homebrew-tap is
regenerated per release.
- drydock by
Andy Miller (
yetidevworks) — the original Rust implementation and is the inspiration for this project: the dashboard, the git semantics, the release logic, the org sync, and the spec this port was built against. coldstorage exists because of it. - Built with Bubble Tea and Lip Gloss by the Charm team.
MIT — see LICENSE. drydock is also MIT, © Andy Miller; this reimplementation carries both copyrights with gratitude.
