249 lines
5.8 KiB
JavaScript
249 lines
5.8 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { access, mkdir, open, writeFile, readFile } from "node:fs/promises";
|
|
import { constants as fsConstants } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
|
|
import { config as loadDotenv } from "dotenv";
|
|
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
if (!args.repoName) {
|
|
console.error("Missing required --repo-name argument.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const voidbotRoot = resolve(args.voidbotRoot ?? process.cwd());
|
|
loadDotenv({ path: join(voidbotRoot, ".env") });
|
|
const storageRoot = resolve(
|
|
voidbotRoot,
|
|
process.env.STORAGE_ROOT && process.env.STORAGE_ROOT.trim().length > 0
|
|
? process.env.STORAGE_ROOT
|
|
: ".voidbot",
|
|
);
|
|
const safeRepoName = encodeURIComponent(args.repoName);
|
|
const logDir = join(storageRoot, "logs", "source-hooks");
|
|
const statusDir = join(storageRoot, "status", "source-hooks");
|
|
const logPath = join(logDir, `${safeRepoName}.log`);
|
|
const statusPath = join(statusDir, `${safeRepoName}.json`);
|
|
|
|
if (args.run) {
|
|
await runWorker();
|
|
} else {
|
|
await launchDetachedWorker();
|
|
}
|
|
|
|
async function launchDetachedWorker() {
|
|
await ensureDirectories();
|
|
|
|
const currentStatus = await readJson(statusPath);
|
|
|
|
if (typeof currentStatus?.pid === "number" && isProcessRunning(currentStatus.pid)) {
|
|
await writeStatus({
|
|
...currentStatus,
|
|
repoName: args.repoName,
|
|
logPath,
|
|
lastSkippedAt: new Date().toISOString(),
|
|
lastSkipReason: "already_running",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const logHandle = await open(logPath, "a");
|
|
const child = spawn(
|
|
process.execPath,
|
|
[
|
|
join(voidbotRoot, "scripts", "git-post-push-index.mjs"),
|
|
"--run",
|
|
"--voidbot-root",
|
|
voidbotRoot,
|
|
"--repo-name",
|
|
args.repoName,
|
|
],
|
|
{
|
|
cwd: voidbotRoot,
|
|
detached: true,
|
|
stdio: ["ignore", logHandle.fd, logHandle.fd],
|
|
windowsHide: true,
|
|
env: process.env,
|
|
},
|
|
);
|
|
|
|
child.unref();
|
|
await logHandle.close();
|
|
|
|
await writeStatus({
|
|
state: "running",
|
|
repoName: args.repoName,
|
|
pid: child.pid,
|
|
queuedAt: new Date().toISOString(),
|
|
logPath,
|
|
});
|
|
}
|
|
|
|
async function runWorker() {
|
|
await ensureDirectories();
|
|
|
|
const sourceRepoRoot = process.env.SOURCE_REPO_ROOT
|
|
? resolve(process.env.SOURCE_REPO_ROOT)
|
|
: undefined;
|
|
const repoPath = undefined;
|
|
const startedAt = new Date().toISOString();
|
|
|
|
await writeStatus({
|
|
state: "running",
|
|
repoName: args.repoName,
|
|
pid: process.pid,
|
|
startedAt,
|
|
logPath,
|
|
repoPath,
|
|
});
|
|
|
|
process.stdout.write(`\n=== source hook run ${startedAt} repo=${args.repoName} ===\n`);
|
|
|
|
if (!sourceRepoRoot) {
|
|
const message = "SOURCE_REPO_ROOT is not configured in VoidBot's .env.";
|
|
process.stderr.write(`${message}\n`);
|
|
await writeStatus({
|
|
state: "failed",
|
|
repoName: args.repoName,
|
|
pid: process.pid,
|
|
startedAt,
|
|
completedAt: new Date().toISOString(),
|
|
exitCode: 1,
|
|
logPath,
|
|
error: message,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const tsxCliPath = join(voidbotRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
if (!(await pathExists(tsxCliPath))) {
|
|
throw new Error(
|
|
`VoidBot's repo-local tsx runtime is missing at ${tsxCliPath}; run npm ci in ${voidbotRoot} before retrying source indexing.`,
|
|
);
|
|
}
|
|
|
|
const child = spawn(
|
|
process.execPath,
|
|
[tsxCliPath, join(voidbotRoot, "scripts", "index-source-repos.ts"), "--repo", args.repoName],
|
|
{
|
|
cwd: voidbotRoot,
|
|
windowsHide: true,
|
|
env: process.env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
},
|
|
);
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
process.stdout.write(chunk);
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
process.stderr.write(chunk);
|
|
});
|
|
|
|
const exitCode = await new Promise((resolveExit, reject) => {
|
|
child.on("error", reject);
|
|
child.on("close", (code) => resolveExit(code ?? 1));
|
|
});
|
|
|
|
await writeStatus({
|
|
state: exitCode === 0 ? "completed" : "failed",
|
|
repoName: args.repoName,
|
|
pid: process.pid,
|
|
startedAt,
|
|
completedAt: new Date().toISOString(),
|
|
exitCode,
|
|
logPath,
|
|
repoPath,
|
|
});
|
|
|
|
process.exit(exitCode);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`${message}\n`);
|
|
await writeStatus({
|
|
state: "failed",
|
|
repoName: args.repoName,
|
|
pid: process.pid,
|
|
startedAt,
|
|
completedAt: new Date().toISOString(),
|
|
exitCode: 1,
|
|
logPath,
|
|
repoPath,
|
|
error: message,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function ensureDirectories() {
|
|
await mkdir(logDir, { recursive: true });
|
|
await mkdir(statusDir, { recursive: true });
|
|
}
|
|
|
|
async function writeStatus(status) {
|
|
await writeFile(statusPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
async function readJson(path) {
|
|
try {
|
|
const content = await readFile(path, "utf8");
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function isProcessRunning(pid) {
|
|
if (typeof pid !== "number" || !Number.isFinite(pid) || pid <= 0) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function pathExists(path) {
|
|
try {
|
|
await access(path, fsConstants.F_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const parsed = {
|
|
run: false,
|
|
voidbotRoot: undefined,
|
|
repoName: undefined,
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const argument = argv[index];
|
|
|
|
if (argument === "--run") {
|
|
parsed.run = true;
|
|
continue;
|
|
}
|
|
|
|
if (argument === "--voidbot-root" && argv[index + 1]) {
|
|
parsed.voidbotRoot = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (argument === "--repo-name" && argv[index + 1]) {
|
|
parsed.repoName = argv[index + 1];
|
|
index += 1;
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|