#!/usr/bin/env lua5.4

package.preload["gitproto"] = assert(load([===[
-- SPDX-License-Identifier: MPL-2.0
--[[
--	rollmeow
--	/src/gitproto.lua
--	Copyright (C) 2025 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local string		= require "string";
local table		= require "table";

local fmtErr		= require "helpers".fmtErr;

--[[
--	Summarized from Git's documentation gitprotocol-common.adoc
--
--		HEXDIG		= DIGIT / "a" / "b" / "c" / "d" / "e" / "f"
--		pkt-line	= data-pkt / flush-pkt
--
--		data-pkt	= pkt-len pkt-payload
--		pkt-len		= 4*(HEXDIG)
--		pkt-payload	= (pkt-len - 4)*(OCTET)
--
--		flush-pkt	= "0000"
--
--	and from gitprotocol-v2.adoc
--
-- > In protocol v2 these special packets will have the following semantics:
-- >   * '0000' Flush Packet (flush-pkt) - indicates the end of a message
-- >   * '0001' Delimiter Packet (delim-pkt) - separates sections of a message
-- >   * '0002' Response End Packet (response-end-pkt) - indicates the end of a
-- >	  response for stateless connections
--]]

local flushPkt, delimPkt, connEndPkt = {}, {}, {};

local specialPktLineLBT = {
	[0] = flushPkt,
	[1] = delimPkt,
	[2] = connEndPkt,
};

local function
matchPktLine(bin, pos)
	return bin:match("^([0-9a-f][0-9a-f][0-9a-f][0-9a-f])()", pos)
end

local function
parsePktLine(bin)
	-- pos keeps track of the data that we've already decoded. It's
	-- impratical to split the data (actual a string) each time we decode
	-- a part, which leads to O(n^2) complexitiy in time thanks to
	-- immutable strings.
	local npktline, pos = 0, 1;
	local pktlines = {};

	while true do
		local pktlen, newpos = matchPktLine(bin, pos);
		if not newpos then
			break;
		end

		pos = newpos;
		pktlen = tonumber("0x" .. pktlen);
		npktline = npktline + 1;

		local specialPktLine = specialPktLineLBT[pktlen];
		if specialPktLine then
			pktlines[npktline] = specialPktLine;
		else
			pktlen = pktlen - 4;

			local data = bin:sub(pos, pos + pktlen - 1);
			pos = pos + pktlen;

			if #data ~= pktlen then
				local msg = ("invalid pkt-len %d, " ..
					     "data ends at byte %d"):
					    format(pktlen, #data);

				return fmtErr("pkg-line data", msg);
			end

			pktlines[npktline] = data;
		end
	end

	if pos == #bin + 1 then
		return pktlines;
	else
		return fmtErr("pkt-line data", "junk at the end");
	end
end

return {
	flushPkt	= flushPkt,
	delimPkt	= delimPkt,
	connEndPkt	= connEndPkt,
	parsePktLine	= parsePktLine,
       };

]===]));

package.preload["rmpackage"] = assert(load([===[
--[[
--	rollmeow
--	/src/rmpackage.lua
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2025 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local table		= require "table";

local function
alignedFormat(n, k, v)
	return ("%s:%s%s"):format(k, (' '):rep(n - #k - 1), v);
end

local function
prettyPrint(name, pkg)
	local pkgAttrs = { "url", "regex", "note", "follow" };
	local buf = { ("name:\t\t%s"):format(name) };

	for _, attr in ipairs(pkgAttrs) do
		local value = pkg[attr];
		if value then
			table.insert(buf, alignedFormat(16, attr, value));
		end
	end

	return table.concat(buf, '\n') .. '\n';
end

local function
pkgType(pkg)
	local url, gitrepo = pkg.url, pkg.gitrepo;
	local regex, follow = pkg.regex, pkg.follow;

	if url and not gitrepo and regex and not follow then
		return "regex-match";
	elseif not url and gitrepo and regex and not follow then
		return "git";
	elseif not gitrepo and not regex and follow then
		return "batched";
	elseif url and not gitrepo and not regex and not follow then
		return "manual";
	end

	return nil;
end

return {
	prettyPrint	= prettyPrint,
	type		= pkgType,
       };

]===]));

package.preload["sync"] = assert(load([===[
--[[
--	rollmeow
--	/src/sync.lua
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2024 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local string		= require "string";
local table		= require "table";
local rmVersion		= require "version";
local rmHelper		= require "helpers";
local rmGitProto	= require "gitproto";
local rmPackage		= require "rmpackage";

local gmatch, gsub	= string.gmatch, string.gsub;
local insert		= table.insert;

local pcall, fmtErr = pcall, rmHelper.fmtErr;

local function
normalizeRegex(pattern)
	return gsub(pattern, "%-", "%%-");
end

local function
allMatches(s, pattern)
	local pattern1 = normalizeRegex(pattern);
	return gmatch(s, pattern1);
end

local function
syncByGit(fetcher, pkg)
	local url = pkg.gitrepo;
	if url:sub(-1, -1) == '/' then
		url = url:sub(1, -2);
	end

	url = url .. "/git-upload-pack";

	local headers = {
		"Git-Protocol: version=2",
		"Content-Type: application/x-git-upload-pack-request",
	};
	-- ls-refs command, delim packet and flush packet
	local cmd = '0014command=ls-refs\n00010000';

	local ok, content = pcall(fetcher, url, headers, cmd);
	if not ok then
		return fmtErr("fetch function", content);
	end

	if options.showfetched then
		-- It's likely that Git responses don't end with a newline.
		-- Always add one to avoid messing the terminal up.
		print(content .. '\n');
	end

	local pktlines, msg = rmGitProto.parsePktLine(content);
	if not pktlines then
		return false, msg;
	end

	local matches = {};
	local pattern = normalizeRegex(pkg.regex);
	for _, pktline in ipairs(pktlines) do
		if type(pktline) ~= "string" then
			goto continue;
		end

		if pktline:sub(-1, -1) == '\n' then
			pktline = pktline:sub(1, -2);
		end

		local match = pktline:match(pattern);
		if match then
			insert(matches, match);
		end
::continue::
	end

	return matches;
end

local function
syncByRegexMatch(fetcher, pkg)
	local ok, content = pcall(fetcher, pkg.url);
	if not ok then
		return fmtErr("fetch function", content);
	end

	local matches = {};

	if options.showfetched then
		rmHelper.pwarn(content .. '\n');
	end

	for match in allMatches(content, pkg.regex) do
		table.insert(matches, match);
	end

	return matches;
end

local syncImplLUTByType <const> = {
	["git"]			= syncByGit,
	["regex-match"]		= syncByRegexMatch,
};

local function
getSyncImpl(pkg)
	local t = rmPackage.type(pkg);

	return t and syncImplLUTByType[t];
end

local vCmp = rmVersion.cmp;
local function
latestVersion(vers)
	local latest = vers[1];
	for i = 2, #vers do
		if vCmp(vers[i], latest) > 0 then
			latest = vers[i];
		end
	end
	return latest;
end

local vConvert = rmVersion.convert;
local function
sync(fetcher, pkg)
	local syncImpl = getSyncImpl(pkg);
	if not syncImpl then
		return fmtErr("package description", "invalid package type");
	end

	local entries, msg = syncImpl(fetcher, pkg);
	if not entries then
		return false, msg;
	end

	local postMatch, filter = pkg.postMatch, pkg.filter;
	local vers = {};
	for _, match in ipairs(entries) do
		if options.showmatch then
			rmHelper.pwarn(match .. "\n");
		end

		if postMatch then
			local ok, ret = pcall(postMatch, match);
			if not ok then
				return fmtErr("postMatch hook", ret);
			end

			if type(ret) ~= "string" then
				return false,
				  "postMatch hook returns a " .. type(ret);
			end

			match = ret;
		end

		local ver = vConvert(match);
		local valid = true;
		if filter then
			local ok, ret = pcall(filter, ver);

			if not ok then
				return fmtErr("filter hook", ret);
			end

			valid = ret;
		end

		if valid then
			insert(vers, ver);
		end
	end

	if #vers == 0 then
		return false, "no valid match in upstream";
	end

	return true, latestVersion(vers);
end

return {
	sync = sync,
       };

]===]));

package.preload["fetcher"] = assert(load([===[
--[[
--	rollmeow
--	/src/fetcher.lua
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2024-2025 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local coroutine			= require "coroutine";
local string			= require "string";
local table			= require "table";

local cURL			= require "cURL";

local rmHelpers			= require "helpers"

local yield, easy	= coroutine.yield, cURL.easy;
local insert, remove	= table.insert, table.remove;
local concat		= table.concat;
local verbosef		= rmHelpers.verbosef;

local function
fetcher(url, headers, body)
	local ok, data = coroutine.yield(url, headers, body);

	if not ok then
		error(data);
	else
		return data;
	end
end

local useragent = ("curl/%s (Rollmeow)"):
		  format(cURL.version_info("version"));

local function
readfunc(ctx, size)
	if not ctx.bodyWritten then
		ctx.bodyWritten = true;
		return ctx.body;
	end
end

-- TODO: make it configurable
local function
createHandleWithOpt(data)
	local handle = easy {
				url = data.url,
				httpheader = data.headers,
				[cURL.OPT_TIMEOUT]		= 10,
				[cURL.OPT_LOW_SPEED_LIMIT]	= 10,
				[cURL.OPT_LOW_SPEED_TIME]	= 10,
				[cURL.OPT_FOLLOWLOCATION]	= true,
				[cURL.OPT_USERAGENT]		= useragent,
			    };

	if data.body then
		handle:setopt(cURL.OPT_POST, true);
		handle:setopt_readfunction(readfunc, data);
	end

	handle.data = data;

	return handle;
end

local function
createConn(f, item)
	local co = coroutine.wrap(f);
	local url, headers, body = co(fetcher, item);

	if not url then
		return nil;
	end

	local data = {
			co	= co,
			buf	= {},
			retry	= 0,
			url	= url,
			headers	= headers,
			body	= body,
		     };

	local handle = createHandleWithOpt(data);

	return handle;
end

local function
nextConn(f, list)
	while list[1] do
		local handle = createConn(f, list[#list]);
		table.remove(list);
		if handle then
			return handle;
		end
	end

	return nil;
end

-- XXX: multi:iperform() doesn't provide a valid iterator in case that no easy
-- handle has been added to the multi instance, thus a for-loop may fail with
-- "attempt to call a nil value". I consider it's a mistake of API design.
-- We provide a wrapper to handle the edge case.
local function
wrapIperform(multi)
	local res = { multi:iperform() };

	if res[1] then
		return table.unpack(res);
	else
		-- End the loop at the first iteration.
		return function(x) return nil; end;
	end
end

-- TODO: make retry configurable
local function
forEach(connections, f, originList)
	local list = {};
	for i = #originList, 1, -1 do
		list[#originList - i + 1] = originList[i];
	end

	local multi = cURL.multi();
	for i = 1, connections do
		local handle = nextConn(f, list);
		if not handle then
			break;
		end

		multi:add_handle(handle);
	end

	for data, type, handle in wrapIperform(multi) do
		local p = handle.data;
		local newConn = false;

		if type == "error" then
			if p.retry < 3 then
				p.retry = p.retry + 1;
				p.bodyWritten = false;

				verbosef("%s: fetch failed, retry %d",
					 p.url, p.retry);

				handle = createHandleWithOpt(p);

				multi:add_handle(handle);
			else
				p.co(false, tostring(data));
				newConn = true;

				handle.data = nil;
			end
		elseif type == "done" then
			p.co(true, concat(p.buf));
			newConn = true;

			handle.data = nil;
		elseif type == "data" then
			insert(p.buf, data);
		end

		if newConn then
			local handle = nextConn(f, list);
			if handle then
				multi:add_handle(handle);
			end
		end
	end
end

return {
	forEach = forEach,
       };

]===]));

package.preload["helpers"] = assert(load([===[
--[[
--	rollmeow
--	/src/helpers.lua
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2024 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local io		= require "io";
local string		= require "string";

local function
pwarn(msg)
	io.stderr:write(msg .. "\n");                        end

local function
perr(msg)
	pwarn(msg);
	os.exit(-1);
end

local function
perrf(msg, ...)
	perr(string.format(msg, ...));
end

local function
pwarnf(msg, ...)
	pwarn(string.format(msg, ...));
end

local function
validateTable(expect, obj)
	if type(obj) ~= "table" then
		return false, "not a table";
	end

	for k, constraints in pairs(expect) do
		local v = obj[k];
		if not v then
			if constraints.optional then
				goto continue;
			end

			return false, "missing field " .. k;
		end

		if type(v) ~= constraints.type then
			return false,
				("type mismatch for %s: expect %s, got %s"):
				format(k, constraints.type, type(v));
		end
::continue::
	end

	return true;
end

local function
fmtErr(place, err)
	return false, ("error in %s: %s"):format(place, tostring(err));
end

local isVerbose = false;

local function
setVerbose(on)
	isVerbose = on;
end

local function
verbose(msg)
	if isVerbose then
		pwarnf(msg);
	end
end

local function
verbosef(msg, ...)
	verbose(string.format(msg, ...));
end

return {
	setVerbose	= setVerbose;
	pwarn		= pwarn,
	perr		= perr,
	pwarnf		= pwarnf,
	perrf		= perrf,
	verbose		= verbose,
	verbosef	= verbosef,
	validateTable	= validateTable,
	fmtErr		= fmtErr,
       };

]===]));

package.preload["cache"] = assert(load([===[
--[[
--	rollmeow
--	/src/cache.lua
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2024 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local io		= require "io";
local string		= require "string";

local rmHelpers		= require "helpers";
local rmVersion		= require "version";

local fmtErr, pwarnf = rmHelpers.fmtErr, rmHelpers.pwarnf;

local function
recreateCache(path)
	local cacheFile, msg = io.open(path, "w");
	if not cacheFile then
		return fmtErrmsg("creating cache file", msg);
	end

	cacheFile:write("local v = {};\n");
	cacheFile:close();

	return true;
end

local cacheMeta = {};
cacheMeta.__index = cacheMeta;

local function
Cache(path)
	local cache = { path = path, news = {}, deleted = {} };
	local cacheFile, msg = io.open(path, "r");
	local recreate = true;

	if cacheFile then
		local rawCache = cacheFile:read("a") .. "return v";
		local cacheF, msg = load(rawCache);
		if not cacheF then
			return fmtErr("loading cache", msg);
		end

		local ok, ret = pcall(cacheF);
		if not ok then
			return fmtErr("loading cache", msg);
		end

		if not ret or type(ret) ~= "table" then
			pwarnf("Invalid cache file, recreating...");
			goto recreate;
		end

		cache.vers = ret;
		recreate = false;
	end

::recreate::
	if recreate then
		local ok, msg = recreateCache(path);
		if not ok then
			return false, msg;
		end
		cache.vers = {};
	end

	return setmetatable(cache, cacheMeta);
end

local function
serializeVer(v)
	local s = '"' .. v[1] .. '"';
	for i = 2, #v do
		s = s .. ',"' .. v[i] .. '"';
	end
	return s;
end

cacheMeta.flush = function(cache)
	local cacheF, msg = io.open(cache.path, "a");
	if not cacheF then
		return fmtErr("flushing package cache", msg);
	end

	for pkg, ver in pairs(cache.news) do
		cacheF:write(("v[%q]={%s}\n"):format(pkg, serializeVer(ver)));
	end

	for pkg, _ in pairs(cache.deleted) do
		cacheF:write(("v[%q]=nil\n"):format(pkg));
	end

	cacheF:close();
	return true;
end

local vCmp = rmVersion.cmp;
cacheMeta.update = function(cache, pkgname, ver)
	local old = cache.vers[pkgname];
	if old and vCmp(old, ver) == 0 then
		return;
	end
	cache.news[pkgname] = ver;
end

cacheMeta.delete = function(cache, pkgname)
	cache.deleted[pkgname] = true;
end

cacheMeta.query = function(cache, pkgname)
	if cache.deleted[pkgname] then
		return nil;
	end

	local ver = cache.news[pkgname];
	if ver then
		return ver;
	end

	return cache.vers[pkgname]
end

return {
	Cache = Cache,
       };

]===]));

package.preload["version"] = assert(load([===[
--[[
--	rollmeow
--	/src/version.lua
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2024 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information.
--]]

local math		= require "math";
local string		= require "string";

local min = math.min;

local function
cmp(v1, v2)
	for i = 1, min(#v1, #v2) do
		local a, b = v1[i], v2[i];
		local a1, b1 = tonumber(a), tonumber(b);

		if a == b then
			goto continue;
		elseif a1 and b1 then
			return a1 > b1 and 1 or -1;
		elseif a1 then
			return 1;
		elseif b1 then
			return -1;
		else
			return a > b and 1 or -1;
		end
::continue::
	end

	return #v1 == #v2 and 0 or
	       #v1 >  #v2 and 1 or
	       #v1 <  #v2 and -1;
end

local gmatch = string.gmatch;
local function
convert(s)
	local r = { "" };
	local i = 1;
	for vs in gmatch(s, "[^%.]+") do
		r[i] = vs;
		i = i + 1;
	end
	return r;
end

local function
verString(v)
	local s = tostring(v[1]);
	for i = 2, #v do
		s = s .. '.' .. tostring(v[i]);
	end
	return s;
end

return {
	cmp		= cmp,
	convert		= convert,
	verString	= verString,
       };

]===]));

--[[
--	rollmeow
--	SPDX-License-Identifier: MPL-2.0
--	Copyright (c) 2024-2025 eweOS developers. All rights reserved.
--	Refer to https://os.ewe.moe/ for more information
--]]

local io		= require "io";
local os		= require "os";
local string		= require "string";

local rmHelpers		= require "helpers";
local rmVersion		= require "version";
local rmSync		= require "sync";
local rmCache		= require "cache";
local rmFetcher		= require "fetcher";
local rmPackage		= require "rmpackage";

local pwarn, perr	= rmHelpers.pwarn, rmHelpers.perr;
local pwarnf, perrf	= rmHelpers.pwarnf, rmHelpers.perrf;
local verbose, verbosef	= rmHelpers.verbose, rmHelpers.verbosef;

local function
safeDoFile(path)
	local f, msg = io.open(path, "r");
	if not f then
		perrf("Cannot load configuration %s:\n%s", path, msg);
	end

	local env = {};
	env._G = env;
	local fcfg, msg = load(f:read("a"), path, "t",
			       setmetatable(env, { __index = _G }));
	if not fcfg then
		perrf("Cannot parse configuration %s:\n%s", path, msg);
	end

	local ok, ret = pcall(fcfg);
	if not ok then
		perrf("Cannot eval configuration %s:\n%s", path, ret);
	end

	return ret;
end

local function
printHelp()
	io.stderr:write(
[==[
Usage: rollmeow [options] [PKGNAME1] [PKGNAME2] ...
]==]);
end

-- global options
options = {
	sync		= false,
	diff		= false,
	json		= false,
	help		= false,
	verbose		= false,
	showfetched	= false,
	showmatch	= false,
	info		= false,
	manual		= false,
	conf	= os.getenv("HOME") .. "/.config/rollmeow/rollmeow.cfg.lua",
};
local i, pkgs = 1, {};
while i <= #arg do
	local s = arg[i];
	if s:sub(1, 2) == "--" then
		s = s:sub(3, -1);	-- strip "--"

		local v = options[s];
		if v == nil then
			perrf("Unknown option %s", s);
		end

		if type(v) == "boolean" then
			options[s] = not v;
		elseif type(v) == "string" then
			if i + 1 > #arg then
				perrf("Option %s requires an argument", s);
			end
			i = i + 1;
			options[s] = arg[i];
		end
	else
		table.insert(pkgs, arg[i]);
	end
	i = i + 1;
end

if options.help then
	printHelp();
	os.exit(0);
end

if options.verbose then
	rmHelpers.setVerbose(options.verbose);
end

local conf = safeDoFile(options.conf);
local confFormat = {
	evalDownstream	= { type = "function" },
	cachePath	= { type = "string" },
	packages	= { type = "table" },
	connections	= { type = "number", optional = true },
	timeout		= { type = "numner", optional = true },
};
local ok, msg = rmHelpers.validateTable(confFormat, conf);
if not ok then
	perrf("Invalid configuration: %s", msg);
end

if conf.fetchUpstream then
	pwarn "From 0.2.0, rollmeow drops `fetchUpstream` in configuration.";
	pwarn "Use default concurrent fetcher instead.";
end

local cache, msg = rmCache.Cache(conf.cachePath);
if not cache then
	perr(msg);
end

for name, pkg in pairs(conf.packages) do
	local t = rmPackage.type(pkg);
	if not t then
		perrf("Invalid type for package %s", name);
	end

	-- Validate the followed package exists
	local follow = pkg.follow;
	if follow then
		if not conf.packages[follow] then
			perrf("Invalid package %s: followed package %s " ..
			      "doesn't exist", name, follow);
		end
	end
end

local evalDownstrean = conf.evalDownstream;

local function
doSync(fetcher, name)
	local pkg = conf.packages[name];
	if not pkg then
		perrf("%s: not found", name);
	end

	local t = rmPackage.type(pkg);
	if t ~= "regex-match" and t ~= "git" then
		return;
	end

	verbosef("syncing %s...", name);

	local ok, ret = rmSync.sync(fetcher, pkg);
	if not ok then
		pwarnf("%s: failed to sync: %s",
		       name, ret or "no information");
		cache:delete(name);
		return;
	end

	cache:update(name, ret);
end

local function
jsonVer(ver)
	local v = "[ " .. ("%q"):format(ver[1]);
	for i = 2, #ver do
		v = v .. ", " .. ("%q"):format(ver[i]);
	end

	return v .. " ]";
end

local function
pkgJSON(status, name, up, down, note)
	local statusStr = status == 0 and "ok" or "outofdate";
	local downStr = jsonVer(down);
	local upStr = up == "MANUAL" and ("%q"):format(up) or jsonVer(up);
	local noteStr = note ~= nil and (', "note": %q '):format(note) or "";
	return
	  ('{ "name": %q, "status": %q, "upstream": %s, "downstream": %s %s}'):
	       format(name, statusStr, upStr, downStr, noteStr);
end

local function
reportPkg(status, name, up, down, note)
	if options.json then
		return pkgJSON(status, name, up, down, note);
	else
		local upStr = up == "MANUAL" and up or rmVersion.verString(up);
		local downStr = rmVersion.verString(down);
		return ("%s%s: upstream %s | downstream %s"):
		       format(name, note ~= nil and "[!]" or "",
		              upStr, downStr);
	end
end

local function
jsonFailed(name, reason)
	return ('{ "name": %q, "status": "failed", "reason": %q }'):
	       format(name, reason);
end

local function
doReport(name)
	local pkg = conf.packages[name];
	if not pkg then
		perrf("%s: not found", name);
	end

	local t = rmPackage.type(pkg);

	if t == "manual" and not options.manual then
		return;
	end

	local ok, downStr = pcall(conf.evalDownstream, name);
	if not downStr then
		if not options.json then
			pwarnf("%s: failed to eval downstream version: %s",
			       name, downVer);
			return;
		else
			return jsonFailed(name, "failed to eval downstream");
		end
	end

	local downVer = rmVersion.convert(downStr);

	local upVer, status = "MANUAL", 1;
	if t ~= "manual" then
		upVer = cache:query(name);
		if not upVer then
			if not options.json then
				pwarnf("%s: not cached", name);
				return;
			else
				return jsonFailed(name, "not cached");
			end
		end

		status = rmVersion.cmp(downVer, upVer);
		if options.diff and status == 0 then
			return;
		end
	end

	return reportPkg(status, name, upVer, downVer, pkg.note);
end

if options.info then
	if #pkgs == 0 then
		perrf("option --info must come with package names");
	end

	for i, pkgname in ipairs(pkgs) do
		if i ~= 1 then
			io.stdout:write('\n');
		end

		local pkg = conf.packages[pkgname];
		if not pkg then
			perrf("%s: not found", name);
		end

		io.stdout:write(rmPackage.prettyPrint(pkgname, pkg));
	end

	os.exit(0);
end

--[[	enumerate all packages	]]
if #pkgs == 0 then
	for name, _ in pairs(conf.packages) do
		table.insert(pkgs, name);
	end
	table.sort(pkgs);
end

local function
syncBatchedPkgs(pkgs)
	for _, name in ipairs(pkgs) do
		local follow = conf.packages[name].follow;

		if not follow then
			goto continue;
		end

		local ver = cache:query(follow);
		if not ver then
			pwarnf("%s: failed to sync: package %s isn't cached",
			       name, follow);
			goto continue;
		end

		cache:update(name, ver);
::continue::
	end
end

if options.sync then
	rmFetcher.forEach(conf.connections or 8, doSync, pkgs);

	syncBatchedPkgs(pkgs);

	local ok, ret = cache:flush();
	if not ok then
		pwarn(ret);
	end
end

local output = {};
for _, pkg in ipairs(pkgs) do
	local s = doReport(pkg);
	if s then
		if options.json then
			table.insert(output, s);
		else
			print(s);
		end
	end
end

if options.json then
	print("[");
	print(table.concat(output, ",\n"));
	print("]");
end
