repo-tender ๐ŸŒฒ

Gem Version

Keep your local git clones forever fresh! ใƒฝ(โ€ขโ€ฟโ€ข)ใƒŽโœจ

repo-tender keeps your local git clones evergreen โ€” clean, on their default branch, and recently fetched โ€” so anything that reads them gets a current, trustworthy copy from your local disk instead of the network! ๐Ÿชžโšก

What does "evergreen" mean? ๐ŸŒฟ

A clone is evergreen when all three of these hold at once:

  • ๐Ÿงผ Clean โ€” no modified, staged, untracked, or deleted files
  • ๐ŸŒณ On the default branch โ€” whatever the remote calls it (main, trunk, masterโ€ฆ); repo-tender resolves it from the remote and never assumes!
  • ๐Ÿ•ฐ๏ธ Fresh โ€” fast-forwarded to the remote within your refresh_interval (default 6h)

repo-tender's whole job is to keep a tidy local mirror current, so another tool can clone any of them from ~/src/evergreen/... instantly. ๐Ÿš€

Perfect for:

  • ๐Ÿชž A local mirror of the repos you clone from constantly
  • ๐ŸŽ๏ธ A downstream "workspace" tool that clones from local disk, not the network
  • ๐Ÿ™ Keeping a whole GitHub org checked out and current
  • ๐ŸŒ™ Hands-off, set-and-forget background maintenance

Why repo-tender rocks:

  • ๐Ÿ”’ Never destroys your work โ€” dirty or diverged repos are reported, never touched. No reset --hard, ever.
  • โšก Concurrent sync sweep powered by socketry/async โ€” fibers all the way down, one process, no thread soup
  • ๐Ÿค– A periodic launchd job syncs on a schedule while you sleep
  • ๐Ÿ™ Track individual repos or whole GitHub orgs (expanded via gh)
  • ๐ŸŽ›๏ธ Interactive, plain, or JSON output โ€” pretty for you, parseable for scripts
  • ๐Ÿ’Ž Built on dry-rb โ€” validated YAML config, Result-typed boundaries

Requirements ๐ŸŒ

repo-tender is macOS-only (it schedules via launchd) and GitHub-only (it lists orgs via gh) โ€” both sit behind decoupled interfaces, but those are today's implementations.

Tool Version Why
๐ŸŽ macOS โ€” launchd scheduling, ~/Library/LaunchAgents
mise 2026.6+ pins and provides Ruby
๐Ÿ’Ž Ruby 4.0.5 runtime (pinned in mise.toml)
git 2.54+ the only SCM
gh 2.93+ GitHub org listing (must be authenticated)

Installation ๐Ÿ“ฆ

We're on RubyGems! ๐ŸŽ‰

gem install repo-tender
repo-tender --help

Prefer to hack on it? Install from source instead: ๐Ÿ› ๏ธ

git clone git@github.com:jetpks/repo-tender.git
cd repo-tender
mise install        # installs Ruby 4.0.5 per mise.toml
bundle install
bin/repo-tender --help

Make sure gh is logged in (otherwise org listing drops to an anonymous 60 req/hour limit):

gh auth status

And you're off! ๐ŸŽ€

Features โœจ

  • The evergreen invariant โ€” clean ยท on default branch ยท fresh, checked per repo
  • Safe by default โ€” dirty/diverged repos are reported and left byte-for-byte alone
  • Whole-org tracking โ€” add socketry and get every repo it owns
  • Bounded concurrency โ€” a fast async fan-out that won't melt your machine
  • launchd scheduling โ€” install, start, stop, restart, status
  • Default-branch aware โ€” resolves trunk/master/main from the remote
  • Three output modes โ€” interactive TUI, plain text, or line-delimited JSON

Quick Start ๐ŸŽ€

Track a repo ๐Ÿ™

Repos are named host/owner/name:

repo-tender repo add github.com/ruby/ruby
# => added: github.com/ruby/ruby

repo-tender repo list
# => github.com/ruby/ruby

Track a whole org ๐ŸŒ

Orgs expand to their member repos at sync time (archived repos and forks are excluded by default):

repo-tender org add socketry              # host defaults to github.com
repo-tender org add github.com/socketry   # equivalent, explicit host

Sync everything now โšก

repo-tender sync

