949fe00f6a
- Reject archive members that are absolute, escape via .., are special files or link outside the destination, before anything is written - Stage under the pack root so the swap is a same-filesystem rename, with an EXDEV copy fallback; propagate auth errors, validate dir_name
169 lines
6.0 KiB
Markdown
169 lines
6.0 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. List every archive member and reject the archive outright if any entry is absolute,
|
|
escapes the destination via `..`, is a device or fifo, or is a link pointing outside
|
|
the destination. Nothing is written until the whole listing is clean.
|
|
5. Extract into `<pack root>/.vimpack-stage/`, hoisting a single wrapper directory.
|
|
Staging lives under the pack root so the swap below is a same-filesystem rename;
|
|
`stdpath('cache')` and `stdpath('data')` are frequently separate mounts.
|
|
6. Rename into `stdpath('data')/site/pack/vimpack/{start,opt}/<name>`, parking the old
|
|
tree aside first and restoring it if the rename fails. Should the two ever land on
|
|
different filesystems anyway, the staged tree is copied beside the target and that
|
|
copy is renamed into place, so the target is never half-written.
|
|
7. 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.
|
|
|
|
Concurrent Neovim instances. The manifest is rewritten wholesale at the end of a run, so
|
|
two `:VimpackInstall` runs overlapping across processes are last-writer-wins and one set
|
|
of manifest updates is lost; the installed trees themselves stay consistent.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
nvim --headless --clean -l tests/minimal.lua
|
|
luacheck lua plugin tests
|
|
stylua --check lua plugin tests
|
|
```
|