Add vimpack: archive-based Neovim plugin manager
## 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:
@@ -0,0 +1,105 @@
|
||||
local fsmod = require('vimpack.fs')
|
||||
|
||||
describe('scan', function()
|
||||
it('maps directories to their bucket', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/start/a/lua', 'p')
|
||||
vim.fn.mkdir(root .. '/opt/b', 'p')
|
||||
vim.fn.writefile({ '' }, root .. '/start/loose.txt')
|
||||
|
||||
local found = fsmod.scan(root)
|
||||
assert_eq(false, found.a.opt)
|
||||
assert_eq(true, found.b.opt)
|
||||
assert_nil(found['loose.txt'])
|
||||
end)
|
||||
|
||||
it('returns nothing for a missing root', function()
|
||||
assert_eq({}, fsmod.scan(tmpdir() .. '/absent'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('target_dir', function()
|
||||
it('splits start and opt', function()
|
||||
assert_eq('/r/start/a', fsmod.target_dir({ name = 'a', dir_name = 'a', opt = false }, '/r'))
|
||||
assert_eq('/r/opt/a', fsmod.target_dir({ name = 'a', dir_name = 'a', opt = true }, '/r'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('strip_top_level', function()
|
||||
it('hoists a single wrapper directory', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/oil.nvim-2.11.0/lua', 'p')
|
||||
assert_eq(root .. '/oil.nvim-2.11.0', fsmod.strip_top_level(root))
|
||||
end)
|
||||
|
||||
it('leaves a flat archive alone', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/lua', 'p')
|
||||
vim.fn.mkdir(root .. '/plugin', 'p')
|
||||
assert_eq(root, fsmod.strip_top_level(root))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('swap', function()
|
||||
it('replaces an existing install', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/target', 'p')
|
||||
vim.fn.writefile({ 'old' }, root .. '/target/marker')
|
||||
vim.fn.mkdir(root .. '/staged', 'p')
|
||||
vim.fn.writefile({ 'new' }, root .. '/staged/marker')
|
||||
|
||||
assert_true(fsmod.swap(root .. '/staged', root .. '/target'))
|
||||
assert_eq({ 'new' }, vim.fn.readfile(root .. '/target/marker'))
|
||||
assert_true(not fsmod.exists(root .. '/target.vimpack-old'))
|
||||
end)
|
||||
|
||||
it('keeps the previous install when the staged tree is missing', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/target', 'p')
|
||||
vim.fn.writefile({ 'old' }, root .. '/target/marker')
|
||||
|
||||
local ok, err = fsmod.swap(root .. '/absent', root .. '/target')
|
||||
assert_true(not ok)
|
||||
assert_match('cannot move new install into place', err)
|
||||
assert_eq({ 'old' }, vim.fn.readfile(root .. '/target/marker'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('extract', function()
|
||||
local function wait_extract(archive, dest)
|
||||
local result
|
||||
fsmod.extract(archive, dest, function(ok, err)
|
||||
result = { ok = ok, err = err }
|
||||
end)
|
||||
vim.wait(10000, function()
|
||||
return result ~= nil
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
it('unpacks a tarball', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
|
||||
vim.fn.writefile({ 'return {}' }, root .. '/src/plug/lua/plug.lua')
|
||||
vim.fn.system({ 'tar', '-czf', root .. '/plug.tar.gz', '-C', root .. '/src', 'plug' })
|
||||
|
||||
local result = wait_extract(root .. '/plug.tar.gz', root .. '/dest')
|
||||
assert_true(result.ok, tostring(result.err))
|
||||
assert_true(fsmod.exists(root .. '/dest/plug/lua/plug.lua'))
|
||||
assert_eq(root .. '/dest/plug', fsmod.strip_top_level(root .. '/dest'))
|
||||
end)
|
||||
|
||||
it('rejects an unknown archive type', function()
|
||||
local result = wait_extract(tmpdir() .. '/thing.rpm', tmpdir() .. '/dest')
|
||||
assert_true(not result.ok)
|
||||
assert_match('unsupported archive type', result.err)
|
||||
end)
|
||||
|
||||
it('reports a corrupt tarball', function()
|
||||
local root = tmpdir()
|
||||
vim.fn.writefile({ 'definitely not a tarball' }, root .. '/bad.tar.gz')
|
||||
local result = wait_extract(root .. '/bad.tar.gz', root .. '/dest')
|
||||
assert_true(not result.ok)
|
||||
assert_match('extraction failed', result.err)
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user