Repository navigation
Command reference
| Command | Installs missing | Fetches sources | Re-clones | Writes runtime lock | Writes manifest | Prints script |
|---|---|---|---|---|---|---|
lock |
yes | no | no | yes | yes, pins honored | no |
lock --update |
yes | yes | no | yes | yes, pins refreshed | no |
lock --reinstall |
yes | no | yes, pins honored | yes | yes | no |
source |
yes, when stale | no | no | only when relocking | only when relocking | yes |
source --relock |
yes | no | no | yes | yes | yes |
source --update |
yes | yes | no | yes | yes | yes |
source --reinstall |
yes | no | yes | yes | yes | yes |
update |
yes | yes | no | no | no | yes |
update --lock |
yes | yes | no | yes | yes | no |
--force joins --update or --reinstall and pulls frozen plugins in.
Every mode except the update ones reads pinned revisions from the manifest and checks them out. That is what makes git pull && shelf lock reproduce a setup. Update modes resolve refs from the config instead, then rewrite the manifest.
--update really does fetch, the objects and origin/* refs land in the clone, but an already-installed plugin stays on its current commit. Fresh installs get the current tip. This is a known bug in the update path, the fix is to check out the fetched ref once the fetch returns.
Until it lands:
-
New upstream commits reach an existing plugin only through a fresh clone. Delete the manifest and re-clone everything:
rm ~/.config/shelf/plugins.lock # or plugins.<profile>.lock shelf lock --reinstall
Plugins with a
tagorrevpin in the config go back to that pin, which is what pins are for. -
Changing
tag,rev, orbranchin the config takes effect withshelf lock --updateorshelf update --lock. Plainshelf lockre-applies the manifest and the manifest wins over the config, so it restores the old revision. -
A tag that moved upstream needs a reinstall. Fetch never overwrites an existing local tag.
shelf init [--shell bash|zsh]
Creates a new config.toml. Interactive mode asks which shell to use and confirms the target path. --shell skips the shell question, --non-interactive skips both, and both together write the file with no prompts at all.
If the config already exists, init prints a warning to stderr and does nothing. It never overwrites.
shelf init --shell bash --non-interactiveshelf lock [--update | --reinstall] [--concurrency N] [--force]
Installs every configured plugin, then writes two files, the runtime lock in the data directory and the revision manifest next to your config. This is the command to run after any config edit.
Modes:
- plain
shelf lockinstalls what is missing and checks out the pinned revisions from the manifest. Existing installs are left alone. -
--updatefetches sources, resolves refs from the config instead of the manifest, and rewrites the manifest with the result. Frozen plugins are skipped. -
--reinstalldeletes and re-clones every source, then applies the manifest pins again. This is how you apply a newdepthorcloneopts, clone options only affect fresh clones. -
--forcewith--updateor--reinstallalso refreshes frozen plugins.
Installs run concurrently, 8 at a time by default. --concurrency 1 serializes them, which is occasionally what a flaky server needs.
shelf source [--relock | --update | --reinstall] [--concurrency N] [--force]
Prints the shell script for your current lock to stdout. This is the command that belongs in .bashrc or .zshrc:
eval "$(shelf source)"With no flags it reads the existing lock, verifies it against the config fingerprint and the installed files, and renders. If the config, profile, shell, or installed files changed, it relocks on the spot. Flags force the issue:
-
--relockregenerates the lock even when it looks valid. -
--updateupdates sources, rewrites the lock, and renders. -
--reinstallre-clones and rewrites the lock, then renders.
On the fast path --verbose prints Unlocked <path> so you can see it skipped the relock.
shelf reload
Prints exec <shell> to stdout. Evaluate it in the current shell after a config change:
eval "$(shelf reload)"The exec replaces the shell and reruns your startup files, so the eval "$(shelf source)" line picks up new plugins without opening a new terminal.
shelf update [--lock] [--concurrency N] [--force]
Fetches every non-frozen plugin, then prints the refreshed script to stdout. --lock skips the script and writes both the runtime lock and the revision manifest instead, which is what cron jobs and dotfiles bootstraps want.
The fetch does not move an installed clone to the new commits yet. See update behavior today above for what actually changes and how to pick up new commits in the meantime.
Prints the resolved paths as key=value lines:
config_dir=...
data_dir=...
config_file=...
lock_file=...
lock_file reflects the active profile, so shelf --profile work path shows plugins.work.lock.
Verifies the lock, then checks every git-backed plugin's checked-out commit against the locked revision in parallel. Output is one line per plugin:
zsh-autosuggestions: ok
powerlevel10k: revision abc1234, want def5678
some-plugin: unable to read revision
Any non-ok line makes the command exit 2 with error: installed plugins differ from the lockfile. A stale lock fails earlier with error: lockfile is stale or selected plugin files are missing.
Checks the whole installation and prints a report:
version: shelf 1.2.3
shell: /usr/bin/zsh
zsh 5.9
config: ok ~/.config/shelf/config.toml
git: ok /usr/bin/git
lock: ok ~/.local/share/shelf/plugins.lock
No problems found
The git: line only appears when the config has git-based plugins, and doctor then fails if git is missing from PATH. It also fails when the shell in the config is not installed. Run this first when something feels wrong.
Removes installed sources that the config no longer references. Prints removed: <path> per entry, or nothing to clean. lock, update, and a relocking source prune the same way automatically, so you rarely need clean by hand. It is the right command after a big config edit or a profile rename.
Prints every plugin name from the config, one per line. Reads the config, not the lock, so it works before the first shelf lock.
Reads the lock and reports one plugin:
- source: "https://github.com/zsh-users/zsh-autosuggestions"
- rev: "a411ef3e09924c1d7d4f0a3e9d0b8b6f3d3e2b5a"
- files: "/home/you/.local/share/shelf/repos/github.com/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh"
- size: "184K"
rev only appears for git sources, size only for plugins with an installed directory. Inline plugins have neither. Fails with error: plugin "x" is not in the lock file when the name is unknown or the lock is stale.
Adds a plugin to config.toml while preserving comments and formatting around it.
Source flags, pick exactly one:
--github owner/repo --gist user/id --gitlab owner/repo
--bitbucket team/repo --codeberg owner/repo --git URL
--remote URL --local PATH --inline "shell code"
Selection and behavior flags:
--dir SUBDIR plugin subdirectory inside the source
--file FILE one exact file to load instead of globbing
--use GLOB load glob (repeatable)
--ignore GLOB exclude glob (repeatable)
--apply NAME template name (repeatable)
--build CMD install-time command (repeatable)
--hooks key=value hook (repeatable)
--profiles NAME profile (repeatable)
--branch NAME | --tag NAME | --rev SHA
--proto https|git|ssh forge protocol
--depth N clone depth, 0 means full history
--cloneopts ARG extra `git clone` argument (repeatable)
--optional skip when the local path is missing
--frozen never fetch updates for this plugin
Examples:
shelf add autosuggest --github zsh-users/zsh-autosuggestions --apply defer
shelf add private --github myorg/secret --proto ssh
shelf add theme --local ~/dotfiles/themes/mythemeOpens config.toml with $SHELF_EDITOR, then $VISUAL, then $EDITOR. The value is split with shell-word rules, so SHELF_EDITOR="nvim --wait" works and quoted paths survive. No editor configured is an error, not a silent no-op.
shelf remove NAME
shelf remove --interactive # -i
Deletes one plugin table from the config, comments and unrelated TOML intact. --interactive opens a multi-select picker instead and cannot be combined with NAME or with --non-interactive. After removing, run shelf lock to drop the plugin from the lock and prune its install.
Generates a completion script. SHELL is one of bash, zsh, fish, powershell.
shelf completion zsh > "${fpath[1]}/_shelf"shelf self-update [--version TAG] [--yes] [--force]Downloads the latest release archive, verifies it against the published checksums, and replaces the running binary atomically. Development builds refuse to update unless you pass --force.
--version TAG installs an exact release, which unlike an unpinned update may move backwards on purpose. --yes answers the confirmation prompt; a run with no terminal, or with --non-interactive, cannot prompt and needs it.
Self-update suits a standalone install. The official installer places the binary at ~/.local/bin/shelf and can itself update:
curl -fsSL https://github.com/rubiin/shelf/releases/latest/download/install.sh | shThe script downloads the matching release archive, verifies it against the release's published sha256, and installs the binary. SHELF_INSTALL_PATH chooses another location and SHELF_VERSION pins a release.
A package manager that owns the install disables self-update rather than letting it replace the packager's binary. The AUR, deb, rpm, and apk packages install /usr/lib/shelf/.disable-self-update, so shelf self-update warns and refuses instead:
self-update is disabled for this install.
Update shelf the same way you installed it, or reinstall it with the standalone installer:
curl -fsSL https://github.com/rubiin/shelf/releases/latest/download/install.sh | sh
A packager can also ship update instructions at lib/shelf-self-update-instructions.toml, lib/shelf/shelf-self-update-instructions.toml, or lib64/shelf/shelf-self-update-instructions.toml; shelf prints the file's message (or first named command) instead. Marker and instructions paths are resolved under the install prefix, the directory two levels above the binary, so /usr/bin/shelf looks under /usr. SHELF_SELF_UPDATE_AVAILABLE=false disables self-update and =true re-enables it, and --force bypasses the refusal.
Prints shelf version <version>.