# 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}/`, 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 ```