Two Repos-tab quality-of-life changes:
- External/vendored clones (repos with zero of your own commits — e.g. an
upstream cloned for reference and never pushed to) are measured on
refresh, cached in stats_cache (RepoStatsEntry.authored), shown with a
dim ⧉ marker prefixed to the name, and hidden by the persistent
hide_external_clones config toggle (key `x`, the `S` menu, or
`vulp list --external|--no-external`). A fork you do commit to stays
yours, so only true reference clones are flagged.
- Inside the `/` filter box, ↑↓ now move through the live-filtered
results and Enter opens the highlighted repo/project directly, instead
of the filter only "locking" on Enter with no way to scroll or select.
OwnEmailMatcher::author_regex() emits an extended-regex alternation
(<email>|<noreply-address>), but scan_repo_own_commits ran `git log
--author=` without -E. git defaults --author to *basic* regex, where |
is a literal, so the pattern matched nothing for anyone with two or more
identities — silently zeroing streak, heatmap, digest and the hour/day
patterns (measured 0 -> 141 own commits across the workspace after the
fix). Add -E here and in the new has_own_commit helper.
Implements the approved mockup from the UX debate report:
- new DisplayRow::TodoHeader variant: collapsible group headers, toggled
by Enter/Space, tracked in HubState.todo_group_collapsed
- [g] cycles grouping: status (default) -> project -> repo -> none
- [s] cycles which statuses are included before grouping: active
(open+in-progress, default) -> open -> done -> all
- table: dropped SRC/EST (near-always empty), code-scan todos show a dim
'no status icon' marker instead of a separate source column, project
name colored by the project's own color (was DarkGray, wasting the most
useful visual identifier in the tool), age now tracks updated_at not
created_at ('what's moving' beats 'what's old')
- right panel: added Repo/Difficulty/Estimate, previously only visible in
the table despite the panel having more room for them
- block title shows live counts + current view/group/code-scan state
Simplified vs. the literal ASCII mockup: [s] filters the underlying set
before grouping, so e.g. the default 'active' view omits Done/Cancelled
groups entirely rather than always showing them collapsed - a simpler,
more internally consistent model. Flagged for foksiu to request the exact
mockup behavior if he wants it.
Verified via pyte: group/status-view cycling, collapse/expand (arrow flips
and child rows appear/disappear), right panel on header vs. todo row.
Every place building an owner/repo slug for the provider API (refresh,
release, add_collaborator) used path.file_name() as the repo segment - the
local checkout's directory name. That's usually the same as the GitHub repo
name, but not always: vulp's own folder is 'vulp', its GitHub repo is 'fxg',
so every refresh silently 404'd fetching its own metadata/PRs/issues/CI (the
cache carried an empty meta block with fetched_at: 0, never actually set).
RepoSummary now also carries remote_repo_name, parsed from the git remote
URL alongside owner (same source, same parsing approach) - the segment the
provider actually knows the repo by. All four slug-building call sites now
prefer it, falling back to the folder name only when there's no remote to
parse it from. Verified: 'vulp refresh' now correctly hits dualfroz/fxg and
the cache shows real stars/issues/PRs for vulp instead of the empty stub.
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