repoman — Vision Doc
Status: v0.1 design draft
Date: 2026-04-29
Origin: Bash prototype at ~/.local/bin/repoman (~200 LOC). This doc captures the design intent for a real productized rewrite in reef-lang.
Vision sentence
repoman is the bridge between a developer's local NVMe working copies, their per-project isolated Incus containers, and their homelab NFS/ZFS backup — one tool, opinionated conventions, both interactive and flag-driven.
Why this exists
The original problem: enable running Claude Code (or any agent / risky tool) with --dangerously-skip-permissions while limiting blast radius. Each project lives in its own Incus container; the host stays safe because the worst case is "lose a container, not the host."
That naturally turned into a workflow: per-project containers + bind-mounted repos + NFS backup + ZFS snapshots for history. Doing it with raw incus and rsync works but accretes ad-hoc shell scripts. repoman makes the workflow first-class.
Audience
Homelab developer running:
- Incus for containerization (Linux-only for v0.1).
- Local SSD/NVMe for active project working copies (latency matters).
- NFS-mounted remote storage for durable backup (ideally ZFS-backed, for snapshot history).
- One developer, multiple projects. Not multi-user, not multi-host coordination.
Realistic audience size: small. Niche tool serving its narrow audience well > generic tool nobody loves.
Differentiators (vs rolling your own scripts)
- Incus
projectfor ownership. All repoman-managed containers live in anincus project create repomannamespace — clean scoping, no leakage into user's other Incus work, easy to nuke the entire estate. - Single tool covering container lifecycle + project conventions + backup. Existing tools each cover one slice (
incusfor containers,restic/borgfor backup,distroboxfor dev shells); none do all three with conventions tuned for "code projects on NFS/ZFS homelab." - Opinionated backup story. rsync mirror + autofs-aware mount checks + ZFS snapshots assumed on the file server. No bespoke history layer — ZFS is the time machine.
- Both interactive and flag-driven. Same code path serves a guided wizard and a shell-scriptable CLI.
- Native single-binary distribution via reef compiling to native code. No runtime install for users.
v0.1 subcommands
Every subcommand has both an interactive form (when args missing) and a flag-driven form (when args provided). Identical underlying code path.
| Subcommand | Purpose |
|---|---|
repoman setup |
First-time host setup. Creates Incus project repoman, ensures profiles exist (claude-share or successor), validates LOCAL_REPOS and NFS_REPOS, writes initial registry. Idempotent. |
repoman new <name> [--repo <dir>] [--image <img>] |
Create a new managed container. Bind-mounts the repo, applies profiles, restarts. Interactive form prompts for unset values. |
repoman list |
List managed containers: name, state, IP, repo path, image, last sync. Reads from registry + reconciles against incus list --project repoman. |
repoman shell <name> |
Drop into the container as the local user, in the repo's directory. Replaces the verbose incus shell --user 1000 --cwd ... invocation. |
repoman sync [name] [--no-delete] [--dry-run] |
rsync mirror local → NFS, all projects or one. NFS auto-mount aware. ZFS snapshots on the file server provide history. |
repoman remove <name> [--keep-repo] |
Destroy the container; repo dir untouched by default. |
repoman status [name] |
Health check: container state, mounts, NFS reachability, last sync, drift between registry and Incus. |
Out of scope for v0.1:
- Other container runtimes (Docker, Podman) — Incus only.
- Other backup destinations (S3, restic, borg) — NFS only.
- macOS/Windows — Linux only.
- Multi-host coordination, replication.
- ZFS snapshot management (file-server-side; user runs sanoid themselves).
- Cron installation (provide a doc snippet, not a subcommand).
- IDE integration.
Configuration
Central registry: ~/.config/repoman/repoman.toml
Tracks every project repoman owns. Source of truth for "what should exist."
[defaults]
local_repos = "~/repos"
nfs_repos = "/nfs/repos"
incus_project = "repoman"
default_image = "images:ubuntu/26.04/cloud"
profiles = ["default", "claude-share"]
[[project]]
name = "isurus"
repo = "isurus-project" # relative to local_repos
image = "images:ubuntu/26.04/cloud"
created = "2026-04-28T15:00:00Z"
last_sync = "2026-04-28T20:17:03Z"
backup = true
Per-project override (optional): <repo>/.repoman.toml
Lets a repo declare its own runtime needs. Read on new, can be re-applied with repoman update <name>.
image = "images:debian/12/cloud"
profiles = ["default", "claude-share", "node-dev"]
[[mount]] # extra bind-mounts beyond the repo itself
source = "~/.npm"
path = "/home/ctusa/.npm"
[env]
NODE_ENV = "development"
Drift handling
Source of truth: repoman registry. Reality: Incus DB.
Every command opens with a reconciliation pass against incus list --project repoman:
- Container in registry but not in Incus → flag missing, offer to recreate or remove from registry.
- Container in Incus (in
repomanproject) but not in registry → flag orphan, offer to adopt or destroy. - Differences in state — note in
list/statusoutput.
Reconciliation is read-only by default; modifications need explicit --repair or interactive confirmation.
Implementation: reef-lang
Reef has the right shape for this:
- Native compilation → C → GCC/Clang → static binary. Single-file distribution.
- Stdlib coverage we'll use:
encoding.toml— config files (central registry + per-project)encoding.json— Incus REST API responsesnet.http(with bundled LibreSSL) — talk to Incus over unix-socket REST APInet.unix— raw unix-socket primitives (unix_connect/unix_send/unix_recv), fallback ifnet.httpdoesn't accept a unix-socket transportsys.args/sys.flag— CLI argument and flag parsingsys.env— env var reads (LOCAL_REPOS,NFS_REPOS, etc.)sys.process— argv-listprocess_spawn(program, [argv])for rsync/incus shell-outs (full POSIX surface incl. wait/kill/signal/setsid)sys.signal— graceful Ctrl-C handling during long syncsio.console— interactive prompts for the wizard modeio.file,io.dir,io.path,io.stream— local FS opsfs.stat,fs.perm— file metadata, permissions, uid/gid name resolutioncore.result,core.option— error handling primitives- Active Objects — natural fit for parallel operations (e.g.
repoman syncsyncing N projects concurrently, or healthchecks across containers)
- Multi-platform reef = future macOS/BSD/illumos support comes for free if/when needed.
Open implementation questions
- Subprocess execution. (Resolved — 2026-04-29)
reef-stdlib/sys/process.reefexports a comprehensive POSIX surface:process_spawn(program, argv: [string]),process_exec(program, argv),process_spawn_shell(cmd),process_fork(), plusprocess_wait/process_try_wait/process_wait_any[_nohang],process_kill/killpg,process_setsid/process_set/getpgid,umask,getpid/getppid, and post-wait introspection (process_exit_code,process_exited_normally,process_was_signaled,process_term_signal). Implementation rule for repoman: useprocess_spawn(program, argv)exclusively for rsync and incus invocations — neverprocess_spawn_shell, so user-derived names (container/repo/path) cannot cause shell injection. - Incus integration: REST vs CLI shell-out.
- REST (preferred long-term): unix socket
/var/lib/incus/unix.socket, JSON in/out, structured error codes. Cleaner. Reef has raw unix-socket I/O vianet.unix; whethernet.httpaccepts a unix-socket transport directly still needs verification — if not, build a minimal HTTP-over-unix on top ofnet.unix. - CLI shell-out (faster to bootstrap): parse
incus list --format json. Brittle but works today. - Recommendation: shell out for v0.1 (using
process_spawn, argv-list), migrate per-command to REST as we go.
- REST (preferred long-term): unix socket
- Interactive UX library.
io.consoleprovides line I/O. For richer prompts (default values, validation, menus), do we build a thin reef wrapper or keep it line-based? Recommendation: line-based prompts in v0.1; richer UX is v0.2. - Reef stability. Reef is at 0.5.9. Pre-1.0 means breaking changes possible. Recent reef work has been GC stability fixes (BUG-033, BUG-034 through 0.5.7) — encouraging signal. Building repoman in reef serves as a real-world stress test that helps the language. Coordinate with reef releases for stability windows.
- Project structure inside reef. Module layout, build setup, test framework — needs a separate "scaffolding" task before feature work.
Migration from bash prototype
The current bash ~/.local/bin/repoman covers create and sync. The reef rewrite should:
- Match feature parity with bash version on day 1 (
new≈create, plussync). - Add
setup,list,shell,remove,statusas the actual productization. - Introduce the Incus
projectnamespace (the bash version doesn't use it — all containers live in the default project). - Keep config-by-env-var as a fallback for the central registry's
[defaults]section, so existing automation isn't broken.
The bash script stays useful as a simpler tool while the rewrite matures. Plan for a clean cut-over once the reef version is stable, not a parallel maintenance burden.
Naming convention (post-fix)
In bash v1, the create command had implicit -project suffix logic that conflated container name with repo name. v0.1 reef contract:
- Container name =
<name>(positional, required). - Repo dirname =
<name>by default, overridable with--repo <dirname>. - Repo absolute path =
<local_repos>/<repo-dirname>. - No magic suffix logic. Explicit > implicit.
Differentiation reality check
Why someone would pick repoman over their own scripts:
- They want the Incus-project-as-namespace pattern but don't want to wire it themselves.
- They want guided setup — first-time onboarding into "this style of homelab dev workflow."
- They want a unified registry of "what projects do I have, where are they, when did they last back up."
Why they wouldn't:
- They already have ansible/nix/etc. covering this.
- They use Docker, not Incus.
- They don't separate local working copies from backup destinations.
That's fine. The audience is small and that's the intentional design.
Success criteria for v0.1
- Builds clean from a fresh reef-lang install on Linux x86_64.
-
repoman setupruns idempotently and produces a working baseline on a fresh host. -
repoman newcreates a container that matches the bash prototype's behavior, then surpasses it (uses Incus project, persists in registry, supports per-project config). -
repoman syncmatches bash prototype's behavior with autofs check + mirror semantics. - All subcommands have both interactive and flag-driven invocation paths.
- Single static binary distributable via tarball or a per-distro package.
- README + man page + at least one end-to-end example walkthrough.
- License chosen (MIT or Apache-2.0; user's call).
- Public repo on a forge with a clear contribution guide.
Open product questions
- License. MIT? Apache-2.0? Let user decide.
- Forge. GitHub, Codeberg, self-hosted? Distribution implications (binaries, releases).
- Versioning. Semver. v0.x while pre-1.0 (parallel with reef's own version).
- Configuration migration. When
repoman.tomlschema changes between versions, how do we migrate?repoman config migrate? - Logging / observability. Just stderr for now, or structured (JSON lines) output mode for automation?
Handover note
This doc is the contract. The bash prototype at ~/.local/bin/repoman is the behavioral spec — it's running in production on this host (cron-synced nightly, shipped containers in use). Read its source to understand exactly what new/sync do today before reimplementing in reef.
The next session should start by:
- Reading this doc and the bash prototype together.
- Auditing reef-lang's stdlib for the gaps called out (notably subprocess exec).
- Sketching the reef project skeleton (module layout, Makefile/build, first hello-world
repoman --version). - Porting
syncfirst (simpler, less surface area) beforenew(more moving parts).