e5a260ad25
A ref goes into the archive URL verbatim and an explicit url goes to curl
as a positional, so both could name one thing and fetch another.
- reject "#", "?", "%", "[", "]", "{" and "}" in a tag or branch, which
curl or the server would turn into a different path
- pass "--" before the url so curl cannot read it as an option
- document the ref rules in README and the help file
186 lines
7.9 KiB
Markdown
186 lines
7.9 KiB
Markdown
# arti-pack
|
|
|
|
A Neovim plugin manager that installs plugins from an artifact repository
|
|
instead of git. It downloads upstream source archives over HTTP — a tag for
|
|
anything that publishes one, a branch for anything that does not — and unpacks
|
|
them into Neovim's native package path — see `:h packages`.
|
|
|
|
It works with any HTTP artifact repository that serves upstream tag and branch
|
|
archives at a stable URL — a JFrog Artifactory remote GitHub repository, or any
|
|
caching proxy with the same path shape. Nothing is cloned, no git binary is
|
|
needed, and the editor never has to reach GitHub: every plugin arrives as a
|
|
cached artifact from the same store your builds already pull from.
|
|
|
|
## Requirements
|
|
|
|
- Neovim 0.10+
|
|
- `curl`
|
|
|
|
Archives are unpacked in pure Lua, so no `unzip` or `tar` binary is needed.
|
|
Entry paths are validated and every entry's CRC32 is checked before it lands on
|
|
disk.
|
|
|
|
## Install
|
|
|
|
Each tag is published as a source zip under the same tag-archive path shape
|
|
arti-pack expects, so it installs the same way as everything it manages —
|
|
mirror it into your own artifact repository and fetch it from there:
|
|
|
|
```sh
|
|
curl -fsSL -o /tmp/arti-pack.zip \
|
|
https://artifacts.example.com/artifactory/nvim-plugins/unkin/arti-pack/archive/refs/tags/v0.1.0.zip
|
|
mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack/arti-pack/start"
|
|
unzip -q /tmp/arti-pack.zip -d /tmp/arti-pack
|
|
mv /tmp/arti-pack/arti-pack-0.1.0 \
|
|
"${XDG_DATA_HOME:-$HOME/.local/share}/nvim/site/pack/arti-pack/start/arti-pack"
|
|
```
|
|
|
|
Once installed it can carry itself, since `url` bypasses the registry:
|
|
|
|
```lua
|
|
{
|
|
'unkin/arti-pack',
|
|
tag = 'v0.1.0',
|
|
url = 'https://artifacts.example.com/artifactory/nvim-plugins/unkin/arti-pack/archive/refs/tags/v0.1.0.zip',
|
|
}
|
|
```
|
|
|
|
## Usage
|
|
|
|
```lua
|
|
require('arti-pack').setup({
|
|
registry = 'https://artifacts.example.com/artifactory/github-remote',
|
|
plugins = {
|
|
{ 'nvim-lua/plenary.nvim', tag = 'v0.1.4' },
|
|
{ 'folke/which-key.nvim', tag = 'v3.13.3', opt = true },
|
|
{ 'folke/tokyonight.nvim', tag = 'v4.10.0', as = 'tokyonight' },
|
|
{ 'tpope/vim-fugitive', tag = 'v3.7', ext = 'tar.gz' },
|
|
{ 'hrsh7th/cmp-path', branch = 'main' },
|
|
},
|
|
})
|
|
```
|
|
|
|
`registry` is required and has no default: it is the URL prefix your own
|
|
artifact repository serves upstream tag archives under, so there is no sensible
|
|
value to ship. It must be an `http://` or `https://` URL; surrounding
|
|
whitespace is trimmed and anything else is rejected. Until you set it,
|
|
`:ArtiPackSync` refuses to run and says so:
|
|
|
|
```
|
|
[arti-pack] registry is not configured; set it in require('arti-pack').setup{ registry = 'https://artifacts.example.com/artifactory/github-remote' }
|
|
```
|
|
|
|
`:ArtiPackClean` and `:ArtiPackList` never resolve a URL, so they work without
|
|
it, as do plugins that carry an explicit `url`.
|
|
|
|
Every plugin names an explicit ref — either a `tag` or a `branch`, never both
|
|
and never neither. Plugins install to
|
|
`stdpath('data')/site/pack/arti-pack/start/<name>`, or `.../opt/<name>` when
|
|
`opt = true`.
|
|
|
|
### Spec fields
|
|
|
|
| Field | Default | Meaning |
|
|
| -------- | ---------- | ---------------------------------------------------- |
|
|
| `[1]` | *required* | `owner/repo` |
|
|
| `tag` | *one of* | Upstream tag, used verbatim in the archive path |
|
|
| `branch` | *one of* | Upstream branch to track, used verbatim in the archive path |
|
|
| `opt` | `false` | Install under `opt/` for `:packadd` instead of `start/` |
|
|
| `as` | repo name | Directory name to install as |
|
|
| `ext` | `'zip'` | `zip` or `tar.gz` |
|
|
| `url` | — | Full archive URL, bypassing the registry (no whitespace) |
|
|
|
|
`tag` and `branch` are mutually exclusive: a spec setting both, or neither, is
|
|
rejected with an error naming the plugin and nothing is installed for it. Both
|
|
are one path segment of the archive URL, so both are trimmed of surrounding
|
|
whitespace and rejected if they are `.` or `..`, or contain inner whitespace,
|
|
`/`, `\`, or any of `#`, `?`, `%`, `[`, `]`, `{`, `}` — each of those would
|
|
make curl fetch something other than the ref you named. A branch whose name
|
|
contains `/` therefore needs an explicit `url`.
|
|
|
|
### Options
|
|
|
|
| Option | Default | Meaning |
|
|
| ------------- | ------- | ------------------------------------------- |
|
|
| `registry` | *required* | Archive URL prefix placed in front of `owner/repo` |
|
|
| `auto_sync` | `true` | Sync missing and changed plugins on startup |
|
|
| `concurrency` | `4` | Plugins downloaded in parallel |
|
|
| `timeout` | `120` | Per-download timeout in seconds |
|
|
|
|
### Commands
|
|
|
|
| Command | Effect |
|
|
| ----------------- | ---------------------------------------------------------- |
|
|
| `:ArtiPackSync` | Install missing plugins and apply changed refs |
|
|
| `:ArtiPackSync!` | Reinstall everything, ignoring recorded state |
|
|
| `:ArtiPackUpdate` | Refetch every branch-tracking plugin |
|
|
| `:ArtiPackClean` | Remove installed plugins no longer in the spec |
|
|
| `:ArtiPackList` | Show every configured plugin and its state |
|
|
|
|
## Tracking a branch
|
|
|
|
Plenty of upstreams publish no tags at all, so a `branch` spec installs from the
|
|
branch archive instead. A branch is mutable, and that changes when arti-pack
|
|
fetches:
|
|
|
|
- `:ArtiPackSync` installs a branch plugin that is missing, that moved between
|
|
`start` and `opt`, or whose `branch` you changed — and otherwise leaves it
|
|
alone. It says how many branch plugins it did not recheck. Startup cost and
|
|
the load on your artifact store stay the same as a tag-only setup, and your
|
|
editor does not change under you on a restart.
|
|
- `:ArtiPackUpdate` refetches every branch plugin, whether or not upstream
|
|
moved. This is the only command that goes looking for new branch content.
|
|
- `:ArtiPackSync!` still reinstalls everything.
|
|
|
|
Branch archives are never served from the local archive cache, so an update
|
|
always sees what the store has now.
|
|
|
|
The lockfile records the branch name, a digest of the archive that was actually
|
|
installed, and when it was fetched — never a version, because a branch is not
|
|
one. `:ArtiPackList` reports that instead of guessing:
|
|
|
|
```
|
|
hrsh7th/cmp-path @main start tracking branch main, fetched 2026-09-08 10:14 (7db8f64a)
|
|
nvim-lua/plenary.nvim v0.1.4 start installed
|
|
```
|
|
|
|
The digest is a content identity, not a file checksum: Neovim's `sha256()`
|
|
refuses binary input, so it hashes the archive's base64 form. It will not match
|
|
`sha256sum <archive>`.
|
|
|
|
A branch plugin is never reported as up to date: nothing on disk can know
|
|
whether upstream has moved. After an update, an unchanged digest is reported as
|
|
`unchanged` rather than as an update that changed nothing.
|
|
|
|
## How it resolves an archive
|
|
|
|
```
|
|
<registry>/<owner>/<repo>/archive/refs/tags/<tag>.<ext>
|
|
<registry>/<owner>/<repo>/archive/refs/heads/<branch>.<ext>
|
|
```
|
|
|
|
which for the examples above are
|
|
|
|
```
|
|
https://artifacts.example.com/artifactory/github-remote/nvim-lua/plenary.nvim/archive/refs/tags/v0.1.4.zip
|
|
https://artifacts.example.com/artifactory/github-remote/hrsh7th/cmp-path/archive/refs/heads/main.zip
|
|
```
|
|
|
|
That is GitHub's own archive path shape, so a remote repository proxying
|
|
`github.com` resolves it unchanged.
|
|
|
|
Archives are cached under `stdpath('cache')/arti-pack`, checked for a real zip
|
|
or gzip magic header before being trusted, and unpacked into a staging
|
|
directory. Source archives wrap their contents in a single `<repo>-<ref>`
|
|
directory, which is stripped on the way in. Installs are recorded in
|
|
`stdpath('data')/arti-pack/lock.json`, so a sync with nothing to do costs
|
|
nothing. Helptags are regenerated per plugin after install.
|
|
|
|
A failed plugin is reported and skipped; the rest of the run continues.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
make test
|
|
```
|