Add vimpack: archive-based Neovim plugin manager
ci/woodpecker/pr/pre-commit Pipeline failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline failed

## 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.
This commit is contained in:
2026-09-05 13:28:53 +10:00
parent e510cdec23
commit 35b9df9eba
25 changed files with 2370 additions and 1 deletions
+154
View File
@@ -0,0 +1,154 @@
-- End-to-end install over file:// URLs: real curl, real tar, real swap.
local checksum = require('vimpack.checksum')
local config = require('vimpack.config')
local fsmod = require('vimpack.fs')
local installer = require('vimpack.installer')
local manifest = require('vimpack.manifest')
local plan = require('vimpack.plan')
local function make_tarball(root, version, body)
local src = vim.fs.joinpath(root, 'src', 'demo.nvim-' .. version, 'lua')
vim.fn.mkdir(src, 'p')
vim.fn.writefile({ body }, vim.fs.joinpath(src, 'demo.lua'))
local archive = vim.fs.joinpath(root, 'demo-' .. version .. '.tar.gz')
vim.fn.system({ 'tar', '-czf', archive, '-C', vim.fs.joinpath(root, 'src'), 'demo.nvim-' .. version })
return archive
end
local function install(cfg, data, pack_root)
local built = plan.build(cfg, data, fsmod.scan(pack_root))
local items = vim.list_extend(vim.deepcopy(built.install), built.update)
local results
installer.run(items, {
manifest = data,
pack_root = pack_root,
timeout = 30000,
concurrency = 2,
}, function(r)
results = r
end)
vim.wait(30000, function()
return results ~= nil
end)
return results, items
end
describe('install end to end', function()
it('downloads, extracts, swaps and records the manifest', function()
local root = tmpdir()
local pack_root = vim.fs.joinpath(root, 'pack')
local archive = make_tarball(root, '1.0.0', 'return 1')
local cfg = assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive } },
}))
local data = manifest.empty()
local results = install(cfg, data, pack_root)
assert_eq(1, #results)
assert_true(results[1].ok, tostring(results[1].err))
local installed = vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')
assert_true(fsmod.exists(installed), 'expected ' .. installed)
assert_eq({ 'return 1' }, vim.fn.readfile(installed))
local entry = manifest.get(data, 'demo.nvim')
assert_eq('1.0.0', entry.version)
assert_eq(assert(checksum.sha256_file(archive)), entry.sha256)
-- Second pass is a no-op: nothing left to install.
local _, items = install(cfg, data, pack_root)
assert_eq(0, #items)
end)
it('replaces the old tree on a version bump', function()
local root = tmpdir()
local pack_root = vim.fs.joinpath(root, 'pack')
local data = manifest.empty()
local v1 = make_tarball(root, '1.0.0', 'return 1')
local cfg1 = assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. v1 } },
}))
assert_true(install(cfg1, data, pack_root)[1].ok)
local v2 = make_tarball(root, '2.0.0', 'return 2')
local cfg2 = assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '2.0.0', url = 'file://' .. v2 } },
}))
local results = install(cfg2, data, pack_root)
assert_true(results[1].ok, tostring(results[1].err))
assert_eq({ 'return 2' }, vim.fn.readfile(vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')))
assert_eq('2.0.0', manifest.get(data, 'demo.nvim').version)
end)
it('aborts on a checksum mismatch and keeps the previous install', function()
local root = tmpdir()
local pack_root = vim.fs.joinpath(root, 'pack')
local data = manifest.empty()
local v1 = make_tarball(root, '1.0.0', 'return 1')
assert_true(install(
assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. v1 } },
})),
data,
pack_root
)[1].ok)
local v2 = make_tarball(root, '2.0.0', 'return 2')
local results = install(
assert(config.normalize({
plugins = {
{ name = 'demo.nvim', version = '2.0.0', url = 'file://' .. v2, sha256 = string.rep('a', 64) },
},
})),
data,
pack_root
)
assert_true(not results[1].ok)
assert_match('checksum mismatch', results[1].err)
assert_eq({ 'return 1' }, vim.fn.readfile(vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')))
assert_eq('1.0.0', manifest.get(data, 'demo.nvim').version)
end)
it('names the url when the download fails', function()
local root = tmpdir()
local missing = 'file://' .. vim.fs.joinpath(root, 'nope.tar.gz')
local results = install(
assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '1.0.0', url = missing } },
})),
manifest.empty(),
vim.fs.joinpath(root, 'pack')
)
assert_true(not results[1].ok)
assert_match('download failed', results[1].err)
assert_match(missing, results[1].err)
end)
it('moves a plugin from start to opt', function()
local root = tmpdir()
local pack_root = vim.fs.joinpath(root, 'pack')
local data = manifest.empty()
local archive = make_tarball(root, '1.0.0', 'return 1')
assert_true(install(
assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive } },
})),
data,
pack_root
)[1].ok)
local results = install(
assert(config.normalize({
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive, opt = true } },
})),
data,
pack_root
)
assert_true(results[1].ok, tostring(results[1].err))
assert_true(fsmod.exists(vim.fs.joinpath(pack_root, 'opt', 'demo.nvim', 'lua', 'demo.lua')))
assert_true(not fsmod.exists(vim.fs.joinpath(pack_root, 'start', 'demo.nvim')))
end)
end)