Commit Graph
32 Commits
Author SHA1 Message Date
dualfroz c38d9aea99 docs+fix: document auth subcommands; enforce --no-external/--external
docs/cli.md called itself the 'complete non-interactive surface' but
never listed vulp auth add/remove/switch/test (only import) — real,
script-usable subcommands with zero documentation. Also documented
the missing --allow-secrets flag on the sync synopsis line. Separately,
'list --no-external --external' together silently returned an
always-empty result (two independent retain passes) instead of an
error; now rejected as a usage error (exit 2), matching what the
synopsis's '|' already implied.
2026-07-23 07:25:45 +02:00
dualfroz 7bba2c7d0b fix: help-screen drift, dead Shift+D binding, byte/char title truncation
'?' help never mentioned the M (Profiles & Sync) menu at all, and
documented 'd'/'p'/'c' only for their generic meaning while Stats/
Activity tabs silently overload the same keys for something else —
same class of bug the earlier B4 fix addressed, just new drift since.
Also: 'D' with an explicit SHIFT-modifier guard never matched, since
terminals deliver Shift+d as an already-uppercase char with no
modifier flag (same root cause as the earlier Ctrl+A fix) — merged
into the identical plain-Delete arm instead of leaving it dead code.
Also: PR/issue title continuation used title.len() (bytes) to decide
whether to render a second line, while the actual text used
.chars() — a non-ASCII title could get a spurious blank line.
2026-07-23 07:25:37 +02:00
dualfroz 1b6a94dce6 fix: close TOCTOU window on tokens file permissions; byte-safe preview
file_write_token() wrote plaintext via fs::write (default-mode,
typically world-readable 0644) and only chmod'd to 0600 afterward —
every store/rotate had a real window where another local user could
read the token. Now opens with mode 0600 set atomically at creation
on Unix. Also fixed 'auth test' slicing a token's preview by byte
index instead of char boundary, which would panic on non-ASCII input.
2026-07-23 07:25:29 +02:00
dualfroz e6f5116827 fix: expired/revoked GitHub tokens weren't recognized as auth failures
A dead PAT returns GitHub's actual 401 body ('Bad credentials'), which
matched none of the auth_fail substrings — it fell into the 'API
error, but authenticated' branch and never prompted the user to
re-run vulp auth import. Added 401/'bad credentials' to the check.
2026-07-23 07:25:22 +02:00
dualfroz b9fa29f656 fix: editor commands configured with flags never launched
Command::new(&cfg.editor) treated the whole configured string as one
binary name, so an editor set to e.g. 'code --wait' or 'vim -R' — a
common config style — failed to spawn at all every single time. Added
a shared launch_editor() that splits on whitespace (program + args)
first, used by every one of the 6 call sites across detail/ and hub/.
2026-07-23 07:25:14 +02:00
dualfroz edf4bd34bf fix: clipboard auto-detect had no macOS/Windows fallback, failed silently
copy_auto only ever tried wl-copy/xclip, so on macOS or Windows it
always returned false with no clipboard tool attempted. Added
pbcopy/clip fallbacks per-OS. Also, the detail-pane 'y'/'u' handlers
showed a status message only on success — a failed copy (no tool
found) looked like nothing happened at all; both now report the
outcome either way.
2026-07-23 07:25:08 +02:00
dualfroz cd4e8e92ea feat: respect $VULP_REPO env override for update-check; add CI workflow
The compiled binary's update-check tile hardcoded dualfroz/fxg with no
way to override it, unlike the install scripts which already honor
$VULP_REPO — a fork or a future repo rename needed a rebuild just to
fix the tile. Also add .github/workflows/ci.yml (build + clippy -D
warnings + test on push/PR): the only existing workflow was the
tag-triggered release build, so nothing gated PRs before merge.
2026-07-23 07:24:58 +02:00
dualfroz 2944d4df05 fix: GitHub list_issues built the wrong URL for org-owned repos
Used format!("repos/{owner}/{repo}/issues", ...) instead of the
slug() helper list_pull_requests already uses, so an org-owned repo
(passed as 'Org/repo') produced repos/{personal_owner}/Org/repo/issues
— a 404 the code silently swallows into an empty list, making the
Issues tab look like 'no issues' instead of a broken request.
2026-07-23 07:24:51 +02:00
dualfroz 0aed003b32 fix: GitLab repo_id mishandled group/subgroup-owned projects
repo_id() always prefixed self.namespace onto the given repo name, so
any repo already passed as a full group/repo or group/subgroup/repo
path got mangled into namespace/group/repo and 404'd on every
settings-tab mutation. Now mirrors GitHubProvider::slug: an
already-qualified path (contains '/') is used verbatim.
2026-07-23 07:24:44 +02:00
dualfroz f5fc22eae7 fix: guard shared background-job channels against concurrent overwrite
Bulk-import, import-directory, remove-git-from-all, create-repo,
publish-existing, create-PR, create-issue, profile-push, and
profile-pull all set state.pull_all_rx/create_repo_rx/profile_pull_rx
unconditionally. Starting a second one of these before the first
finished silently dropped the first job's progress/result (its sends
now went to an orphaned receiver), and for the workspace-mutating jobs
(remove-git-all racing an in-flight clone) could corrupt repos on disk.
Each spawn site now checks the channel is free first and tells the
user to wait instead.
2026-07-23 07:24:37 +02:00
dualfroz a87db8433c fix: guard label-rename against a stale repo index
A background refresh (bulk-import/create-repo/profile-pull all replace
state.repos wholesale) could complete while the 'Set label' modal was
still open, so its captured index either panicked (out-of-bounds) or
silently relabeled the wrong repo. Now degrades gracefully via .get().
2026-07-23 07:24:27 +02:00
dualfroz 1000359393 feat: add your contribution graph to the Stats tab
The Stats tab counted every commit on HEAD (mostly upstream history in a
workspace full of forks), so "your activity" was fictional. It now leads
with an author-filtered panel: current/longest day streak, a GitHub-style
52-week contribution heatmap, and an hour-of-day histogram — computed
from your identities via the same scan the weekly digest uses, in the
existing background stats thread. The all-commits period bar chart moves
below it; languages/top-repos/day-of-week stay on the right.
2026-07-11 22:32:32 +02:00
dualfroz 4723d18c59 feat: make the Activity tab navigable and easier to read
Activity rows are now a cursor list: ↑↓ (and the scroll wheel) move the
highlight, Enter jumps to that repo and opens its detail. Each row shows
a precise local timestamp (MM-DD HH:MM) instead of only a relative age,
colour-codes the kind, colours the repo by its project and widens that
column so long workspace paths are readable, and keeps the relative age
as a dim suffix on the detail. A › marks rows whose repo is still in the
workspace (Enter-able).
2026-07-11 22:25:55 +02:00
dualfroz abb1e92064 feat: hide PRs you can neither act on nor learn from
Overview and the PRs tab listed open PRs from every repo, including ones
cloned for reference (e.g. pterodactyl) whose PRs you can't merge and
didn't author — pure noise. A PR is now surfaced only when you authored
it but can't merge it (your contribution elsewhere) or someone else
authored it and you can (awaiting your review); your own PR in a repo
you control, and strangers' PRs you can't touch, are filtered out.

