Files
arti-pack/lua/arti-pack/init.lua
T
unkin-agent c75a866c02
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Require an explicit registry instead of defaulting to one
The shipped default named one private, internet-unreachable host, so an
unconfigured install failed against a server it could never reach rather
than saying what to set.

- drop the registry default and make the option required
- reject a non-string, empty, or non-http(s) registry with the option name
  and how to set it
- ask for a registry only when a run has to resolve one, so clean, list,
  and specs with an explicit url keep working without it
2026-09-09 22:44:33 +10:00

254 lines
6.4 KiB
Lua

local fs = require('arti-pack.fs')
local installer = require('arti-pack.installer')
local Lock = require('arti-pack.lock')
local spec_mod = require('arti-pack.spec')
local M = {}
-- registry has no default: it is site-specific, so any shipped value would point
-- every install at a host it cannot reach. See M.setup.
local DEFAULTS = {
plugins = {},
auto_sync = true,
concurrency = 4,
timeout = 120,
}
M.config = vim.deepcopy(DEFAULTS)
local running = false
local function paths()
local data = vim.fn.stdpath('data')
return {
pack_dir = fs.join(data, 'site', 'pack', 'arti-pack'),
lock_path = fs.join(data, 'arti-pack', 'lock.json'),
cache_dir = fs.join(vim.fn.stdpath('cache'), 'arti-pack'),
}
end
local function notify(msg, level)
vim.notify('[arti-pack] ' .. msg, level or vim.log.levels.INFO)
end
local function context()
local p = paths()
return {
registry = M.config.registry,
pack_dir = p.pack_dir,
cache_dir = p.cache_dir,
concurrency = M.config.concurrency,
timeout = M.config.timeout,
lock = Lock.new(p.lock_path):load(),
}
end
-- A spec carrying its own url never touches the registry, so only demand one
-- when something in this run would have to be resolved against it.
local function registry_error(specs, registry)
for _, spec in ipairs(specs) do
if not spec.url then
local _, err = spec_mod.check_registry(registry)
return err
end
end
end
local function installed_dir(ctx, name, opt)
return fs.join(ctx.pack_dir, opt and 'opt' or 'start', name)
end
local function finalise(ctx, results)
local counts = { installed = 0, updated = 0, failed = 0 }
for _, res in ipairs(results) do
counts[res.status] = (counts[res.status] or 0) + 1
if res.status == 'failed' then
notify(string.format('%s: %s', res.name, res.err), vim.log.levels.ERROR)
end
end
ctx.lock:save()
for _, res in ipairs(results) do
if res.status ~= 'failed' then
local entry = ctx.lock:get(res.name)
local dir = installed_dir(ctx, res.name, entry and entry.opt)
local doc = fs.join(dir, 'doc')
if fs.exists(doc) then
pcall(vim.cmd, 'helptags ' .. vim.fn.fnameescape(doc))
end
if not (entry and entry.opt) then
pcall(vim.cmd.packadd, res.name)
end
end
end
local parts = {}
for _, key in ipairs({ 'installed', 'updated', 'failed' }) do
if counts[key] > 0 then
table.insert(parts, string.format('%d %s', counts[key], key))
end
end
if #parts == 0 then
notify('everything up to date')
else
notify(table.concat(parts, ', '), counts.failed > 0 and vim.log.levels.WARN or vim.log.levels.INFO)
end
end
function M.sync(opts)
opts = opts or {}
if running then
return notify('a sync is already running', vim.log.levels.WARN)
end
local specs, errors = spec_mod.parse_all(M.config.plugins)
for _, err in ipairs(errors) do
notify(err, vim.log.levels.ERROR)
end
local ctx = context()
local pending = {}
for _, spec in ipairs(specs) do
local current = ctx.lock:is_current(spec) and fs.exists(installed_dir(ctx, spec.name, spec.opt))
if opts.force or not current then
table.insert(pending, spec)
end
end
if #pending == 0 then
return notify('everything up to date')
end
local reg_err = registry_error(pending, ctx.registry)
if reg_err then
return notify(reg_err, vim.log.levels.ERROR)
end
running = true
notify(string.format('syncing %d plugin(s)', #pending))
installer.run(ctx, pending, function(results)
running = false
finalise(ctx, results)
end)
end
function M.clean()
local specs, errors = spec_mod.parse_all(M.config.plugins)
for _, err in ipairs(errors) do
notify(err, vim.log.levels.ERROR)
end
-- An unparseable entry still names a plugin the user wants, so the set of
-- wanted names is incomplete; deleting from it would remove a live install.
if #errors > 0 then
return notify('clean aborted, no plugin spec may be in error', vim.log.levels.WARN)
end
local wanted = {}
for _, spec in ipairs(specs) do
wanted[spec.name] = true
end
local ctx = context()
local removed = {}
for _, sub in ipairs({ 'start', 'opt' }) do
local root = fs.join(ctx.pack_dir, sub)
for _, entry in ipairs(fs.entries(root)) do
if not wanted[entry.name] then
fs.rm_rf(fs.join(root, entry.name))
table.insert(removed, entry.name)
end
end
end
for _, name in ipairs(ctx.lock:names()) do
if not wanted[name] then
ctx.lock:remove(name)
end
end
ctx.lock:save()
if #removed == 0 then
notify('nothing to clean')
else
table.sort(removed)
notify('removed ' .. table.concat(removed, ', '))
end
end
function M.list()
local specs, errors = spec_mod.parse_all(M.config.plugins)
local ctx = context()
local lines = {}
for _, spec in ipairs(specs) do
local entry = ctx.lock:get(spec.name)
local state
if not entry then
state = 'missing'
elseif not ctx.lock:is_current(spec) then
state = 'outdated (' .. tostring(entry.tag) .. ')'
elseif not fs.exists(installed_dir(ctx, spec.name, spec.opt)) then
state = 'gone from disk'
else
state = 'installed'
end
table.insert(
lines,
string.format('%-32s %-14s %-5s %s', spec.source, spec.tag, spec.opt and 'opt' or 'start', state)
)
end
for _, name in ipairs(ctx.lock:names()) do
local known = false
for _, spec in ipairs(specs) do
known = known or spec.name == name
end
if not known then
table.insert(lines, string.format('%-32s %-14s %-5s %s', name, '-', '-', 'orphaned'))
end
end
for _, err in ipairs(errors) do
table.insert(lines, 'error: ' .. err)
end
if #lines == 0 then
lines = { 'no plugins configured' }
end
notify(table.concat(lines, '\n'))
return lines
end
function M.setup(opts)
opts = opts or {}
M.config = vim.tbl_deep_extend('force', vim.deepcopy(DEFAULTS), opts)
-- tbl_deep_extend merges lists element-wise, which would splice defaults into
-- a user's plugin list; the caller's list is always authoritative.
M.config.plugins = opts.plugins or {}
if opts.registry ~= nil then
local _, err = spec_mod.check_registry(opts.registry)
if err then
notify(err, vim.log.levels.ERROR)
end
end
local p = paths()
fs.mkdirp(fs.join(p.pack_dir, 'start'))
fs.mkdirp(fs.join(p.pack_dir, 'opt'))
if M.config.auto_sync then
vim.schedule(function()
M.sync()
end)
end
return M
end
return M