local M = {} local async_install_started = false local function has_valid_head(path) if vim.fn.isdirectory(path .. "/.git") == 0 then return true end vim.fn.system({ "git", "-C", path, "rev-parse", "--verify", "HEAD" }) return vim.v.shell_error == 0 end local function prune_broken_pack_repos() local root = vim.fn.stdpath("data") .. "/site/pack/core/opt" if vim.fn.isdirectory(root) == 0 then return end for _, dir in ipairs(vim.fn.readdir(root)) do local path = root .. "/" .. dir if vim.fn.isdirectory(path) == 1 and not has_valid_head(path) then vim.fn.delete(path, "rf") end end end local function to_src(value) if value:match("^[%w+.-]+://") or value:match("^git@") then return value end return "https://github.com/" .. value end local function plugin_name(spec) if type(spec) == "table" and type(spec.name) == "string" and spec.name ~= "" then return spec.name end local src = type(spec) == "string" and spec or spec.src local name = src:gsub("/$", ""):match("([^/]+)$") or src return name:gsub("%.git$", "") end local function plugin_installed(spec) local root = vim.fn.stdpath("data") .. "/site/pack/core/opt" local path = root .. "/" .. plugin_name(spec) return vim.fn.isdirectory(path) == 1 end local function install_missing_async(specs) if async_install_started or #specs == 0 then return end async_install_started = true local specs_json = vim.json.encode(specs) local lua_expr = string.format( "local s=vim.json.decode(%q); vim.pack.add(s,{confirm=false,load=false})", specs_json ) vim.system({ vim.v.progpath, "--headless", "-u", "NONE", "+lua " .. lua_expr, "+qall", }, { detach = true }) end function M.add(specs) local resolved = {} for _, spec in ipairs(specs) do if type(spec) == "string" then table.insert(resolved, to_src(spec)) else local plugin = vim.deepcopy(spec) plugin.src = to_src(plugin.src) table.insert(resolved, plugin) end end local installed = {} local missing = {} for _, spec in ipairs(resolved) do if plugin_installed(spec) then table.insert(installed, spec) else table.insert(missing, spec) end end if #missing > 0 then install_missing_async(missing) vim.schedule(function() vim.notify( "Missing plugins detected. Started background install; restart Neovim when it finishes.", vim.log.levels.WARN ) end) end if #installed == 0 then return false end local ok, err = pcall(vim.pack.add, installed, { confirm = false, load = true }) if not ok and tostring(err):find("ambiguous argument 'HEAD'", 1, true) then prune_broken_pack_repos() vim.fn.delete(vim.fn.stdpath("config") .. "/nvim-pack-lock.json") ok, err = pcall(vim.pack.add, installed, { confirm = false, load = true }) end if not ok then vim.schedule(function() local msg = "vim.pack failed to load installed plugins." vim.notify(msg .. "\n" .. tostring(err), vim.log.levels.WARN) end) return false end return true end return M