Files
vulp/docs/cli.md
T
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

9.6 KiB

vulp CLI — reference

Complete non-interactive surface. Written for machine consumers (AI agents, scripts) as much as humans: every read command supports --json, nothing ever prompts, and exit codes are meaningful.

vulp                    # no args → interactive TUI (humans)
vulp <command> [--json] # non-interactive (scripts/AI)

Contract

  • --json (global flag): machine-readable JSON on stdout. Available on: list, status, scan, brief, stats, activity, todo list, pr list, issue list, project list, accounts, refresh, repo license.
  • No prompts, ever. Missing input = error + non-zero exit.
  • stdout = results only. stderr = progress and errors.
  • Exit codes: 0 ok · 1 execution error · 2 bad usage · 3 auth missing/invalid · 4 not found (repo/todo/project).
  • Repo arguments match the display label or the directory basename, case-insensitively (e.g. vulp status vulp matches "Vulp - Open Source!").
  • Mutating commands write to the same ~/.vulp/activity.log as the TUI, so CLI actions appear in the TUI's Overview feed.

Guardrails (for AI agents especially)

  • --dry-run (global flag): mutating commands (sync, release, init, repo collab, repo license) print DRY RUN: would ... lines and exit 0 without changing anything. Read commands ignore it.
  • protected_repos (config, comma-separated in vulp config): mutating commands refuse to touch listed repos (exit 1) unless --force is passed. Matching is by display label or dir basename, case-insensitive. The protection check runs before --dry-run, so a dry run also reveals it.
  • Secret scan: sync --action commit-push refuses to commit staged changes that look like credentials (see Git operations); override with --allow-secrets.
  • Discord webhook secret: vulp digest webhook <url> stores the URL in ~/.vulp/.tokens (mode 0600) the same way provider tokens are stored — never in config.toml, never printed back, never logged. Whoever has the URL can post to (and edit/delete messages in) that channel.
  • VULP_AGENT (env var, any non-empty value): every activity-log entry written while it's set gets a · via agent marker in its detail, so human and agent actions are distinguishable in vulp activity and the TUI feed. AI agents driving vulp should always set it.

Workspace (local, no network)

vulp scan                                # repo count + dirty/behind/ahead summary
vulp brief [repo]                        # one-shot picture: state + commits + todos + PR/issues
vulp list [--dirty] [--behind] [--ahead] [--lang ts] [--project NAME] [--alerts] [--stale-deploy] [--no-external | --external]
vulp status <repo>                       # branch, dirty, ahead/behind, last commit...
vulp stats [--period 1d|7d|30d|90d|1y|all]   # commits per repo in period
vulp activity [--limit N] [--kind push|pull|release|"new repo"|todo|pr|issue|commit|license]

vulp list --json returns an array of repo objects:

[{ "name": "...", "path": "...", "branch": "main", "ahead": 0, "behind": 0,
   "is_dirty": true, "last_commit_msg": "...", "last_commit_age": "2h ago",
   "last_commit_ts": 1780000000, "last_tag": null, "visibility": null,
   "owner": "dualfroz", "has_license": false, "lang": "TypeScript 96%",
   "bookmarked": false, "stars": null, "open_issues": null, "open_prs": null,
   "stash_count": 0, "ci_status": "success", "dependabot_open": 0,
   "deploy_status": "behind:3" }]

stars/open_issues/open_prs/ci_status/dependabot_open/deploy_status come from the on-disk cache (run vulp refresh first). ci_status is "success" | "failure" | "running" | null; dependabot_open: null means the alert endpoint isn't available for that repo's owner (personal account, missing token scope, or feature disabled) as opposed to 0 = "checked, none open". --alerts filters to repos with dependabot_open > 0.

deploy_status is "in_sync" | "behind:N" | "diverged" | null — see Deploy drift below. --stale-deploy filters to repos that are behind or diverged (excludes in_sync and null/not-applicable).

--no-external hides vendored/reference clones (repos you have zero of your own commits in — e.g. an upstream pterodactyl you cloned but never pushed to); --external shows only those. Classification is measured on vulp refresh (a git-side --author scan of your identities, cached in stats_cache.toml); before the first refresh nothing is classified and both flags match nothing. A fork you do commit to (even a handful of commits) counts as yours and is not external. In the TUI this is the x key / the ⧉ marker, and the persistent hide_external_clones config toggle.

Deploy drift

