mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
Adds `tools/pr` as the maintainer-only control plane for PR-duty work on this repo. Thin `gh` wrapper that encodes repo-specific knowledge: review lanes, forbidden surfaces, lane-specific checklists, validation command derivation from touched packages. Subcommands: - `list` — triage open queue by lane and review-state bucket. - `view <num>` — agent-friendly review brief for a single PR. - `classify [num]` — emit script-level tags for one PR or the whole open queue; full-queue JSON output lands under `.tmp/tools-pr/classify/` with rate-limit telemetry per run. - `assignment` — assigner-perspective view of PR ownership, idle time, and blockers (derived from existing tags; no new judgments). Tag dictionary (13 tags) covers: bot-only-approval, needs-rebase, forbidden-surface, unlabeled, duplicate-title, non-ascii-slug, maintainer-edits-disabled, org-member, unresolved-changes-requested, stale-approval, and three awaiting-* timing tags. Each rule is expressible as one factual sentence over `gh` data + repo paths — see `tools/pr/AGENTS.md` for the full dictionary plus precision rules. Templates in `tools/pr/templates/*.md` are aesthetic references for recurring maintainer comments (duplicate-title ask, awaiting-author nudge, agent-review brief shape). `templates/examples/` holds frozen-in-time agent-review snapshots for three PR shapes. Infrastructure: - `gh()` wraps `execFile` with minimum-touch retry (2 attempts at 1s + 2s backoff) on transient 5xx / network errors. Persistent failures still surface — retry is anti-jitter, not an exponential-backoff resilience layer. - Heavy chunks (`reviews`, `comments`, `commits`, assignment timelines) use cursor-paginated `gh api graphql` via `fetchPaginatedPrList` to stay under GitHub's GraphQL server-side timeout. Light chunks stay on `gh pr list --json`. - `fetchOrgMembers` cached per process via `gh api orgs/<owner>/members --paginate`. Wiring: - Root `package.json` adds `pnpm tools-pr` to the allowed root entry points. - `scripts/postinstall.mjs` builds `tools/pr` alongside other workspace packages. - `scripts/guard.ts` allowlists `tools/pr/bin/tools-pr.mjs` and `tools/pr/esbuild.config.mjs`, and adds `pr/` to the `tools/` top-level layout allowlist. - Root `AGENTS.md` and `tools/AGENTS.md` document the new command surface, root-command-boundary update, and per-tool ownership.
90 lines
2.8 KiB
JavaScript
90 lines
2.8 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, extname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(scriptDir, "..");
|
|
|
|
const buildTargets = [
|
|
"packages/contracts",
|
|
"packages/sidecar-proto",
|
|
"packages/sidecar",
|
|
"packages/platform",
|
|
"tools/dev",
|
|
"tools/pack",
|
|
"tools/pr",
|
|
];
|
|
|
|
const jsExtensions = new Set([".js", ".cjs", ".mjs"]);
|
|
|
|
function resolvePackageManagerInvocation() {
|
|
const pnpmExecPath = process.env.npm_execpath;
|
|
if (pnpmExecPath != null && pnpmExecPath.length > 0) {
|
|
if (jsExtensions.has(extname(pnpmExecPath).toLowerCase())) {
|
|
return { argsPrefix: [pnpmExecPath], command: process.execPath };
|
|
}
|
|
return { argsPrefix: [], command: pnpmExecPath };
|
|
}
|
|
|
|
return { argsPrefix: [], command: process.platform === "win32" ? "pnpm.cmd" : "pnpm" };
|
|
}
|
|
|
|
const packageManager = resolvePackageManagerInvocation();
|
|
|
|
for (const target of buildTargets) {
|
|
const result = spawnSync(
|
|
packageManager.command,
|
|
[...packageManager.argsPrefix, "-C", target, "run", "build"],
|
|
{
|
|
cwd: repoRoot,
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
|
|
if (result.error != null) {
|
|
throw result.error;
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
// Verify the better-sqlite3 native addon loads under the current Node.js ABI.
|
|
// better-sqlite3 is a dep of apps/daemon (not the workspace root), so resolve
|
|
// it from the daemon package context. prebuild-install may have fetched a
|
|
// prebuilt binary for a different ABI (e.g. after switching between Node 22 /
|
|
// 24 / 25). When the addon fails to dlopen, pnpm rebuild handles the rebuild
|
|
// using its own node-gyp lifecycle — no assumptions about where node-gyp lives.
|
|
const req = createRequire(resolve(repoRoot, "apps/daemon/package.json"));
|
|
let needsRebuild = false;
|
|
try {
|
|
req("better-sqlite3");
|
|
} catch (e) {
|
|
// MODULE_NOT_FOUND means daemon deps aren't installed yet — not our problem.
|
|
// Any other error (ERR_DLOPEN_FAILED, ABI mismatch, etc.) warrants a rebuild.
|
|
if (e?.code !== "MODULE_NOT_FOUND") {
|
|
needsRebuild = true;
|
|
}
|
|
}
|
|
|
|
if (needsRebuild) {
|
|
process.stdout.write(
|
|
`postinstall: rebuilding better-sqlite3 for Node.js ${process.version}...\n`,
|
|
);
|
|
const rebuild = spawnSync(
|
|
packageManager.command,
|
|
[...packageManager.argsPrefix, "--filter", "@open-design/daemon", "rebuild", "better-sqlite3"],
|
|
{ cwd: repoRoot, stdio: "inherit" },
|
|
);
|
|
if (rebuild.error != null) throw rebuild.error;
|
|
if (rebuild.status !== 0) {
|
|
process.stderr.write(
|
|
"postinstall: better-sqlite3 rebuild failed.\n" +
|
|
"Install build tools (python3, make, g++ or clang++) then run: pnpm install\n",
|
|
);
|
|
process.exit(rebuild.status ?? 1);
|
|
}
|
|
}
|