Sprint E, redesigned from the original per-repo-metadata plan: the deploy
host already mirrors workspace_root 1:1 (documented convention), so instead
of maintaining a repo->host/compose-path mapping by hand, vulp refresh runs
one batched remote command (via a configurable ssh wrapper) listing every
mirrored repo's HEAD sha and diffs it against local HEAD.
- src/deploy.rs: DeployState (InSync/Behind(n)/Diverged), scan_deploy_heads
(one remote round-trip, retried once for this VPS's known SSH flakiness),
drift_for (path relative to workspace_root -> host lookup -> local git
rev-list --count)
- config: deploy_ssh_cmd + deploy_root, both empty by default (feature is
fully inert, no SSH attempted, until configured)
- cache: RepoMeta.deploy_status, folded into the existing refresh loop
(TUI [R] and CLI) alongside CI/dependabot rather than a second pass
- table: DEPLOY column (checkmark/behind-count/diverged/dash); CLI
list --stale-deploy and deploy_status in --json
- verified against the real VPS: 37/49 repos matched to a mirror (30 in
sync, 7 behind 1-4 commits - a real signal), everything else correctly
shows unset (no mirror there, e.g. vulp itself, a pure OSS dev tool)
- set deploy_ssh_cmd/deploy_root in the live config so this is active now,
not just theoretically configurable
The hub hands its live terminal to detail/project_detail on open (already
flash-free going in), but both sub-views unconditionally tore the terminal
down on exit; the hub's next loop iteration then always called tui::setup()
again, producing a real LeaveAlternateScreen -> (plain terminal visible for
one frame) -> EnterAlternateScreen blink on every trip back.
- detail::run / project_detail::run now return the live terminal on a clean
exit instead of tearing it down (only torn down if the inner loop itself
errors); hub::tui_phase accepts an already-live terminal and skips its own
setup() when given one, threading it across loop iterations
- project_detail's own nested repo-detail call (Enter on its Repos tab) had
the same issue one level down, fixed the same way via Option<Tty> so the
terminal can be taken out for the nested call and put back
- verified via raw pty byte counts: two full hub<->detail round trips now
show zero EnterAlternateScreen/LeaveAlternateScreen pairs in between,
vs. one pair per trip before
- src/stats/: OwnEmailMatcher (config emails + GH no-reply per account) +
git-side --author filtering (cheap even on the pterodactyl fork's 4700+
commits); streak (current/longest, midnight-grace rule), 52-week heatmap,
day/hour histograms — one scan feeds all of them, data layer only for now
(visual Stats-tab redesign stays in Sprint D's mockup-first cycle)
- src/stats/cadence.rs: release cadence (median tag gap, local tags, Tier A)
- src/stats/size.rs + cache.rs: git-object vs working-tree size, persisted
in new ~/.vulp/stats_cache.toml (max 1 sample/day), recorded only during
[R]/vulp refresh — confirms vulp's own filter-repo cleanup (539MB->834KB)
- src/digest.rs: WeeklyDigest struct + 3 renderers (Discord embed/markdown
archive/plaintext); vulp digest [send discord|webhook <url>] CLI, webhook
stored via existing auth token store (~/.vulp/.tokens, 0600), never logged;
send respects --dry-run
- TUI: [d] on Stats previews the digest in a Modal::Info before anything is
ever sent
- fix: list_contributors used --all (double-counts after fetching remotes),
now --branches
- config: stats_author_emails field (TUI-editable)
- verified: streak/delta/top-repo/release math against a hand-built commit
history, webhook validation+permissions, dry-run, real refresh (51 repos,
vulp's own entry confirms the filter-repo win), TUI preview via pyte
- Provider::org_dependabot_counts: one org-level request per distinct owner
(cursor pagination via Link header — page= returns 400 on this endpoint);
403/404 → None (unknown) vs Some(0) = checked, none open
- Provider::latest_ci_status: latest Actions run on the repo's branch →
success/failure/running; CiStatus enum with stable string form
- cache RepoMeta: ci_status + dependabot_open replace unused ci_ok;
RepoSummary carries both, applied at startup/scan/refresh (TUI + CLI)
- Repos table: CI column (✓/✗/●) + ALERT column (count, red when >0,
'—' = unknown); CLI: list --alerts filter, scan_repos now cache-enriched
- verified against live API: 49 repos, 44 with alert counts, CI statuses
match reality; table rendering checked via pyte
- global --dry-run: sync/release/init/collab/license print 'DRY RUN: would...'
and exit 0 (no auth needed for previews where possible)
- config protected_repos (name/basename, case-insensitive): mutating commands
exit 1 on listed repos unless --force; checked before dry-run
- VULP_AGENT env: activity::log centrally appends '· via agent' to detail,
covering both CLI and TUI writers
- sync: unknown --action now rejected before touching any repo