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.
234 lines
10 KiB
Markdown
234 lines
10 KiB
Markdown
# 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:
|
|
```json
|
|
[{ "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.
|
|
|
|
```toml
|
|
# ~/.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] [--allow-secrets]
|
|
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.
|
|
|
|
## Auth (non-interactive account management)
|
|
|
|
```
|
|
vulp auth import # import from `gh` CLI, no token needed
|
|
vulp auth add --provider github --token ghp_xxx [--alias work] [--url https://...]
|
|
vulp auth remove <account> # <account> = provider/alias, e.g. github/work
|
|
vulp auth switch <account> # change which account is the default
|
|
vulp auth test [<account>] # verify a token still works, print a preview
|
|
```
|
|
|
|
`--url` is for self-hosted GitLab/Gitea instances; omit it for github.com/gitlab.com.
|
|
Every subcommand here is fully non-interactive (no prompts) and usable from
|
|
scripts/CI without ever touching `gh` — `vulp auth import` is the convenience
|
|
path when `gh` is already authenticated, `add` is the direct path when it isn't.
|
|
|
|
## 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
|
|
|
|
```bash
|
|
# 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
|
|
```
|