Clones what's missing, fast-forwards what's clean-and-behind, and reports (never touches!) anything dirty or diverged. Scope it to one repo with --repo github.com/ruby/ruby. ๐ŸŽฏ

Check your repos' health ๐Ÿฉบ

repo-tender status
REPO                            STATUS  DEFAULT_BRANCH  LAST_SYNCED_AT        LAST_FETCH_AT
github.com/dry-rb/dry-monads    clean   main            2026-06-14T20:01:34Z  2026-06-14T20:01:33Z
github.com/ruby/ruby            dirty   trunk           2026-06-14T20:01:36Z  2026-06-14T20:01:35Z

clean is the happy path. Anything else is repo-tender telling you a repo needs your attention โ€” it won't touch it for you. ๐Ÿ”’

Schedule it & forget it ๐Ÿค–

Install a per-user launchd agent that runs sync every refresh_interval:

repo-tender daemon install
repo-tender daemon status

macOS now syncs your repos in the background. Tear it down anytime:

repo-tender daemon stop
repo-tender daemon uninstall

Tune it ๐ŸŽ›๏ธ

Find your config, then edit it (repo-tender config path):

base_dir: ~/src/evergreen   # where clones live (pick your own!)
refresh_interval: 90m       # "6h", "90m", "45s", "30d", or integer seconds
concurrency: 8              # max parallel git/gh operations per run
repo-tender config show   # see the effective, validated config

Output for robots ๐Ÿค“

repo-tender sync --json    # one JSON object per event line (12-factor!)
repo-tender sync --plain   # one plain line per event, no color
repo-tender status --json

--no-color and --quiet/-q work everywhere; color auto-disables off a TTY.

Command Overview ๐Ÿ”

Track repos & orgs:

  • repo add|remove|list REF โ€” manage individual repos (host/owner/name)
  • org add|remove|list NAME โ€” manage whole orgs (name or host/name)

Run & inspect:

  • sync [--repo REF] โ€” run one sync pass (optionally scoped to one repo)
  • status โ€” print the per-repo evergreen status table (reads state, no network)
  • config path|show โ€” show the config path, or the effective config

Schedule (launchd):

  • daemon install|uninstall โ€” write/remove the launchd agent
  • daemon start|stop|restart โ€” enable / disable / run-now
  • daemon status โ€” loaded? running? last exit?

Global flags: --plain ยท --json ยท --no-color ยท --quiet/-q ยท --help/-h ยท --version

How it works ๐Ÿ› ๏ธ

The bits worth knowing the why of:

  • ๐Ÿ”’ The cardinal rule: never lose your work. repo-tender only ever fast-forwards a clean repo that's strictly behind. A dirty tree, local commits the remote lacks, a detached HEAD, a non-default branch โ€” all reported and left untouched. There is no destructive path.
  • ๐Ÿค– A periodic launchd job, not a resident daemon. No socket, no IPC, no in-process scheduler. launchd wakes a short-lived sync every refresh_interval; it fans out, writes state, and exits. (StartInterval + RunAtLoad, no KeepAlive.)
  • ๐Ÿ“ก Local-first, network-last. Each sync checks on-disk facts first โ€” path present? on default branch? clean? .git/FETCH_HEAD younger than the interval? โ€” and only then touches the network. Re-runs are cheap and idempotent. โœจ
  • ๐Ÿ—‚๏ธ Config vs. state. config.yaml is your durable intent (hand-edited or via the CLI); state.yaml is machine-managed (statuses, fetch times, org expansions). Splitting them keeps machine rewrites away from the file you actually wrote.

Documentation ๐Ÿ“–

  • Full Reference ๐Ÿ“˜ โ€” every command, flag, config key, status value, file location, and exit code
  • Design (PRD) ๐Ÿ—๏ธ โ€” the full design & decisions
  • Builder context ๐Ÿค โ€” toolchain & conventions

Development ๐Ÿงช

Want to hack on it? Yay! ๐ŸŽ‰

bundle install
bundle exec rake test          # full minitest suite
bundle exec standardrb         # lint / format check
bundle exec standardrb --fix   # autofix

Contributing ๐Ÿ’

Bug reports and pull requests are welcome at github.com/jetpks/repo-tender! ๐ŸŒฒ

License ๐Ÿ“„

Available as open source under the terms of the MIT License.


Made with ๐Ÿ’– and a deep distrust of reset --hard, by Eric ๐ŸŒฒ