| #!/usr/bin/env node
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import fs from "node:fs";
|
| import path from "node:path";
|
| import { fileURLToPath } from "node:url";
|
| import { execSync } from "node:child_process";
|
|
|
| const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
|
|
|
|
|
|
|
| const IGNORE_FROM_CODE = new Set([
|
| "NODE_ENV",
|
| "PATH",
|
| "HOME",
|
| "USER",
|
| "LOGNAME",
|
| "XDG_CURRENT_DESKTOP",
|
| "PWD",
|
| "SHELL",
|
| "TERM",
|
| "TZ",
|
| "LANG",
|
| "LC_ALL",
|
| "LC_MESSAGES",
|
| "CI",
|
| "GITHUB_ACTIONS",
|
| "RUNNER_OS",
|
|
|
| "PROJECT_ROOT",
|
| "ARTIFACTS_DIR",
|
|
|
| "APPDATA",
|
| "LOCALAPPDATA",
|
| "XDG_CONFIG_HOME",
|
| "USERPROFILE",
|
| "PREFIX",
|
|
|
| "DISPLAY",
|
|
|
| "LOGNAME",
|
| "XDG_CURRENT_DESKTOP",
|
|
|
| "NEXT_DIST_DIR",
|
| "NEXT_PHASE",
|
| "NEXT_RUNTIME",
|
| "NODE_TEST_CONTEXT",
|
| "VITEST",
|
|
|
| "NODE_TLS_REJECT_UNAUTHORIZED",
|
|
|
| "GITHUB_BASE_REF",
|
| "GITHUB_BASE_SHA",
|
|
|
| "OMNIROUTE_DISABLE_CLI_TOKEN",
|
|
|
| "OMNIROUTE_NO_UPDATE_NOTIFIER",
|
|
|
| "OMNIROUTE_HEADLESS",
|
|
|
|
|
| "CODESPACES",
|
| "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN",
|
| "GITPOD_WORKSPACE_ID",
|
| "NO_COLOR",
|
| "REPL_ID",
|
| "REPL_SLUG",
|
| "WSL_DISTRO_NAME",
|
| "WSL_INTEROP",
|
|
|
| "DISPLAY",
|
| "WAYLAND_DISPLAY",
|
|
|
| "OPENAPI_SPEC",
|
|
|
| "API_KEY",
|
| "APP_URL",
|
| "PUBLIC_URL",
|
| "ANTHROPIC_API_URL",
|
| "OPENAI_API_URL",
|
| "LOG_LEVEL",
|
|
|
| "QA_BASE_URL",
|
| "QA_LOCALES",
|
| "QA_REPORT_SUFFIX",
|
| "QA_ROUTES",
|
|
|
| "OMNIROUTE_DOCTOR_HOST",
|
| "OMNIROUTE_DOCTOR_LIVENESS_URL",
|
| "OMNIROUTE_PROVIDER_CATALOG_PATH",
|
| "OMNIROUTE_PROVIDER_TEST_MODEL",
|
|
|
|
|
| "OMNIROUTE_CLI_SKIP_REPO_ENV",
|
|
|
|
|
|
|
| "OMNIROUTE_BUILD_SHA",
|
|
|
| "OMNIROUT",
|
|
|
| "PAYLOAD_RULES_PATH",
|
|
|
|
|
| "NODE_PATH",
|
|
|
| "NVIDIA_BASE_URL",
|
| "NVIDIA_MODEL",
|
| ]);
|
|
|
|
|
|
|
|
|
| const DOC_ONLY_ALLOWLIST = new Set([
|
|
|
| "CEREBRAS_API_KEY",
|
| "COHERE_API_KEY",
|
| "FIREWORKS_API_KEY",
|
| "GROQ_API_KEY",
|
| "MISTRAL_API_KEY",
|
| "NEBIUS_API_KEY",
|
| "PERPLEXITY_API_KEY",
|
| "TOGETHER_API_KEY",
|
| "XAI_API_KEY",
|
| "QIANFAN_API_KEY",
|
| "CURSOR_PROTOBUF_DEBUG",
|
| "CLI_COMPAT_KIRO",
|
| "CLI_KIMI_CODING_BIN",
|
| "CLI_ROO_BIN",
|
| "IFLOW_OAUTH_CLIENT_ID",
|
| "IFLOW_OAUTH_CLIENT_SECRET",
|
|
|
| "CLI_COMPAT_OMITTED_PROVIDER_IDS",
|
|
|
| "CHANGEME",
|
|
|
|
|
| "OMNIROUTE_CRYPT_KEY",
|
| "OMNIROUTE_API_KEY_BASE64",
|
|
|
| "MAX_RETRY_INTERVAL_SEC",
|
| "REQUEST_RETRY",
|
| "SKILLS_EXECUTION_TIMEOUT_MS",
|
| "SKILLS_SANDBOX_DOCKER_IMAGE",
|
| ]);
|
|
|
|
|
|
|
| const ENV_ONLY_ALLOWLIST = new Set([
|
|
|
| "CODEX_REFRESH_SPACING_MS",
|
| "DEBUG",
|
| "HEAP_PRESSURE_THRESHOLD_MB",
|
| "OMNIRROUTE_TRACE",
|
| "PII_TEST_BYPASS_MIN_WINDOW",
|
| "PII_WINDOW_SIZE",
|
| "TRAE_STREAM_TIMEOUT_MS",
|
| "TRAE_TOKEN",
|
| ]);
|
|
|
|
|
|
|
| |
| |
|
|
| export function parseEnvExampleVars(text) {
|
| const vars = new Set();
|
| for (const line of String(text ?? "").split("\n")) {
|
| const m = line.match(/^#?\s*([A-Z][A-Z0-9_]+)\s*=/);
|
| if (m) vars.add(m[1]);
|
| }
|
| return vars;
|
| }
|
|
|
| |
| |
| |
|
|
| export function parseEnvDocVars(text) {
|
| const vars = new Set();
|
| for (const m of String(text ?? "").matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) {
|
| vars.add(m[1]);
|
| }
|
| return vars;
|
| }
|
|
|
| |
| |
| |
|
|
| function scanCodeVars({ cwd } = {}) {
|
| const repoRoot = cwd ?? REPO_ROOT;
|
| const stdout = execSync(
|
| "grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' " +
|
| "src/ open-sse/ bin/ scripts/ electron/main.js electron/preload.js 2>/dev/null || true",
|
| { cwd: repoRoot, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }
|
| );
|
| const vars = new Set();
|
| for (const line of stdout.split("\n")) {
|
| const m = line.match(/^process\.env\.([A-Z][A-Z0-9_]+)$/);
|
| if (m) vars.add(m[1]);
|
| }
|
| return vars;
|
| }
|
|
|
| |
| |
|
|
| function diff(set, against) {
|
| return [...set].filter((v) => !against.has(v)).sort((a, b) => a.localeCompare(b));
|
| }
|
|
|
|
|
|
|
| |
| |
| |
| |
| |
|
|
| export function runEnvDocSync(options = {}) {
|
| const ignore = options.ignore ?? IGNORE_FROM_CODE;
|
| const docOnly = options.docOnlyAllowlist ?? DOC_ONLY_ALLOWLIST;
|
| const envOnly = options.envOnlyAllowlist ?? ENV_ONLY_ALLOWLIST;
|
|
|
| const envExampleText =
|
| options.envExampleText ??
|
| (options.envExamplePath
|
| ? fs.readFileSync(options.envExamplePath, "utf8")
|
| : fs.readFileSync(path.join(REPO_ROOT, ".env.example"), "utf8"));
|
| const envDocText =
|
| options.envDocText ??
|
| (options.envDocPath
|
| ? fs.readFileSync(options.envDocPath, "utf8")
|
| : fs.readFileSync(path.join(REPO_ROOT, "docs", "reference", "ENVIRONMENT.md"), "utf8"));
|
|
|
| const envVars = parseEnvExampleVars(envExampleText);
|
| const docVars = parseEnvDocVars(envDocText);
|
|
|
| const codeVars = new Set(
|
| [...(options.codeVars ?? scanCodeVars({ cwd: options.root }))].filter((v) => !ignore.has(v))
|
| );
|
|
|
| const codeMissingEnv = diff(codeVars, envVars);
|
| const envMissingDoc = diff(envVars, docVars).filter((v) => !envOnly.has(v));
|
| const docMissingEnv = diff(docVars, envVars).filter((v) => !docOnly.has(v));
|
|
|
| const ok =
|
| codeMissingEnv.length === 0 && envMissingDoc.length === 0 && docMissingEnv.length === 0;
|
|
|
| return {
|
| ok,
|
| summary: {
|
| code: codeVars.size,
|
| envExample: envVars.size,
|
| doc: docVars.size,
|
| },
|
| problems: {
|
| codeMissingEnv,
|
| envMissingDoc,
|
| docMissingEnv,
|
| },
|
| };
|
| }
|
|
|
|
|
|
|
| function printList(label, list, marker) {
|
| if (list.length === 0) {
|
| console.log(` ${marker || "β"} ${label}: none`);
|
| return;
|
| }
|
| console.log(` β ${label}: ${list.length}`);
|
| for (const v of list.slice(0, 50)) console.log(` - ${v}`);
|
| if (list.length > 50) console.log(` ... and ${list.length - 50} more`);
|
| }
|
|
|
| function main() {
|
| const lenient = process.argv.includes("--lenient");
|
| const result = runEnvDocSync();
|
|
|
| console.log("Env var contract sync report");
|
| console.log("============================");
|
| console.log(`Code references: ${result.summary.code} unique vars`);
|
| console.log(`In .env.example: ${result.summary.envExample} unique vars`);
|
| console.log(`In docs/reference/ENVIRONMENT.md: ${result.summary.doc} unique vars`);
|
| console.log();
|
|
|
| printList("In code but missing from .env.example", result.problems.codeMissingEnv);
|
| printList("In .env.example but missing from ENVIRONMENT.md", result.problems.envMissingDoc);
|
| printList("In ENVIRONMENT.md but missing from .env.example", result.problems.docMissingEnv);
|
|
|
| if (result.ok) {
|
| console.log("\nβ Env / docs contract is in sync.");
|
| process.exit(0);
|
| }
|
|
|
| if (lenient) {
|
| console.log("\nβ Drift detected (lenient mode β exit 0).");
|
| process.exit(0);
|
| }
|
|
|
| console.log(
|
| "\nβ Env / docs contract is out of sync. Update .env.example, docs/reference/ENVIRONMENT.md,"
|
| );
|
| console.log(" or the allowlists in scripts/check/check-env-doc-sync.mjs and try again.");
|
| process.exit(1);
|
| }
|
|
|
| if (import.meta.url === `file://${process.argv[1]}`) {
|
| main();
|
| }
|
|
|