"use strict";
const { spawnSync } = require("child_process");
const fs = require("fs");
const { createWriteStream, chmodSync, readFileSync } = fs;
const { join } = require("path");
const { tmpdir } = require("os");
const https = require("https");
const os = require("os");
const CONNECT_TIMEOUT_MS = 10_000;
const DOWNLOAD_TIMEOUT_MS = 60_000;
const INSTALL_TIMEOUT_MS = 300_000;
const REPO = "epicsagas/research-agent";
const BINARY = "research";
const INSTALLER_SH = `https://github.com/${REPO}/releases/latest/download/install.sh`;
const INSTALLER_PS1 = `https://github.com/${REPO}/releases/latest/download/install.ps1`;
function log(msg) {
process.stderr.write(`[research plugin] ${msg}\n`);
}
function hasBinary() {
const r = spawnSync(BINARY, ["--help"], { stdio: "pipe", shell: false });
return r.status === 0;
}
function getBinaryVersion() {
try {
const r = spawnSync(BINARY, ["--version"], { stdio: "pipe", shell: false });
if (r.status === 0 || (r.stdout && r.stdout.length)) {
const out = (r.stdout ? r.stdout.toString() : "") + (r.stderr ? r.stderr.toString() : "");
const m = out.match(/(\d+\.\d+\.\d+)/);
return m ? m[1] : null;
}
} catch (_) {}
return null;
}
function getPluginVersion() {
const root = process.env.CLAUDE_PLUGIN_ROOT || process.env.GROK_PLUGIN_ROOT || "";
for (const rel of [
join(".claude-plugin", "plugin.json"),
join(".grok-plugin", "plugin.json"),
]) {
try {
return JSON.parse(readFileSync(join(root, rel), "utf8")).version || null;
} catch (_) {}
}
return null;
}
function semverGt(a, b) {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i++) {
if (pa[i] > pb[i]) return true;
if (pa[i] < pb[i]) return false;
}
return false;
}
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
const follow = (u, depth) => {
if (depth > 5) {
reject(new Error(`too many redirects for ${url}`));
return;
}
const req = https.get(u, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
const location = res.headers.location;
res.resume();
if (!location) {
reject(new Error(`redirect without Location for ${u}`));
return;
}
follow(location, depth + 1);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode} for ${u}`));
return;
}
res.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
req.destroy(new Error(`download timed out for ${u}`));
});
res.pipe(file);
file.on("finish", () => file.close(resolve));
});
req.setTimeout(CONNECT_TIMEOUT_MS, () => {
req.destroy(new Error(`connect timed out for ${u}`));
});
req.on("error", (e) => {
file.close(() => reject(e));
});
file.on("error", reject);
};
follow(url, 0);
});
}
async function install() {
const dir = fs.mkdtempSync(join(tmpdir(), "research-install-"));
try {
if (os.platform() === "win32") {
const tmp = join(dir, "installer.ps1");
log("Downloading Windows installer...");
await downloadFile(INSTALLER_PS1, tmp);
const r = spawnSync(
"powershell",
["-ExecutionPolicy", "Bypass", "-File", tmp],
{ stdio: "inherit", timeout: INSTALL_TIMEOUT_MS }
);
if (r.status !== 0) throw new Error("PowerShell installer failed");
} else {
const tmp = join(dir, "installer.sh");
log("Downloading installer...");
await downloadFile(INSTALLER_SH, tmp);
chmodSync(tmp, 0o755);
const r = spawnSync("sh", [tmp], { stdio: "inherit", timeout: INSTALL_TIMEOUT_MS });
if (r.status !== 0) throw new Error("Shell installer failed");
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
async function main() {
if (!hasBinary()) {
log(`${BINARY} not found — installing...`);
try {
await install();
} catch (e) {
log(`Install failed: ${e.message}`);
log(`Install manually: https://github.com/${REPO}#installation`);
process.exit(0); }
return;
}
const pluginVersion = getPluginVersion();
if (pluginVersion) {
const binaryVersion = getBinaryVersion();
if (binaryVersion && semverGt(pluginVersion, binaryVersion)) {
log(`Updating ${BINARY} ${binaryVersion} → ${pluginVersion}...`);
try {
await install();
const now = getBinaryVersion();
if (now) log(`Updated to ${now}`);
} catch (e) {
log(`Update failed: ${e.message} — continuing with ${binaryVersion}`);
}
}
}
}
main().catch((e) => {
log(`Unexpected error: ${e.message}`);
process.exit(0); });