Files
vimpack/lua/vimpack/plan.lua
T
unkin-agent 35b9df9eba
ci/woodpecker/pr/pre-commit Pipeline failed
ci/woodpecker/pr/lint Pipeline failed
ci/woodpecker/pr/test Pipeline failed
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.
2026-09-05 13:28:53 +10:00

95 lines
2.9 KiB
Lua

local M = {}
--- Decide what a single plugin needs. `installed` is the on-disk record for the
--- plugin name: { opt = boolean } or nil when nothing is on disk.
--- Returns 'install' | 'update' | 'skip' plus a human reason.
function M.decide(spec, entry, installed)
if not installed then
return 'install', 'not installed'
end
if not entry then
return 'update', 'no manifest entry for an existing directory'
end
if entry.version ~= spec.version then
return 'update', string.format('version %s -> %s', entry.version, spec.version)
end
if spec.sha256 and entry.sha256 and entry.sha256:lower() ~= spec.sha256:lower() then
return 'update', 'sha256 in spec differs from installed'
end
if installed.opt ~= spec.opt then
return 'update', string.format('moves to %s/', spec.opt and 'opt' or 'start')
end
if spec.url and entry.url and entry.url ~= spec.url then
return 'update', 'url changed'
end
return 'skip', string.format('%s already at %s', spec.name, spec.version)
end
--- Build the full plan for a config against the manifest and the disk scan.
--- `installed` maps plugin name -> { opt = boolean, dir = string }.
function M.build(cfg, manifest, installed)
installed = installed or {}
local plan = { install = {}, update = {}, skip = {}, orphans = {} }
local wanted = {}
for _, spec in ipairs(cfg.plugins) do
wanted[spec.name] = true
local action, reason = M.decide(spec, manifest.plugins[spec.name], installed[spec.name])
plan[action][#plan[action] + 1] = { spec = spec, reason = reason }
end
for name, rec in pairs(installed) do
if not wanted[name] then
plan.orphans[#plan.orphans + 1] = { name = name, dir = rec.dir, opt = rec.opt }
end
end
for name in pairs(manifest.plugins) do
if not wanted[name] and not installed[name] then
plan.orphans[#plan.orphans + 1] = { name = name, dir = nil, opt = false }
end
end
table.sort(plan.orphans, function(a, b)
return a.name < b.name
end)
return plan
end
--- Rows for :VimpackStatus, sorted by name.
function M.status_rows(cfg, manifest, installed)
installed = installed or {}
local plan = M.build(cfg, manifest, installed)
local rows = {}
local function push(list, state)
for _, item in ipairs(list) do
local entry = manifest.plugins[item.spec.name]
rows[#rows + 1] = {
name = item.spec.name,
wanted = item.spec.version,
installed = entry and entry.version or '-',
state = state,
reason = item.reason,
}
end
end
push(plan.skip, 'ok')
push(plan.install, 'missing')
push(plan.update, 'drift')
for _, orphan in ipairs(plan.orphans) do
local entry = manifest.plugins[orphan.name]
rows[#rows + 1] = {
name = orphan.name,
wanted = '-',
installed = entry and entry.version or 'unknown',
state = 'orphan',
reason = 'not in spec',
}
end
table.sort(rows, function(a, b)
return a.name < b.name
end)
return rows
end
return M