Providers report viewer merge permission (RepoMeta.viewer_can_merge,
from GitHub/Gitea repo permissions and GitLab access level), stamped
onto each PR at refresh. The cache keeps every PR; the filter is applied
where the hub builds state.prs, so the per-repo detail view is unchanged.
2026-07-11 22:19:59 +02:00
dualfroz 4a3fa58745 feat: show active-todo count in the tab bar and declutter the Todos title
The Todos tab now carries a count badge (4 Todos(N), N = open + in
progress) like PRs/Issues do. The panel title drops the [s]/[g]/[c]
keybind soup that read as clutter; the current view/group/code-scan
state and the keys that change it move to the bottom hint bar, where
every other keybinding already lives.
2026-07-11 22:19:59 +02:00
dualfroz 3426a28c48 feat: show local branch count in the Repos table
A repo that keeps several branches (e.g. one theme per branch) only ever
showed its checked-out branch, hiding that the others exist. The Repos
table now renders the active branch plus `+N` for the other local
branches (cyan when >1), the BRANCH column widened to fit, and
`vulp status` prints "(+N more local)". The full list stays in the
detail Branches tab; RepoSummary.branch_count is in --json too.
2026-07-08 16:21:22 +02:00
dualfroz 212c55d138 feat: flag vendored clones and make the repo filter navigable
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.
2026-07-08 16:15:02 +02:00
dualfroz 2ccf69f64c fix: pass -E to git so the multi-identity author filter matches
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.
2026-07-08 16:14:43 +02:00
dualfroz 6adc22d665 feat: redesign Todos tab — grouping, collapsible headers, cleaned columns
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.
2026-07-07 19:17:28 +02:00
dualfroz 3eb922559c fix: provider API calls used the local folder name, not the actual repo name
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.
2026-07-07 19:00:08 +02:00
dualfroz 4b0a6e324f feat: deploy drift via auto-discovered VPS mirror, no manual metadata
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
2026-07-07 18:55:13 +02:00
dualfroz a3658d5e97 fix: eliminate alt-screen flash when returning from repo/project detail
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
2026-07-07 17:58:02 +02:00
dualfroz 14ef7fb06c Sprint C: author-filtered stats scan, release cadence + repo size, weekly digest
- 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
2026-07-06 16:47:19 +02:00
dualfroz d99660420f Sprint B.9+B.10: Dependabot alerts + CI status in table, cache and CLI
- 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
2026-07-06 16:02:49 +02:00
dualfroz 1e74de5e2e Sprint B.8: vulp brief [repo] --json — one-shot state for agents
- workspace brief: summary counts (incl. missing_license), needs-attention
  repos, open/in-progress todos, cached PR/issue lists
