35b9df9eba
## 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.
95 lines
2.9 KiB
Lua
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
|