## 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.
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
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:
--- (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:
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.
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, ortoken_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
- Resolve
(name, version)to a URL and headers. curlthe archive intostdpath('cache')/vimpack/archives/.- Verify
sha256when the spec carries one; a mismatch aborts with expected and got. - Extract into
stdpath('cache')/vimpack/stage/, hoisting a single wrapper directory. - Rename into
stdpath('data')/site/pack/vimpack/{start,opt}/<name>, parking the old tree aside first and restoring it if the rename fails. - 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
nvim --headless --clean -l tests/minimal.lua
luacheck lua plugin tests
stylua --check lua plugin tests