Files
vimpack/README.md
T
unkin-agent 35b9df9eba
ci/woodpecker/pr/pre-commit Pipeline failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline failed
Add vimpack: archive-based Neovim plugin manager
## Why
Cloning plugins from git ties the editor to GitHub uptime and moving branches; archives at a pinned version from our own registry are reproducible and reachable offline.

## How
- Add a registry resolver interface `(spec, registry) -> {url, headers, filename, sha256?}` with builtin `template` and `artifactapi` kinds, a per-plugin registry override and a `url` escape hatch.
- Take auth tokens only from an env var or a `token_cmd`; reject a literal token at setup.
- Download with curl, verify an optional sha256, extract to a staging dir and rename into `pack/vimpack/{start,opt}/<name>`, restoring the previous tree if the swap fails.
- Track installed name/version/sha256 in `stdpath('data')/vimpack/manifest.json` and skip plugins already at the wanted version.
- Add `:VimpackInstall`, `:VimpackUpdate`, `:VimpackClean` and `:VimpackStatus`, running downloads through `vim.system` under a concurrency cap.
- Hash with a pure-Lua SHA-256; `vim.fn.sha256()` rejects binary archives with E976.
- Add a dependency-free `tests/minimal.lua` harness and 57 specs covering resolution, auth, manifest, planning, checksum mismatch, orphan detection and end-to-end install.
- Add `.woodpecker/{test,lint,pre-commit}.yaml`, luacheck/stylua config and pre-commit hooks.
2026-09-05 13:28:53 +10:00

158 lines
5.2 KiB
Markdown

# vimpack
A `vim.pack`-shaped Neovim plugin manager that installs plugins from **archives on an
artifact registry** instead of git clones.
## Why
Git-cloning plugins at startup makes an editor depend on GitHub being up, on `git` being
on `PATH`, and on whatever a moving branch happens to contain today. vimpack pulls a
tarball or zip at an explicit version from whatever HTTP registry you already run —
artifactapi, Artifactory, Nexus, S3 behind a CDN, a plain nginx directory — verifies it
against a checksum, and unpacks it into `pack/vimpack/`. The result is reproducible,
auditable, and works on hosts that cannot reach github.com.
Pure Lua. No dependencies beyond Neovim 0.10+ (`vim.system`, `vim.uv`, `vim.json`) plus
`curl` and `tar`/`unzip` for the fetch and unpack.
## Install
vimpack manages plugins but does not manage itself. Drop it on your `runtimepath` — a
submodule, an archive from your registry, or a clone:
```
~/.config/nvim/pack/bootstrap/start/vimpack/
```
## Configure
```lua
require('vimpack').setup({
registry = {
kind = 'artifactapi',
base_url = 'https://artifactapi.k8s.syd1.au.unkin.net',
repo = 'nvim-plugins',
auth = { env = 'ARTIFACTAPI_TOKEN' },
},
plugins = {
{ name = 'telescope.nvim', version = '0.1.8' },
{ name = 'oil.nvim', version = '2.11.0', sha256 = 'd9e2...' },
{ name = 'vim-fugitive', version = '3.7', opt = true },
-- explicit URL, registry bypassed entirely
{ name = 'plenary.nvim', version = '0.1.4', url = 'https://example.com/plenary-0.1.4.tar.gz' },
},
concurrency = 4,
timeout = 120000,
})
```
Then `:VimpackInstall`.
### Plugin spec
| Field | Meaning |
| --- | --- |
| `name` | required; also the directory name under `pack/vimpack/{start,opt}/` |
| `version` | required unless `url` is given; fed to the registry resolver |
| `sha256` | optional; the download is rejected on mismatch |
| `opt` | `true` places the plugin in `opt/` for `:packadd` |
| `url` | fully explicit archive URL; skips resolution |
| `ext` | archive extension, default `tar.gz` |
| `registry` | per-plugin registry override |
| `dir_name` | override the on-disk directory name |
## Registries
A registry turns `(name, version)` into a download URL plus optional auth headers. A
resolver is a plain function:
```lua
--- (spec, registry) -> resolved | nil, err
--- resolved = { url = string, headers = table, filename = string, sha256 = string? }
require('vimpack').register_resolver('mine', function(spec, registry)
return {
url = registry.base_url .. '/' .. spec.name .. '@' .. spec.version .. '.tar.gz',
headers = {},
filename = spec.name .. '.tar.gz',
}
end)
```
Two resolvers ship built in.
### `template` — any HTTP server
`${name}`, `${version}`, `${ext}`, `${slug}`, `${repo}` and `${base_url}` are substituted.
An unknown placeholder is an error, not an empty string.
Artifactory generic repo:
```lua
registry = {
kind = 'template',
url_template = 'https://artifactory.example.com/artifactory/nvim-generic/${name}/${version}/${name}-${version}.${ext}',
auth = { header = 'X-JFrog-Art-Api', prefix = '', token_cmd = { 'pass', 'show', 'artifactory/api-key' } },
}
```
### `artifactapi` — the estate's registry
Resolves to `${base_url}/api/v2/remotes/${repo}/files/${path}`, where `path` defaults to
`${name}/${name}-${version}.${ext}` and is overridable via `path_template`.
```lua
registry = {
kind = 'artifactapi',
base_url = 'https://artifactapi.k8s.syd1.au.unkin.net',
repo = 'nvim-plugins',
auth = { env = 'ARTIFACTAPI_TOKEN' },
}
```
### Auth
Tokens are never written into config. `auth` takes either:
- `env = 'VAR'` — read from the environment, or
- `token_cmd = { 'pass', 'show', 'path' }` (or a shell string) — a command that prints the token.
`header` defaults to `Authorization` and `prefix` to `Bearer ` for that header. A literal
`token` key is rejected at `setup()`.
## Commands
| Command | Effect |
| --- | --- |
| `:VimpackInstall [name...]` | install missing plugins and reconcile drift; up-to-date plugins are skipped |
| `:VimpackUpdate [name...]` | re-fetch regardless of the manifest |
| `:VimpackClean` | remove installed plugins no longer in the spec |
| `:VimpackStatus` | table of wanted vs installed version and state (`ok`, `missing`, `drift`, `orphan`) |
Downloads and extraction run through `vim.system`, capped at `concurrency` in flight, so
the UI stays responsive.
## How it installs
1. Resolve `(name, version)` to a URL and headers.
2. `curl` the archive into `stdpath('cache')/vimpack/archives/`.
3. Verify `sha256` when the spec carries one; a mismatch aborts with expected and got.
4. Extract into `stdpath('cache')/vimpack/stage/`, hoisting a single wrapper directory.
5. Rename into `stdpath('data')/site/pack/vimpack/{start,opt}/<name>`, parking the old
tree aside first and restoring it if the rename fails.
6. Record name, version, sha256 and bucket in `stdpath('data')/vimpack/manifest.json`.
A failure at any step leaves the previous install untouched.
## Out of scope (v1)
Lazy loading. `opt = true` places a plugin under `opt/` for `:packadd`; there is no
event, filetype or command trigger machinery.
## Tests
```sh
nvim --headless --clean -l tests/minimal.lua
luacheck lua plugin tests
stylua --check lua plugin tests
```