# 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 per section: the `[[plugins]]` TOML entry uses `pick_plugin_entry_decision` (**fresh / merged / keep**, 3-way), and each per-plugin hook file (`init.lua` / `before.lua` / `after.lua`) uses `pick_hook_decision` which is the same 3-way **plus a `Remove existing` choice** when the user already has the file on disk (4-way; #115). `Replace` mode strips stale TOML fields the AI omitted (e.g. an outdated `on_cmd`). When the AI omits a hook tag entirely AND the user has the file, the hook menu collapses to a 2-choice `Keep / Remove existing` prompt with `[OMITTED BY AI]` flagged in the preview — `Remove` is hook-only and the menu defaults to `Keep` (safe-by-default), so deletion only happens on an explicit pick. 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.