Files
openclaw/scripts/check-env-var-count.mjs
2026-07-28 07:06:00 -04:00

138 lines
4.6 KiB
JavaScript

import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const BUDGET_PATH = "config/env-var-count-budget.txt";
const SOURCE_ROOTS = ["src", "packages", "extensions"];
const SOURCE_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
const ENV_VAR_PATTERN = /OPENCLAW_[A-Z0-9_]+/gu;
export function isCountedSourcePath(filePath) {
const normalized = filePath.replaceAll("\\", "/");
if (!SOURCE_ROOTS.some((root) => normalized.startsWith(root + "/"))) {
return false;
}
if (!SOURCE_EXTENSIONS.has(path.posix.extname(normalized))) {
return false;
}
if (
/^(?:extensions\/(?:qa-lab|test-support)|.*\/(?:__tests__|test|tests|test-utils|test-support))\//u.test(
normalized,
)
) {
return false;
}
return !/(?:^|[./-])(?:e2e|live-helpers|live-harness|spec|suite|test|test-helpers|test-harness|test-setup|test-support|test-utils)(?:[./-]|$)/u.test(
normalized,
);
}
export function collectEnvVarNames(root = process.cwd(), options = {}) {
const staged = options.staged === true;
const files = execFileSync(
"git",
[
"ls-files",
"-z",
"--cached",
...(staged ? [] : ["--others", "--exclude-standard"]),
"--",
...SOURCE_ROOTS,
],
{ cwd: root, maxBuffer: 256 * 1024 * 1024 },
)
.toString("utf8")
.split("\0")
.filter(isCountedSourcePath)
.filter((file) => staged || fs.existsSync(path.join(root, file)));
const names = new Set();
for (const file of files) {
const source = staged
? execFileSync("git", ["show", `:${file}`], { cwd: root, encoding: "utf8" })
: fs.readFileSync(path.join(root, file), "utf8");
for (const match of source.matchAll(ENV_VAR_PATTERN)) {
names.add(match[0]);
}
}
return [...names].toSorted((left, right) => (left < right ? -1 : left > right ? 1 : 0));
}
export function parseBudget(source) {
const values = source
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
if (values.length !== 1 || !/^\d+$/u.test(values[0])) {
throw new Error(`${BUDGET_PATH} must contain exactly one non-negative integer`);
}
return Number(values[0]);
}
function readBaseBudget(root, ref) {
const resolved = spawnSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
cwd: root,
encoding: "utf8",
});
if (resolved.status !== 0) {
throw new Error(`Could not resolve env-var count base ref: ${ref}`);
}
const mergeBase = spawnSync("git", ["merge-base", "HEAD", ref], {
cwd: root,
encoding: "utf8",
});
const baselineRef = mergeBase.stdout.trim();
if (mergeBase.status !== 0 || !baselineRef) {
throw new Error(`Could not resolve env-var count merge base for: ${ref}`);
}
const entry = execFileSync("git", ["ls-tree", "--name-only", baselineRef, "--", BUDGET_PATH], {
cwd: root,
encoding: "utf8",
}).trim();
if (!entry) {
return null;
}
return parseBudget(
execFileSync("git", ["show", `${baselineRef}:${BUDGET_PATH}`], {
cwd: root,
encoding: "utf8",
}),
);
}
export function main(argv = process.argv.slice(2), root = process.cwd()) {
const baseIndex = argv.indexOf("--base");
const baseRef = baseIndex >= 0 ? argv[baseIndex + 1] : "origin/main";
const staged = argv.includes("--staged");
const expectedLength = (baseIndex >= 0 ? 2 : 0) + (staged ? 1 : 0);
if ((baseIndex >= 0 && !baseRef) || argv.length !== expectedLength) {
throw new Error("Usage: node scripts/check-env-var-count.mjs [--staged] [--base <git-ref>]");
}
const budgetSource = staged
? execFileSync("git", ["show", `:${BUDGET_PATH}`], { cwd: root, encoding: "utf8" })
: fs.readFileSync(path.join(root, BUDGET_PATH), "utf8");
const budget = parseBudget(budgetSource);
const baseBudget = readBaseBudget(root, baseRef);
if (baseBudget !== null && budget > baseBudget) {
throw new Error(`OPENCLAW_* budget grew from ${baseBudget} to ${budget}`);
}
const names = collectEnvVarNames(root, { staged });
if (names.length !== budget) {
const direction = names.length > budget ? "exceeds" : "is below";
throw new Error(
`OPENCLAW_* count ${names.length} ${direction} budget ${budget}; update ${BUDGET_PATH}`,
);
}
console.log(`OPENCLAW_* count ${names.length}/${budget}`);
return names.length;
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}