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:

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:

{
  '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

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

make test
S
Description
Neovim plugin manager that installs plugins from tagged archives in an HTTP artifact repository into nvim's native package path
Readme 256 KiB
Languages
Lua 97.7%
Makefile 2.3%