import { spawn } from "node:child_process"; function runProcess(command, argv, { env, cwd }) { return new Promise((resolve, reject) => { const child = spawn(command, argv, { env, cwd, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (d) => { stdout += d; }); child.stderr.on("data", (d) => { stderr += d; }); child.on("error", (err: any) => { if (err?.code === "ENOENT") { reject(new Error("gh not found on PATH (install GitHub CLI)")); return; } reject(err); }); child.on("close", (code) => { if (code === 0) return resolve({ stdout, stderr }); reject(new Error(`gh failed (${code}): ${stderr.trim() || stdout.trim()}`)); }); });} import { diffAndStore } from "../state/store.js"; function pickSubset(snapshot) { if (!snapshot || typeof snapshot !== "object") return null; return { number: snapshot.number, title: snapshot.title, url: snapshot.url, state: snapshot.state, isDraft: snapshot.isDraft, mergeable: snapshot.mergeable, reviewDecision: snapshot.reviewDecision, updatedAt: snapshot.updatedAt, baseRefName: snapshot.baseRefName, headRefName: snapshot.headRefName, };} export function buildPrChangeSummary(before, after) { const a = pickSubset(after); const b = pickSubset(before); if (!a) return { changedFields: [], changes: {} }; if (!b) { return { changedFields: Object.keys(a), changes: Object.fromEntries(Object.keys(a).map((k) => [k, { from: null, to: a[k] }])), }; } const changes = {}; for (const key of Object.keys(a)) { if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) { changes[key] = { from: b[key], to: a[key] }; } } return { changedFields: Object.keys(changes), changes, };} function formatPrChangeMessage({ repo, pr, changedFields, prInfo }) { const fields = changedFields.length ? ` (${changedFields.join(", ")})` : ""; const title = prInfo?.title ? `: ${prInfo.title}` : ""; const url = prInfo?.url ? ` ${prInfo.url}` : ""; return `PR updated: ${repo}#${pr}${title}${fields}.${url}`.replace(/\s+/g, " ").trim();} export async function runGithubPrMonitorWorkflow({ args, ctx }) { const repo = args.repo; const pr = args.pr; if (!repo || !pr) throw new Error("github.pr.monitor requires args.repo and args.pr"); const key = args.key ?? `github.pr:${repo}#${pr}`; const changesOnly = Boolean(args.changesOnly); const summaryOnly = Boolean(args.summaryOnly); const argv = [ "pr", "view", String(pr), "--repo", String(repo), "--json", "number,title,url,state,isDraft,mergeable,reviewDecision,author,baseRefName,headRefName,updatedAt", ]; const { stdout } = (await runProcess("gh", argv, { env: ctx.env, cwd: process.cwd() })) as any; let current; try { current = JSON.parse(stdout.trim()); } catch { throw new Error("gh returned non-JSON output"); } const { changed, before } = await diffAndStore({ env: ctx.env, key, value: current }); if (changesOnly && !changed) { return { kind: "github.pr.monitor", repo, prNumber: Number(pr), key, changed: false, suppressed: true, }; } const summary = buildPrChangeSummary(before, current); if (summaryOnly) { return { kind: "github.pr.monitor", repo, prNumber: Number(pr), key, changed, summary, pr: { number: current.number, title: current.title, url: current.url, state: current.state, updatedAt: current.updatedAt, }, }; } return { kind: "github.pr.monitor", repo, prNumber: Number(pr), key, changed, summary, prSnapshot: current, };} export async function runGithubPrMonitorNotifyWorkflow({ args, ctx }) { const base = await runGithubPrMonitorWorkflow({ args: { ...args, changesOnly: true, summaryOnly: true, }, ctx, }); if (base.suppressed) { return { kind: "github.pr.monitor.notify", suppressed: true }; } const changedFields = base.summary?.changedFields ?? []; const prInfo = base.pr ?? {}; return { kind: "github.pr.monitor.notify", changed: Boolean(base.changed), repo: args.repo, prNumber: Number(args.pr), message: formatPrChangeMessage({ repo: args.repo, pr: Number(args.pr), changedFields, prInfo, }), pr: prInfo, summary: base.summary, };}
import { spawn } from "node:child_process";
function runProcess(command, argv, { env, cwd }) {
return new Promise((resolve, reject) => {
const child = spawn(command, argv, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (d) => {
stdout += d;
});
child.stderr.on("data", (d) => {
stderr += d;
});
child.on("error", (err: any) => {
if (err?.code === "ENOENT") {
reject(new Error("gh not found on PATH (install GitHub CLI)"));
return;
}
reject(err);
});
child.on("close", (code) => {
if (code === 0) return resolve({ stdout, stderr });
reject(new Error(`gh failed (${code}): ${stderr.trim() || stdout.trim()}`));
});
});
}
import { diffAndStore } from "../state/store.js";
function pickSubset(snapshot) {
if (!snapshot || typeof snapshot !== "object") return null;
return {
number: snapshot.number,
title: snapshot.title,
url: snapshot.url,
state: snapshot.state,
isDraft: snapshot.isDraft,
mergeable: snapshot.mergeable,
reviewDecision: snapshot.reviewDecision,
updatedAt: snapshot.updatedAt,
baseRefName: snapshot.baseRefName,
headRefName: snapshot.headRefName,
};
}
export function buildPrChangeSummary(before, after) {
const a = pickSubset(after);
const b = pickSubset(before);
if (!a) return { changedFields: [], changes: {} };
if (!b) {
return {
changedFields: Object.keys(a),
changes: Object.fromEntries(Object.keys(a).map((k) => [k, { from: null, to: a[k] }])),
};
}
const changes = {};
for (const key of Object.keys(a)) {
if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) {
changes[key] = { from: b[key], to: a[key] };
}
}
return {
changedFields: Object.keys(changes),
changes,
};
}
function formatPrChangeMessage({ repo, pr, changedFields, prInfo }) {
const fields = changedFields.length ? ` (${changedFields.join(", ")})` : "";
const title = prInfo?.title ? `: ${prInfo.title}` : "";
const url = prInfo?.url ? ` ${prInfo.url}` : "";
return `PR updated: ${repo}#${pr}${title}${fields}.${url}`.replace(/\s+/g, " ").trim();
}
export async function runGithubPrMonitorWorkflow({ args, ctx }) {
const repo = args.repo;
const pr = args.pr;
if (!repo || !pr) throw new Error("github.pr.monitor requires args.repo and args.pr");
const key = args.key ?? `github.pr:${repo}#${pr}`;
const changesOnly = Boolean(args.changesOnly);
const summaryOnly = Boolean(args.summaryOnly);
const argv = [
"pr",
"view",
String(pr),
"--repo",
String(repo),
"--json",
"number,title,url,state,isDraft,mergeable,reviewDecision,author,baseRefName,headRefName,updatedAt",
];
const { stdout } = (await runProcess("gh", argv, { env: ctx.env, cwd: process.cwd() })) as any;
let current;
try {
current = JSON.parse(stdout.trim());
} catch {
throw new Error("gh returned non-JSON output");
}
const { changed, before } = await diffAndStore({ env: ctx.env, key, value: current });
if (changesOnly && !changed) {
return {
kind: "github.pr.monitor",
repo,
prNumber: Number(pr),
key,
changed: false,
suppressed: true,
};
}
const summary = buildPrChangeSummary(before, current);
if (summaryOnly) {
return {
kind: "github.pr.monitor",
repo,
prNumber: Number(pr),
key,
changed,
summary,
pr: {
number: current.number,
title: current.title,
url: current.url,
state: current.state,
updatedAt: current.updatedAt,
},
};
}
return {
kind: "github.pr.monitor",
repo,
prNumber: Number(pr),
key,
changed,
summary,
prSnapshot: current,
};
}
export async function runGithubPrMonitorNotifyWorkflow({ args, ctx }) {
const base = await runGithubPrMonitorWorkflow({
args: {
...args,
changesOnly: true,
summaryOnly: true,
},
ctx,
});
if (base.suppressed) {
return { kind: "github.pr.monitor.notify", suppressed: true };
}
const changedFields = base.summary?.changedFields ?? [];
const prInfo = base.pr ?? {};
return {
kind: "github.pr.monitor.notify",
changed: Boolean(base.changed),
repo: args.repo,
prNumber: Number(args.pr),
message: formatPrChangeMessage({
repo: args.repo,
pr: Number(args.pr),
changedFields,
prInfo,
}),
pr: prInfo,
summary: base.summary,
};
}