Skip to main content

Module registry

Module registry 

Source
Expand description

Durable, system-wide record of which teams are up and where they live.

teamctl’s other notions of identity are all keyed on name, never on path: the session UUID, the tmux session name, the mailbox row key, the applied.json key. So nothing could durably answer “what teams are running across this machine, and in which directories?” — the question behind system-wide teamctl ps, orphan reaping, and the same-name guard.

This registry is that durable record. It lives at ~/.config/teamctl/teams.json (the same config dir the context store uses) and is keyed on the compound (project_id, root) — one entry per project per absolute team root, so two installs of the same template in different folders are distinct rows rather than a collision.

up upserts this team’s entries (see crates/teamctl/src/cmd/up.rs), down clears them (down.rs). Writes are atomic (temp + rename) so a reader never observes a torn file. Atomicity is per-write, not a cross-process lock: two ups of different teams racing the load-modify-save could still drop one update — acceptable for a self-healing side store (the next up re-records it), and called out here rather than over-claimed.

The store records the path alongside the name; it deliberately does NOT make the session UUID or tmux name itself path-aware (a larger identity change, out of scope).

Structs§

Registry
The whole store. #[serde(default)] per field keeps old files forward-compatible as the shape grows, matching ContextStore.
RosterEntry
One recorded (project, agent) running under a root, with its tmux session name reconstructed from the prefix the team was brought up with. The unit down/reload orphan-reaping operates on.
TeamEntry
One running team: a single (project_id, root) pair plus the metadata ps / reaping / the same-name guard need. Field names serialize verbatim (snake_case), matching every other persisted teamctl store.

Functions§

clear
Clear entries for root. project = None drops every entry at that root (a whole-team down); project = Some(id) drops only that project’s entry (a project-scoped down on a multi-project root, so sibling projects stay registered). A no-op write is skipped.
config_dir
~/.config/teamctl — derived from $HOME ($USERPROFILE fallback for Windows). None when neither is set, so callers warn-and-skip rather than guess a path. Mirrors the CLI context store’s resolver and session::claude_home; deliberately NOT dirs::config_dir(), which on macOS resolves to ~/Library/Application Support and would break the literal ~/.config/teamctl path the rest of the CLI uses.
is_orphan
true when this entry’s recorded root no longer holds a team-compose.yaml — a stale row whose team config was moved or deleted. up always records the .team directory as root, so the <root>/team-compose.yaml check is the live one for registry entries; the <root>/.team/team-compose.yaml fallback is kept only for parity with the shared sessions::root_has_compose rule (which also accepts a root that points at the bare project dir). path_exists is injected so the classifier is pure and unit-testable; production passes |p| p.exists().
load
Load the registry from dir/teams.json. A missing file is an empty registry; a corrupt one degrades to empty rather than erroring (the next up overwrites it) — the same tolerance ContextStore::load and snapshot::read apply. Only a genuine read error (e.g. permissions) is propagated. dir is the config dir (see config_dir); it is taken as a parameter so tests can point at a tempdir without touching $HOME.
now_rfc3339
RFC3339 (seconds, Z) — the serialized-timestamp idiom shared with snapshot::applied_at / attachments. Co-located here because this module owns the started_at field’s format contract.
orphans_for_root
Load the registry at dir and return the orphan roster for root — recorded agents no longer in desired. The down/reload reap entry point: composes load + Registry::roster_for_root + reap_targets so the whole on-disk → orphans chain is one tested call. dir is taken as a parameter so tests point at a tempdir without touching $HOME. A missing store loads as empty (no orphans); only a genuine read error propagates.
reap_targets
The reap set: recorded roster agents that are no longer desired. desired is the current compose’s agent ids (<project>:<agent>). scoped limits the reap to one project (a --project down/reload); per_agent (a --agent selector active) disables reaping entirely — a partial teardown leaves the rest of the team registered and running, so nothing there is an orphan. Pure; mirrors down::registry_clear_scope’s scope contract so reaping is never broader than the invocation.
same_name_other_root
If another team is registered with the same project_id but a DIFFERENT root that still holds a compose (not an orphan), return that root — the same-name-different-folder collision up must refuse. It’s the cluster’s root cause: identity is name-keyed, not path-keyed, so two such teams alias each other’s sessions, tmux names, and mailbox keys. The team’s own root is never a conflict (a re-up of the same (project_id, root) is fine). path_exists is injected so the liveness check is pure and unit-testable; production passes |p| p.exists().
upsert
Upsert one team entry. Convenience over upsert_many.
upsert_many
Upsert a batch in a single load-modify-save (one atomic write for the whole up). Each entry replaces any existing row with the same (project_id, root) key, preserving that row’s original started_at; new keys are appended. Entries stay sorted by (root, project_id) for stable on-disk output.