- repo brief: full RepoSummary + 10 recent commits + repo todos + cached
  PRs/issues; name matches display label or basename
- read-only, cache-sourced (vulp refresh for freshness); human + --json
2026-07-06 15:50:52 +02:00
dualfroz c0b282335d Sprint B.7: secret scan before commit-push (TUI + CLI)
- src/secrets.rs: scans added lines of the staged diff for GitHub/GitLab
  token shapes, AWS access keys, Discord token assignments, private key
  blocks; prefix+tail-length matching (no regex dep), matched values never
  echoed
- a hit blocks the commit and unstages (git reset); CLI override
  --allow-secrets; TUI sync flow shares the same scan
- verified: block+unstage, override commits, prose mentions don't trigger
2026-07-06 15:47:32 +02:00
dualfroz 8119cdeba7 Sprint B.6: CLI guardrails — --dry-run, protected_repos, VULP_AGENT marker
- 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
2026-07-06 15:43:14 +02:00
dualfroz f1432d1cd3 Sprint B.5: add-LICENSE action (MIT/Apache-2.0) in [m] menu + vulp repo license
- src/license/: embedded templates, holder resolution (config > git user.name
  > account username), write path shared by TUI and CLI
- TUI: bulk-aware [m] entry over action_targets(), kind select + confirm,
  point-updates has_license/is_dirty, logs to activity (new License kind)
- CLI: vulp repo license <repo>...|--all [--kind] [--holder] [--json],
  skips already-licensed repos, exit 2/4 contract kept
- config: license_holder + default_license fields (TUI-editable)
- never overwrites an existing license, never auto-commits
2026-07-06 15:37:25 +02:00
dualfroz 0468c032c6 Phases 0-1 + Sprint A: module split, CLI layer, org-refresh fix, hygiene
- LICENSE (MIT) + README truth-pass; docs/cli.md CLI contract
- CLI subcommands (src/cli_cmds/) per "TUI for users, CLI for users+AI"
- Hub/detail refactor: keymap dispatch, modal split, action enum, BgMsg bus
- Fix: org repos were never refreshed (owner==username match); refresh now
  parallel — 49 repos in ~11s, surfaced 70 previously invisible PRs
- Fix: 3 date bugs in detail/git.rs (format-local, Local::now, 1y month math)
- Stash indicator + [m] actions, session persistence, gh CLI token fallback
- Legacy UI modules removed; plan/ docs (roadmap, audit, debate, handoff)
2026-07-06 14:42:52 +02:00
dualfroz 5787b14880 update 2026-06-19 01:10:19 +02:00
dualfroz d4fdc9890a Test message 123! 2026-06-14 00:22:54 +02:00
dualfroz 4cf726709f init 2026-06-10 12:41:14 +02:00