Auto-discovered, not manually configured: if deploy_root (config) mirrors workspace_root 1:1 on a remote host (e.g. /opt/projects/X/Y on the host ↔ {workspace_root}/X/Y locally), and deploy_ssh_cmd (config) is a command that runs its final argument as a shell command on that host and prints the result (a wrapper around ssh), vulp refresh runs one batched remote command listing every mirrored repo's HEAD sha, then compares each against local HEAD. A repo with no mirror on the host (nothing to compare against — e.g. a pure open-source component that never runs live anywhere) simply has deploy_status: null, not an error.

# ~/.vulp/config.toml
deploy_ssh_cmd = "python3 /path/to/vps_exec.py"   # appends the remote command as its last arg
deploy_root    = "/opt/projects"

Both empty (the default) disables the feature entirely — no SSH attempted.

Git operations

vulp sync <repo>... [--action pull|push|commit-push] [-m MSG]
vulp sync --all --action pull
vulp sync --project NAME --action push
vulp release <repo> --tag v1.2.0 [--notes "..."]   # git tag + push + provider release

commit-push requires -m/--message. sync exits 1 if any target failed (per-repo progress on stderr).

Before every commit-push commit, the staged diff is scanned for secret shapes (GitHub/GitLab tokens, AWS access keys, Discord token assignments, private key blocks). A hit blocks that repo's commit and unstages the changes; pass --allow-secrets to override. The same scan guards the TUI sync flow. Matched values are never echoed back.

Todos

vulp todo list [--status open|in-progress|done|cancelled] [--json]
vulp todo add "Title" [--desc "..."] [--project NAME]
vulp todo start|done|cancel|rm <id>

Projects (repo groupings)

vulp project list [--json]
vulp project create NAME [--color "#e74c3c"]
vulp project assign NAME <repo>...
vulp project rm NAME

Provider API (needs vulp auth import first)

vulp pr list [--repo R] [--json]        # cached — run `vulp refresh` for fresh
vulp pr create --repo R --title T --head BRANCH [--base main]
vulp issue list [--repo R] [--json]
vulp issue create --repo R --title T [--body "..."]
vulp refresh                            # re-fetch meta + PRs/issues + CI status + Dependabot alerts
vulp init NAME [--private] [--description "..."]   # local init + create remote + push
vulp accounts [--json]

pr/issue list read the on-disk cache (~/.vulp/) — the same one the TUI uses; run vulp refresh first when freshness matters.

Per-repo utilities

vulp repo url <repo>                    # print origin URL
vulp repo path <repo>                   # print absolute path
vulp repo open <repo>                   # open provider page in browser
vulp repo label <repo> "Display Name"   # empty name clears the label
vulp repo bookmark <repo>               # toggle
vulp repo pin <repo>                    # toggle
vulp repo collab <repo> <user> [--role pull|push|admin]
vulp repo license <repo>... [--kind mit|apache-2.0] [--holder NAME]
vulp repo license --all                 # every repo missing a license

repo license writes a LICENSE file into each target repo. It never overwrites an existing license (reported as skipped) and never commits — review and commit the file yourself. Defaults come from config: default_license (mit) and license_holder (falls back to git config user.name, then the account username).

Weekly digest

vulp digest [--json]                    # build + print this week's digest, archive to ~/.vulp/digests/
vulp digest send discord                # also POST it to the saved Discord webhook
vulp digest webhook <url>               # store/replace the webhook URL (secret — see Guardrails)

The digest counts only your own commits — filtered by stats_author_emails (config; falls back to git config user.email) plus each configured account's GitHub no-reply address — not every commit in the workspace (most repos here are 90%+ upstream/fork history). It reports: own commits this week vs last week, active days, current streak, top 3 repos by own commits, local tags created this week, PR/issue open+merged/closed counts (provider search API, needs an account), and todos added/done. Every build is archived to ~/.vulp/digests/<ISO-week>.md regardless of send. digest send discord respects global --dry-run.

Recipes for AI agents

# What needs attention right now? (state + todos + cached PRs/issues in one shot)
vulp brief --json

# Everything about one repo before working on it
vulp brief client-panel --json

# Only the dirty repos
vulp list --dirty --json

# Commit and push everything in one project
vulp sync --project client-panel --action commit-push -m "chore: batch update"

# Fresh cross-repo picture (meta + PRs + issues), then read it
vulp refresh --json && vulp pr list --json

# Weekly activity summary
vulp stats --period 7d --json