# CLI Reference
Detailed reference for every `rvpm` subcommand. The high-level overview lives
in [CLAUDE.md](../CLAUDE.md); this file is the authoritative source for flag
behavior and per-command edge cases.
## Command list
| Command | Function | Description |
|---------|------|------|
| `sync [--prune] [--frozen] [--no-lock] [--rebuild [QUERY]]` | `run_sync()` | clone/pull + merged + loader.lua generation. `--prune` also deletes unused plugin directories. Even without it, a warning is shown at the end if any are unused. Loads the lockfile (`<config_root>/rvpm.lock`) to align to pinned commits, and writes back the new HEAD on completion. `--frozen` errors immediately if any plugin is not registered (CI / fresh machine); `--no-lock` skips lockfile entirely. **Build runs only when git HEAD moved** (avoids re-running e.g. `:TSUpdate` on every no-op pull); `--rebuild` restores the previous always-build behavior. `--rebuild <QUERY>` narrows the rebuild scope to plugins whose url / name partially matches (for iterating on a single plugin's build command) — `matches_rebuild_filter` decides this, resolved into a bool before closure spawn so it does not get pulled into the async move. |
| `generate` | `run_generate()` | Regenerate loader.lua only. |
| `clean` | `run_clean()` | Delete `{cache_root}/plugins/repos/<host>/<owner>/<repo>/` for plugins removed from config.toml. No git ops, faster than `sync --prune` (matters on configs with 200+ plugins). Shares the helper `prune_unused_repos()` with `sync --prune`. |
| `add <repo> [--auto-lazy \| --no-lazy]` | `run_add()` | TOML add + clone of just that plugin + generate. Duplicate detection normalizes via `installed_full_name` (absorbs https / owner/repo / ssh / case / `.git` / trailing `/` variation, sharing the same logic as the installed marker in `rvpm browse`). The written URL form follows `options.url_style` (`short` / `full`). **After clone, `plugin_scan::scan_plugin` runs to pick up user-facing commands / keymaps from the plugin's `plugin/` / `ftplugin/` / `after/plugin/` / `lua/`**, and based on `options.auto_lazy` (or `--auto-lazy` / `--no-lazy` overrides) chooses an interactive prompt / unconditional accept / skip (`AutoLazyPolicy::Ask/Always/Never`). On accept, `suggest_cmd_triggers_smart` LCP-clusters command groups (regex-izes them as `/^Prefix/` when there is a 3+ character common prefix); keymaps are enumerated; the corresponding `[[plugins]]` entry in `config.toml` gets `on_cmd` / `on_map` patched in place. |
| `tune [query] [--ai <backend>] [--no-ai]` | `run_tune()` | Run an AI chat loop (`run_ai_tune`) against **plugins already registered in the config**. Differences from `add --ai`: skips clone, shows the AI both the existing entry and existing hook bodies, and asks for "two variants — a fresh proposal (clean redesign) and a merged proposal (keep existing while improving)." On apply, the user picks **fresh / merged / keep existing** per section (`pick_plugin_entry_decision` / `pick_hook_decision`). `Replace` mode removes stale fields the AI omitted (e.g. an outdated `on_cmd`). User guardrails are exercised either by telling the AI "do not touch X" inside the chat loop or by selecting keep existing in preview. AI-only — if `effective_ai == Off`, errors explicitly (use `set` for non-AI tweaks). |
| `update [query]` | `run_update()` | Pull existing plugins (does not clone). On completion, overwrites the lockfile with the new HEAD (entries for non-target plugins are preserved even on partial update). |
| `remove [query]` | `run_remove()` | TOML + directory deletion + generate. |
| `edit [query] [--init\|--before\|--after] [--global]` | `run_edit()` | Edit per-plugin init/before/after.lua in the editor. Flags skip file selection. `--global` edits global hooks — **`--init` directly opens Neovim's main `init.lua` (`nvim_init_lua_path()`)**, while `--before` / `--after` open `<config_root>/before.lua` / `after.lua`. This gives a consistent `init/before/after` 3-way UX between per-plugin and global. The `[ Global hooks ]` sentinel in interactive selection behaves the same. |
| `set [query] [flags]` | `run_set()` | Change lazy/merge/on_* etc. interactively or via arguments. `on_cmd` and friends accept comma-separated or JSON array; `--on-map` also supports JSON object/array for the table form. The `[ Open config.toml in $EDITOR ]` sentinel is an escape hatch for direct TOML editing. |
| `config` | `run_config()` | Open `config.toml` directly in `$EDITOR` (only `generate` runs on exit; if you added a new plugin, run `rvpm sync` explicitly). |
| `init [--write]` | `run_init()` | Show the `dofile(...)` snippet that wires loader.lua into Neovim's `init.lua`. `--write` appends it automatically (creates init.lua if absent). Honors `$NVIM_APPNAME`. |
| `list [--no-tui]` | `run_list()` | Plugin list display. Defaults to a TUI with action keys `[S] sync / [R] sync --rebuild / [u/U] update / [d] remove / [e] edit / [s] set / [t] tune / [c] config.toml / [b] browse / [?] help`. **The first row is the `[ Global hooks ]` sentinel** — `e` jumps to global edit (init/before/after); `u/d/s/t` are no-ops there. Navigation: `j/k/g/G/Ctrl-d/u/f/b`; search: `/n/N`. `--no-tui` outputs pipe-friendly plain text. |
| `browse` | `run_browse()` | Plugin browser TUI for the GitHub `neovim-plugin` topic (up to 300 entries, fetched in 3 pages). README is rendered as GFM via tui-markdown (set `options.browse.readme_command` to delegate to an external renderer like mdcat / glow, with a fallback). A leading `✓` marks installed entries; pressing `Enter` on an installed plugin warns and skips add. `/` is local incremental search (name + description + topics) with `n`/`N` for match jumps. `S` runs a GitHub API search. `Tab` toggles list/README focus. `o` opens the browser; `s` cycles sort; `R` clears cache and refetches; `c` opens config.toml in the editor; `l` jumps to the list TUI; `?` shows help. |
| `doctor` | `run_doctor()` | One-shot command that diagnoses 16 items across config / state / Neovim integration / external tools. 4 categories (plugin config / state integrity / Neovim integration / external tools); output respects `options.icons` (nerd/unicode/ascii). Exit codes: `0` = all ok, `1` = errors present, `2` = warnings only. External commands (nvim/git/chezmoi) are probed via `tokio::process::Command` + 2s timeout so they cannot hang. |
| `profile [--runs N (1..=20)] [--top N] [--json] [--no-tui] [--no-merge] [--no-instrument]` (`--json` and `--no-tui` cannot be combined) | `run_profile()` | Run `nvim --headless --startuptime` N times (default 3) and aggregate startup time per plugin. By default, temporarily swaps loader.lua for a **phase-instrumented build** (`LoaderSwapGuard` + atomic rename, restoring the original even on panic / Ctrl-C). Empty `.vim` markers for phase boundaries + per-plugin init/trig are placed in `tmp/rvpm-profile-markers-*/` ahead of time, and per-plugin times for phases 4/6/7 are extracted from the clock deltas of `vim.cmd("source <marker>")`. `--no-merge` passes `force_unmerge=true` to treat all plugins as merge=false (merged/ is left untouched; only the rtp append path changes). `--no-instrument` skips the swap and uses raw `--startuptime` only (same as v1). On startup, a stale `loader.lua.bak` from a prior crash is auto-restored (`recover_stale_loader_backup`). The TUI adds info via a phase timeline, init/load/trig columns, and a sort cycle (`s`). |
| `log [query] [--last N] [--full] [--diff]` | `run_log()` | Display the change history (`<cache_root>/update_log.json`) recorded during `sync` / `update` / `add`. `[query]` partially matches plugin names; `--last N` (default 1, max 20) shows the last N runs; `--diff` embeds README / CHANGELOG / doc/ patches; `--full` is reserved for future body display. Conventional Commits' `<type>!:` / `BREAKING CHANGE:` footers are highlighted with a `⚠ BREAKING` prefix. |
| `completion <SHELL>` | `run_completion()` | Print a shell completion script to stdout (#114). `SHELL` is one of `bash` / `zsh` / `fish` / `powershell` / `elvish` (clap_complete's supported set). Output is generated at runtime from the live `Cli` definition so new subcommands and flags are picked up automatically. Pipe into the appropriate location for the shell — see `rvpm completion --help` for example install paths. The Neovim-side completion lives separately in `rvpm.nvim` (`lua/rvpm/command.lua`) and is hand-maintained per the contributor checklist below. |
**Removed commands:**
- `status` → folded into `list --no-tui` (plain text output is feature-equivalent).
## Checklist when adding CLI flags / subcommands
When you **add, rename, or remove** a subcommand flag (`--prune` / `--ai` / `--no-tui` etc.) or **add a new subcommand**, also keep `lua/rvpm/command.lua` in [rvpm.nvim](https://github.com/yukimemi/rvpm.nvim) in sync. Specifically:
- New subcommand: add it to the `SUBCOMMANDS` array. If it should be routed to the TUI, register it in the `TUI` table; if it takes a plugin-name argument, register it in the `PLUGIN_ARG_SUBS` table; if it has flags, add an entry to the `FLAGS` table. Consider adding a convenience Lua API in `lua/rvpm/init.lua` as well.
- Adding/renaming/removing a flag on an existing subcommand: update the relevant `FLAGS[<sub>]` entry.
The rvpm.nvim side **hardcodes a mirror** of rvpm core's flag list to power `:Rvpm <sub> --<Tab>` completion (parsing `--help` dynamically was rejected on Neovim startup-cost grounds). Forgetting to sync causes silent drift in Neovim where "an existing flag is missing from completion" or "a removed flag still appears as a candidate." Add this to your CLI-PR self-review checklist.