refactor(scripts): adopt shared scaffolding (#118514)

* refactor(scripts): adopt shared scaffolding

* fix(scripts): satisfy strict tooling checks

* fix(scripts): preserve scaffolding contracts
This commit is contained in:
Peter Steinberger
2026-08-03 04:40:30 -07:00
committed by GitHub
parent 757d220203
commit ea2c6a63c9
79 changed files with 742 additions and 712 deletions
+40 -38
View File
@@ -2,10 +2,10 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { runAndroidSigningCommandSync } from "./lib/android-release-signing-process.mjs";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const rootDir = resolveRepoRoot(import.meta.url);
const defaultManifestPath = path.join(rootDir, "apps", "android", "Config", "ReleaseSigning.json");
const requiredPropertyNames = [
"OPENCLAW_ANDROID_STORE_FILE",
@@ -46,33 +46,43 @@ function parseArgs(argv) {
keystorePath: process.env.OPENCLAW_ANDROID_UPLOAD_KEYSTORE || "",
propertiesPath: process.env.OPENCLAW_ANDROID_SIGNING_PROPERTIES || "",
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--mode") {
options.mode = readOptionValue(argv, index, arg);
index += 1;
} else if (arg === "--manifest") {
options.manifestPath = path.resolve(readOptionValue(argv, index, arg));
index += 1;
} else if (arg === "--workspace") {
options.workspace = path.resolve(readOptionValue(argv, index, arg));
index += 1;
} else if (arg === "--materialized-dir") {
options.materializedDir = path.resolve(readOptionValue(argv, index, arg));
index += 1;
} else if (arg === "--keystore") {
options.keystorePath = path.resolve(readOptionValue(argv, index, arg));
index += 1;
} else if (arg === "--properties") {
options.propertiesPath = path.resolve(readOptionValue(argv, index, arg));
index += 1;
} else if (arg === "-h" || arg === "--help") {
usage();
process.exit(0);
} else {
throw new Error(`Unknown argument: ${arg}`);
}
const helpIndex = argv.findIndex((arg) => arg === "-h" || arg === "--help");
parseFlagArgs(
helpIndex === -1 ? argv : argv.slice(0, helpIndex),
options,
[
stringFlag("--mode", "mode", {
allowInline: false,
missingValueMessage: "Missing value for --mode.",
rejectShortOptions: true,
repeatable: true,
}),
...[
["--manifest", "manifestPath"],
["--workspace", "workspace"],
["--materialized-dir", "materializedDir"],
["--keystore", "keystorePath"],
["--properties", "propertiesPath"],
].map(([flag, key]) =>
stringFlag(flag, key, {
allowInline: false,
missingValueMessage: `Missing value for ${flag}.`,
rejectShortOptions: true,
repeatable: true,
transform: path.resolve,
}),
),
],
{
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`Unknown argument: ${arg}`);
},
},
);
if (helpIndex !== -1) {
usage();
process.exit(0);
}
if (!options.mode) {
@@ -82,14 +92,6 @@ function parseArgs(argv) {
return options;
}
function readOptionValue(argv, index, option) {
const value = argv[index + 1] ?? "";
if (!value || value.startsWith("-")) {
throw new Error(`Missing value for ${option}.`);
}
return value;
}
function requireString(value, key) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`Android release signing manifest missing ${key}.`);
+2 -2
View File
@@ -12,9 +12,9 @@ import {
import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs";
import { optionalBundledClusterSet } from "./lib/optional-bundled-clusters.mjs";
import { escapeRegExp } from "./lib/regexp.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { toLine } from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const srcRoot = path.join(repoRoot, "src");
const extensionsRoot = path.join(repoRoot, BUNDLED_PLUGIN_ROOT_DIR);
const testRoot = path.join(repoRoot, "test");
+3 -3
View File
@@ -4,12 +4,12 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { runManagedCommand } from "./lib/managed-child-process.mjs";
import { assertRealOutputRoot } from "./lib/output-root-guard.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mjs";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const rootDir = resolveRepoRoot(import.meta.url);
const VALID_PHASES = new Set(["build", "copy"]);
// Each complete bundled-plugin asset generator gets the same 10-minute build ceiling.
const BUNDLED_PLUGIN_ASSET_HOOK_TIMEOUT_MS = 600_000;
@@ -5,10 +5,10 @@ import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectTypeScriptFiles,
getPropertyNameText,
resolveRepoRoot,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
+3 -3
View File
@@ -5,9 +5,9 @@ import { spawnSync as defaultSpawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { pathToFileURL } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const tmpDir = process.env.TMPDIR || process.env.TEMP || process.env.TMP || os.tmpdir();
const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
const DEFAULT_COMMAND_TIMEOUT_MS = 60_000;
@@ -2,10 +2,9 @@
// Verifies each generated Control UI sidecar encodes the final emitted asset bytes.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { brotliDecompressSync, gunzipSync } from "node:zlib";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const assetsDir = path.join(repoRoot, "dist", "control-ui", "assets");
const errors = [];
let checked = 0;
@@ -12,7 +12,8 @@ import {
mergeLegacyObjectPropertyValues,
mergeLegacyPathBranchAssignments,
} from "./lib/legacy-store-path-domain.mjs";
import { resolveRepoRoot, runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs";
const databaseFirstLegacyStoreSourceRoots = ["src", "extensions", "packages"];
const databaseFirstNativeSourceRoots = ["apps/macos/Sources/OpenClaw"];
+2 -2
View File
@@ -3,12 +3,12 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const require = createRequire(import.meta.url);
const ts = require("typescript");
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const SCAN_ROOTS = ["src", "extensions", "packages"];
const SOURCE_FILE_RE = /\.(?:ts|tsx)$/;
const SKIP_PATH_RE =
+2 -3
View File
@@ -4,7 +4,6 @@
// the source checkout copied or mounted as the app under test.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { laneResources, laneWeight } from "./lib/docker-e2e-plan.mjs";
import {
allReleasePathLanes,
@@ -12,8 +11,8 @@ import {
publicInstallerLanes,
tailLanes,
} from "./lib/docker-e2e-scenarios.mjs";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const ROOT_DIR = resolveRepoRoot(import.meta.url);
const errors = [];
const packageJson = JSON.parse(readText("package.json"));
const packageScripts = new Set(Object.keys(packageJson.scripts ?? {}));
+2 -3
View File
@@ -2,9 +2,8 @@
// Runs duplicate-code detection with repo-specific excludes.
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const jscpdBin = path.join(repoRoot, "node_modules", "jscpd", "bin", "jscpd");
const targets = [
+2 -6
View File
@@ -4,12 +4,8 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
import {
collectTypeScriptFilesFromRoots,
resolveRepoRoot,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { collectTypeScriptFilesFromRoots, runAsScript, toLine } from "./lib/ts-guard-utils.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const defaultRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
@@ -16,6 +16,7 @@ import os from "node:os";
import path, { dirname, join, resolve } from "node:path";
import pMap from "p-map";
import { parsePositiveInt } from "./lib/numeric-options.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
forwardSignalToVitestProcessGroup,
installVitestProcessGroupCleanup,
@@ -23,7 +24,7 @@ import {
} from "./vitest-process-group.mjs";
const require = createRequire(import.meta.url);
const repoRoot = resolve(import.meta.dirname, "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const tscBin = require.resolve("typescript/bin/tsc");
const nativePreviewPackageJsonPath = require.resolve("@typescript/native-preview/package.json");
const nativePreviewPackageJson = JSON.parse(readFileSync(nativePreviewPackageJsonPath, "utf8"));
@@ -15,8 +15,9 @@ import {
resolveRepoSpecifier,
writeLine,
} from "./lib/guard-inventory-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mjs";
import { resolveRepoRoot, runAsScript } from "./lib/ts-guard-utils.mjs";
import { runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
// Generated bundles are validated at their build owner; they are not bounded authored source.
@@ -4,8 +4,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const LOCAL_WILDCARD_REEXPORT_PATTERN = /^\s*export\s+(?:type\s+)?\*\s+from\s+["'](?:\.{1,2}\/)/u;
+1 -1
View File
@@ -4,10 +4,10 @@
import { promises as fs } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectTypeScriptFilesFromRoots,
getPropertyNameText,
resolveRepoRoot,
runAsScript,
toLine,
unwrapExpression,
@@ -11,7 +11,8 @@ import {
runBaselineInventoryCheck,
resolveRepoSpecifier,
} from "./lib/guard-inventory-utils.mjs";
import { resolveRepoRoot, runAsScript } from "./lib/ts-guard-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const baselinePath = path.join(
+2 -3
View File
@@ -3,16 +3,15 @@
// Verifies plugin SDK subpath exports and generated entrypoint metadata.
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { normalizeRepoPath, visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveSourceRoots,
toLine,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const scanRoots = resolveSourceRoots(repoRoot, [
"src",
"packages",
@@ -4,8 +4,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const extensionsRoot = path.join(repoRoot, "extensions");
const WILDCARD_PLUGIN_SDK_REEXPORT_PATTERN =
+3 -3
View File
@@ -1,8 +1,8 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { pathToFileURL } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const schemaDir = path.join(repoRoot, "packages/gateway-protocol/src/schema");
const failures = [];
const read = (relativePath) => fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
+2 -2
View File
@@ -4,12 +4,12 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const require = createRequire(import.meta.url);
const ts = require("typescript");
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const descriptorPath = "src/gateway/methods/core-descriptors.ts";
function runGit(args) {
+1 -1
View File
@@ -4,9 +4,9 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveRepoRoot,
runAsScript,
toLine,
unwrapExpression,
+1 -1
View File
@@ -3,10 +3,10 @@
import fs from "node:fs/promises";
import path from "node:path";
import ts from "typescript";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectFileViolations,
getPropertyNameText,
resolveRepoRoot,
resolveSourceRoots,
runAsScript,
toLine,
@@ -2,9 +2,9 @@
import path from "node:path";
import ts from "typescript";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectFileViolations,
resolveRepoRoot,
resolveSourceRoots,
runAsScript,
toLine,
@@ -1,9 +1,9 @@
#!/usr/bin/env node
import ts from "typescript";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
collectFileViolations,
resolveRepoRoot,
resolveSourceRoots,
runAsScript,
toLine,
@@ -2,8 +2,8 @@
// Prevents Telegram runtime imports from grammy type-only modules.
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const telegramRoot = path.join(repoRoot, "extensions/telegram");
const importSpecifierPatterns = [
/\bimport\s+(?:type\s+)?[\s\S]*?\bfrom\s*["']([^"']+)["']/gu,
+2 -3
View File
@@ -2,11 +2,10 @@
// Enforces core tsgo project boundaries and sparse-checkout safety.
import { spawnSync } from "node:child_process";
import path from "node:path";
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mjs";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs";
const repoRoot = path.resolve(import.meta.dirname, "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const tsgoPath = resolveRepoToolBinPath("tsgo", { cwd: repoRoot });
const coreGraphs = [
@@ -1,12 +1,10 @@
#!/usr/bin/env node
// Checks core web-fetch surfaces for provider-owned Firecrawl coupling.
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { collectSourceFileContents } from "./lib/source-file-scan-cache.mjs";
import { runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const scanExtensions = new Set([".ts", ".js", ".mjs", ".cjs"]);
const ignoredDirNames = new Set([
".artifacts",
@@ -3,12 +3,11 @@
// Inventories core web-search surfaces that still mention bundled providers.
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { diffInventoryEntries, runBaselineInventoryCheck } from "./lib/guard-inventory-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { collectSourceFileContents } from "./lib/source-file-scan-cache.mjs";
import { runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const baselinePath = path.join(
repoRoot,
"test",
+18 -20
View File
@@ -1,5 +1,6 @@
// Runs the repository check lanes selected by CLI arguments.
import { performance } from "node:perf_hooks";
import { booleanFlag, parseFlagArgs } from "./lib/arg-utils.mjs";
import { printTimingSummary } from "./lib/check-timing-summary.mjs";
import { runManagedCommand } from "./lib/managed-child-process.mjs";
@@ -24,26 +25,23 @@ export function usage() {
* Parses aggregate check runner arguments.
*/
function parseCheckArgs(argv) {
const args = {
help: false,
includeArchitecture: false,
includeTestTypes: false,
timed: false,
};
for (const arg of argv) {
if (arg === "--timed") {
args.timed = true;
} else if (arg === "--include-architecture") {
args.includeArchitecture = true;
} else if (arg === "--include-test-types") {
args.includeTestTypes = true;
} else if (arg === "--help" || arg === "-h") {
args.help = true;
} else {
throw new Error(`unknown argument: ${arg}\n\n${usage()}`);
}
}
return args;
return parseFlagArgs(
argv,
{ help: false, includeArchitecture: false, includeTestTypes: false, timed: false },
[
booleanFlag("--timed", "timed", true, { repeatable: true }),
booleanFlag("--include-architecture", "includeArchitecture", true, { repeatable: true }),
booleanFlag("--include-test-types", "includeTestTypes", true, { repeatable: true }),
booleanFlag("--help", "help", true, { repeatable: true }),
booleanFlag("-h", "help", true, { repeatable: true }),
],
{
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`unknown argument: ${arg}\n\n${usage()}`);
},
},
);
}
/**
+3 -3
View File
@@ -4,12 +4,12 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { renderDocsHeadingMap } from "./docs-list.js";
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "..");
const ROOT = resolveRepoRoot(import.meta.url);
const SOURCE_DOCS_DIR = path.join(ROOT, "docs");
const SOURCE_CONFIG_PATH = path.join(SOURCE_DOCS_DIR, "docs.json");
const INTERNAL_DOCS_DIRS = ["internal"];
+2 -3
View File
@@ -12,10 +12,9 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const engine = process.env.OPENCLAW_SANDBOX_E2E_ENGINE?.trim() || "docker";
const image = process.env.OPENCLAW_SANDBOX_E2E_IMAGE?.trim() || "e2e-sleep:latest";
const useSudo = process.env.OPENCLAW_SANDBOX_E2E_SUDO === "1";
+3 -3
View File
@@ -4,10 +4,10 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { readPositiveEnvInt } from "./lib/numeric-options.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const entryCandidates = ["dist/entry.js", "dist/entry.mjs"];
const startupMetadataPath = "dist/cli-startup-metadata.json";
const DEFAULT_BUILD_TIMEOUT_MS = 10 * 60 * 1000;
+3 -3
View File
@@ -4,14 +4,14 @@
import { spawnSync } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import {
collectBundledPluginBuildEntries,
NON_PACKAGED_BUNDLED_PLUGIN_DIRS,
} from "./lib/bundled-plugin-build-entries.mjs";
import { readPositiveEnvInt } from "./lib/numeric-options.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const DEFAULT_BUILD_TIMEOUT_MS = 10 * 60 * 1000;
/**
+3 -2
View File
@@ -2,12 +2,13 @@
// Ensures Playwright Chromium is installed or a usable system browser is available.
import { spawnSync as spawnSyncImpl } from "node:child_process";
import { existsSync as existsSyncImpl, realpathSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const playwrightInstallBaseArgs = ["--dir", "ui", "exec", "playwright", "install"];
const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH";
const chromiumPackageNames = ["chromium-browser", "chromium"];
+2 -2
View File
@@ -9,9 +9,9 @@ import { pathToFileURL } from "node:url";
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mjs";
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
import { outputTail } from "./lib/output-tail.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
const ROOT = path.resolve(import.meta.dirname, "..");
const ROOT = resolveRepoRoot(import.meta.url);
const CHECK = process.argv.includes("--check");
const DOCS_FORMAT_MAX_BUFFER_BYTES = 1024 * 1024 * 16;
const DOCS_FORMAT_MAX_COMMAND_LINE_BYTES = 24 * 1024;
@@ -19,13 +19,6 @@ export function releaseProfileForTarget(
readPackageJson?: (sha: string) => string,
): "beta" | "stable";
export function releaseEvidenceVerificationArgs(parentRunId: unknown): string[];
export function runGhRead(
args: string[],
params?: {
execFileSyncImpl?: (...args: unknown[]) => unknown;
timeoutMs?: number;
},
): string;
export function shouldDeleteTemporaryWorkflowRef(params: {
keepBranch: boolean;
dryRun: boolean;
+25 -26
View File
@@ -5,6 +5,7 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { execGhRead } from "./lib/plain-gh.mjs";
const WORKFLOW = "full-release-validation.yml";
const TRUSTED_WORKFLOW_PATH = `.github/workflows/${WORKFLOW}`;
@@ -13,6 +14,12 @@ const RELEASE_EVIDENCE_VERIFIER_PATHS = [
".agents/skills/release-openclaw-ci/scripts/release-ci-summary.mjs",
];
const GH_READ_TIMEOUT_MS = 60_000;
const GH_READ_OPTIONS = {
encoding: "utf8",
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "inherit"],
timeout: GH_READ_TIMEOUT_MS,
};
const RELEASE_BRANCH_PATTERN =
/^(?:release\/[0-9]{4}\.[0-9]+\.[0-9]+|extended-stable\/[0-9]{4}\.[0-9]+\.33)$/u;
const RELEASE_TAG_PATTERN = /^v[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)\.[0-9]+)?$/u;
@@ -62,17 +69,6 @@ function runStatus(command, args, options = {}) {
});
}
export function runGhRead(args, params = {}) {
const execFileSyncImpl = params.execFileSyncImpl ?? execFileSync;
const output = execFileSyncImpl("gh", args, {
encoding: "utf8",
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "inherit"],
timeout: params.timeoutMs ?? GH_READ_TIMEOUT_MS,
});
return typeof output === "string" ? output.trim() : "";
}
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (value === undefined || value === "" || value.startsWith("-")) {
@@ -264,20 +260,23 @@ function collectRunId(dispatchOutput) {
}
function findLatestRunId(branch, sha) {
const json = runGhRead([
"run",
"list",
"--workflow",
WORKFLOW,
"--branch",
branch,
"--event",
"workflow_dispatch",
"--limit",
"20",
"--json",
"databaseId,headSha,createdAt",
]);
const json = execGhRead(
[
"run",
"list",
"--workflow",
WORKFLOW,
"--branch",
branch,
"--event",
"workflow_dispatch",
"--limit",
"20",
"--json",
"databaseId,headSha,createdAt",
],
GH_READ_OPTIONS,
);
const runs = JSON.parse(json);
const match = runs.find((runItem) => runItem.headSha === sha);
return match?.databaseId ? String(match.databaseId) : "";
@@ -288,7 +287,7 @@ function readWorkflowRun(parentRunId, workflowSha) {
throw new Error("parent run ID must be a positive decimal");
}
const workflowRun = JSON.parse(
runGhRead(["api", `repos/openclaw/openclaw/actions/runs/${parentRunId}`]),
execGhRead(["api", `repos/openclaw/openclaw/actions/runs/${parentRunId}`], GH_READ_OPTIONS),
);
if (workflowRun.head_sha !== workflowSha) {
throw new Error(
@@ -5,6 +5,7 @@ import { execFileSync } from "node:child_process";
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
/**
* Dependency evidence reports generated for release artifacts.
@@ -379,14 +380,6 @@ async function generateDependencyReleaseEvidence({
return { manifest, counts, outputDir };
}
function readOptionValue(argv, index, optionName, { allowEmpty = false } = {}) {
const value = argv[index + 1];
if (value === undefined || value.startsWith("-") || (!allowEmpty && value === "")) {
throw new Error(`Expected ${optionName} <value>.`);
}
return value;
}
function usage() {
return `Usage: node scripts/generate-dependency-release-evidence.mjs --output-dir <dir> --release-ref <ref> --npm-dist-tag <tag> [options]
@@ -414,60 +407,34 @@ export function parseArgs(argv) {
githubOutput: process.env.GITHUB_OUTPUT,
githubStepSummary: process.env.GITHUB_STEP_SUMMARY,
};
const seen = new Set();
const setOnce = (flag, key, value) => {
if (seen.has(flag)) {
throw new Error(`${flag} was provided more than once.`);
}
seen.add(flag);
options[key] = value;
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") {
continue;
}
if (arg === "-h" || arg === "--help") {
return { ...options, help: true };
}
if (arg === "--root") {
setOnce(arg, "rootDir", readOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--output-dir") {
setOnce(arg, "outputDir", readOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--release-ref") {
setOnce(arg, "releaseRef", readOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--npm-dist-tag") {
setOnce(arg, "npmDistTag", readOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--base-ref") {
setOnce(arg, "baseRef", readOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--github-output") {
setOnce(arg, "githubOutput", readOptionValue(argv, index, arg, { allowEmpty: true }));
index += 1;
continue;
}
if (arg === "--github-step-summary") {
setOnce(arg, "githubStepSummary", readOptionValue(argv, index, arg, { allowEmpty: true }));
index += 1;
continue;
}
throw new Error(`Unsupported argument: ${arg}`);
}
return options;
const helpIndex = argv.findIndex((arg) => arg === "-h" || arg === "--help");
const parsed = parseFlagArgs(
helpIndex === -1 ? argv : argv.slice(0, helpIndex),
options,
[
["--root", "rootDir", false],
["--output-dir", "outputDir", false],
["--release-ref", "releaseRef", false],
["--npm-dist-tag", "npmDistTag", false],
["--base-ref", "baseRef", false],
["--github-output", "githubOutput", true],
["--github-step-summary", "githubStepSummary", true],
].map(([flag, key, allowEmpty]) =>
stringFlag(flag, key, {
allowEmpty,
allowInline: false,
missingValueMessage: `Expected ${flag} <value>.`,
rejectShortOptions: true,
}),
),
{
duplicateOptionMessage: (flag) => `${flag} was provided more than once.`,
onUnhandledArg(arg) {
throw new Error(`Unsupported argument: ${arg}`);
},
},
);
return helpIndex === -1 ? parsed : { ...parsed, help: true };
}
/**
@@ -2,8 +2,8 @@
// Generates Swift constants for the host environment security policy.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadHostEnvSecurityPolicy } from "../src/infra/host-env-security-policy.js";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const args = new Set(process.argv.slice(2));
const checkOnly = args.has("--check");
@@ -14,8 +14,7 @@ if (checkOnly && args.has("--write")) {
process.exit(1);
}
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const policyPath = path.join(repoRoot, "src", "infra", "host-env-security-policy.json");
const outputPath = path.join(
repoRoot,
+35 -32
View File
@@ -2,9 +2,9 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const rootDir = resolveRepoRoot(import.meta.url);
const defaultManifestPath = path.join(rootDir, "apps", "ios", "Config", "AppStoreSigning.json");
function validateAppGroupId(value, context) {
@@ -31,38 +31,41 @@ validates the checked-in manifest and renders local release xcconfig settings.
}
function parseArgs(argv) {
let mode = "";
let manifestPath = defaultManifestPath;
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--mode") {
mode = readOptionValue(argv, i, arg);
i += 1;
} else if (arg === "--manifest") {
manifestPath = path.resolve(readOptionValue(argv, i, arg));
i += 1;
} else if (arg === "-h" || arg === "--help") {
usage();
process.exit(0);
} else {
throw new Error(`Unknown argument: ${arg}`);
}
const options = { manifestPath: defaultManifestPath, mode: "" };
const helpIndex = argv.findIndex((arg) => arg === "-h" || arg === "--help");
parseFlagArgs(
helpIndex === -1 ? argv : argv.slice(0, helpIndex),
options,
[
stringFlag("--mode", "mode", {
allowInline: false,
missingValueMessage: "Missing value for --mode.",
rejectShortOptions: true,
repeatable: true,
}),
stringFlag("--manifest", "manifestPath", {
allowInline: false,
missingValueMessage: "Missing value for --manifest.",
rejectShortOptions: true,
repeatable: true,
transform: path.resolve,
}),
],
{
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`Unknown argument: ${arg}`);
},
},
);
if (helpIndex !== -1) {
usage();
process.exit(0);
}
if (!mode) {
if (!options.mode) {
throw new Error("Missing required --mode.");
}
return { mode, manifestPath };
}
function readOptionValue(argv, index, option) {
const value = argv[index + 1] ?? "";
if (!value || value.startsWith("-")) {
throw new Error(`Missing value for ${option}.`);
}
return value;
return options;
}
function readManifest(manifestPath) {
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
import { existsSync, lstatSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const iosRoot = path.join(repoRoot, "apps", "ios");
const outputPath = path.join(iosRoot, "SwiftSources.input.xcfilelist");
+16 -2
View File
@@ -17,12 +17,24 @@ export function stripLeadingPackageManagerSeparator(argv: string[]): string[];
export function stringFlag<T extends FlagArgs>(
flag: string,
key: string,
options?: { rejectShortOptions?: boolean },
options?: {
allowEmpty?: boolean;
allowInline?: boolean;
missingValueMessage?: string;
rejectShortOptions?: boolean;
repeatable?: boolean;
transform?: (value: string) => unknown;
},
): FlagSpec<T>;
export function stringListFlag<T extends FlagArgs>(
flag: string,
key: string,
options?: { rejectShortOptions?: boolean },
options?: {
allowEmpty?: boolean;
allowInline?: boolean;
missingValueMessage?: string;
rejectShortOptions?: boolean;
},
): FlagSpec<T>;
export function intFlag<T extends FlagArgs>(
flag: string,
@@ -33,6 +45,7 @@ export function booleanFlag<T extends FlagArgs>(
flag: string,
key: string,
value?: unknown,
options?: { repeatable?: boolean },
): FlagSpec<T>;
export function parseFlagArgs<T extends FlagArgs>(
argv: readonly string[],
@@ -40,6 +53,7 @@ export function parseFlagArgs<T extends FlagArgs>(
specs: readonly FlagSpec<T>[],
options?: {
allowUnknownOptions?: boolean;
duplicateOptionMessage?: (flag: string) => string;
ignoreDoubleDash?: boolean;
onUnhandledArg?: (arg: string, args: T) => "handled" | void;
},
+22 -16
View File
@@ -1,4 +1,7 @@
// Shared argument parsing helpers for repository scripts.
function failFlagParse(message) {
throw new Error(message);
}
/**
* Read a flag value from `--flag value` or `--flag=value` arguments.
* @internal Shared repository-script contract.
@@ -25,7 +28,7 @@ export function stripLeadingPackageManagerSeparator(argv) {
}
function isMissingStringFlagValue(value, options = {}) {
if (!value) {
if (value === undefined || (!value && options.allowEmpty !== true)) {
return true;
}
if (value.startsWith("--")) {
@@ -35,10 +38,10 @@ function isMissingStringFlagValue(value, options = {}) {
}
function consumeStringFlag(argv, index, flag, options = {}) {
const inlineValue = readInlineFlagValue(argv[index], flag);
const inlineValue = options.allowInline === false ? null : readInlineFlagValue(argv[index], flag);
if (inlineValue !== null) {
if (isMissingStringFlagValue(inlineValue, options)) {
throw new Error(`${flag} requires a value`);
failFlagParse(options.missingValueMessage ?? `${flag} requires a value`);
}
return {
nextIndex: index,
@@ -50,7 +53,7 @@ function consumeStringFlag(argv, index, flag, options = {}) {
}
const value = argv[index + 1];
if (isMissingStringFlagValue(value, options)) {
throw new Error(`${flag} requires a value`);
failFlagParse(options.missingValueMessage ?? `${flag} requires a value`);
}
return {
nextIndex: index + 1,
@@ -66,7 +69,7 @@ function consumeIntFlag(argv, index, flag, options = {}) {
const parsed = parseIntegerFlagValue(raw.value, flag);
const min = options.min ?? Number.NEGATIVE_INFINITY;
if (parsed < min) {
throw new Error(`${flag} must be at least ${min}`);
failFlagParse(`${flag} must be at least ${min}`);
}
return {
nextIndex: raw.nextIndex,
@@ -83,7 +86,7 @@ function readFlagOptionValue(argv, index, flag) {
const inlineValue = readInlineFlagValue(argv[index], flag);
if (inlineValue !== null) {
if (!inlineValue) {
throw new Error(`${flag} requires a value`);
failFlagParse(`${flag} requires a value`);
}
return { nextIndex: index, value: inlineValue };
}
@@ -92,7 +95,7 @@ function readFlagOptionValue(argv, index, flag) {
}
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${flag} requires a value`);
failFlagParse(`${flag} requires a value`);
}
return { nextIndex: index + 1, value };
}
@@ -100,11 +103,11 @@ function readFlagOptionValue(argv, index, flag) {
function parseIntegerFlagValue(raw, flag) {
const text = String(raw).trim();
if (!/^-?\d+$/u.test(text)) {
throw new Error(`${flag} must be an integer`);
failFlagParse(`${flag} must be an integer`);
}
const parsed = Number(text);
if (!Number.isSafeInteger(parsed)) {
throw new Error(`${flag} must be a safe integer`);
failFlagParse(`${flag} must be a safe integer`);
}
return parsed;
}
@@ -120,9 +123,9 @@ export function stringFlag(flag, key, options = {}) {
return {
flag,
nextIndex: option.nextIndex,
repeatable: false,
repeatable: options.repeatable === true,
apply(target) {
target[key] = option.value;
target[key] = options.transform ? options.transform(option.value) : option.value;
},
};
},
@@ -184,7 +187,7 @@ export function intFlag(flag, key, options) {
}
/** Create a flag spec that assigns a fixed boolean-like value when present. */
export function booleanFlag(flag, key, value = true) {
export function booleanFlag(flag, key, value = true, options = {}) {
return {
consume(argv, index) {
if (argv[index] !== flag) {
@@ -193,7 +196,7 @@ export function booleanFlag(flag, key, value = true) {
return {
flag,
nextIndex: index,
repeatable: false,
repeatable: options.repeatable === true,
apply(target) {
target[key] = value;
},
@@ -218,11 +221,14 @@ export function parseFlagArgs(argv, args, specs, options = {}) {
continue;
}
if (typeof option.flag !== "string" || !option.flag) {
throw new Error("parseFlagArgs specs must declare a flag for consumed options");
failFlagParse("parseFlagArgs specs must declare a flag for consumed options");
}
if (option.repeatable !== true) {
if (seenFlags.has(option.flag)) {
throw new Error(`${option.flag} was provided more than once`);
failFlagParse(
options.duplicateOptionMessage?.(option.flag) ??
`${option.flag} was provided more than once`,
);
}
seenFlags.add(option.flag);
}
@@ -239,7 +245,7 @@ export function parseFlagArgs(argv, args, specs, options = {}) {
continue;
}
if (!options.allowUnknownOptions && arg.startsWith("-")) {
throw new Error(`Unknown option: ${arg}`);
failFlagParse(`Unknown option: ${arg}`);
}
}
return args;
+2 -5
View File
@@ -1,11 +1,8 @@
// Shared scanner for guard scripts that reject disallowed source callsites.
import { promises as fs } from "node:fs";
import path from "node:path";
import {
collectTypeScriptFilesFromRoots,
resolveRepoRoot,
resolveSourceRoots,
} from "./ts-guard-utils.mjs";
import { resolveRepoRoot } from "./repo-root.mjs";
import { collectTypeScriptFilesFromRoots, resolveSourceRoots } from "./ts-guard-utils.mjs";
/** Run a callsite guard over TypeScript roots and exit non-zero on violations. */
export async function runCallsiteGuard(params) {
@@ -10,11 +10,8 @@ import {
resolveRepoSpecifier,
writeLine,
} from "./guard-inventory-utils.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveRepoRoot,
resolveSourceRoots,
} from "./ts-guard-utils.mjs";
import { resolveRepoRoot } from "./repo-root.mjs";
import { collectTypeScriptFilesFromRoots, resolveSourceRoots } from "./ts-guard-utils.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const DEFAULT_BOUNDARY_SOURCE_MAX_BYTES = 2 * 1024 * 1024;
+2 -1
View File
@@ -1,6 +1,7 @@
// Builds shared repo/source-root context for pairing guard scripts.
import path from "node:path";
import { resolveRepoRoot, resolveSourceRoots } from "./ts-guard-utils.mjs";
import { resolveRepoRoot } from "./repo-root.mjs";
import { resolveSourceRoots } from "./ts-guard-utils.mjs";
/** Create repo root and source root helpers for pairing guard scanners. */
export function createPairingGuardContext(importMetaUrl) {
+26
View File
@@ -4,6 +4,12 @@ import type {
ExecFileSyncOptionsWithStringEncoding,
} from "node:child_process";
type ExecGhReadImpl = (
command: string,
args: readonly string[],
options: ExecFileSyncOptions,
) => string | Uint8Array<ArrayBuffer>;
export function plainGhEnv(env?: NodeJS.ProcessEnv): {
[key: string]: string | undefined;
};
@@ -20,6 +26,26 @@ export function execPlainGh(
args: readonly string[],
options?: ExecFileSyncOptions,
): string | Uint8Array<ArrayBuffer>;
export function execGhRead(
args: readonly string[],
options: ExecFileSyncOptionsWithStringEncoding,
params?: { execFileSyncImpl?: ExecGhReadImpl },
): string;
export function execGhRead(
args: readonly string[],
options?: ExecFileSyncOptionsWithBufferEncoding,
params?: { execFileSyncImpl?: ExecGhReadImpl },
): Uint8Array<ArrayBuffer>;
export function execGhRead(
args: readonly string[],
options?: ExecFileSyncOptions,
params?: { execFileSyncImpl?: ExecGhReadImpl },
): string | Uint8Array<ArrayBuffer>;
export function execGhJson(
args: readonly string[],
options?: ExecFileSyncOptions,
params?: { execFileSyncImpl?: ExecGhReadImpl },
): unknown;
export function execGhApiRead(
endpoint: string,
options: ExecFileSyncOptionsWithStringEncoding,
+12 -3
View File
@@ -88,13 +88,22 @@ export function execPlainGh(args, options = {}) {
});
}
export function execGhApiRead(endpoint, options = {}) {
export function execGhRead(args, options = {}, params = {}) {
const env = plainGhEnv(options.env ?? process.env);
// Keep reads on the normal PATH shim; OPENCLAW_GH_BIN pins maintainer writes.
// Reads stay on the cache-aware PATH shim; the explicit binary is reserved for writes.
delete env.OPENCLAW_GH_BIN;
return execFileSync("gh", ["api", endpoint, "--method", "GET"], {
const execFileSyncImpl = params.execFileSyncImpl ?? execFileSync;
return execFileSyncImpl("gh", args, {
...options,
env,
maxBuffer: options.maxBuffer ?? PLAIN_GH_MAX_BUFFER_BYTES,
});
}
export function execGhJson(args, options = {}, params = {}) {
return JSON.parse(execGhRead(args, { ...options, encoding: "utf8" }, params));
}
export function execGhApiRead(endpoint, options = {}) {
return execGhRead(["api", endpoint, "--method", "GET"], options);
}
+2
View File
@@ -0,0 +1,2 @@
/** Resolves the repository root by walking upward from the caller module. */
export function resolveRepoRoot(importMetaUrl: string): string;
+20
View File
@@ -0,0 +1,20 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/** Resolves the repository root by walking upward from the caller module. */
export function resolveRepoRoot(importMetaUrl) {
let dir = path.dirname(fileURLToPath(importMetaUrl));
const { root } = path.parse(dir);
while (dir !== root) {
if (
existsSync(path.join(dir, ".git")) ||
(existsSync(path.join(dir, "package.json")) &&
existsSync(path.join(dir, "pnpm-workspace.yaml")))
) {
return dir;
}
dir = path.dirname(dir);
}
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
}
+22 -42
View File
@@ -1,17 +1,7 @@
// Parses report CLI output arguments and writes optional artifacts.
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
/**
* Parses shared `--root`, `--json`, and `--markdown` flags for report scripts.
*/
function readReportOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (value === undefined || value === "" || value.startsWith("-")) {
throw new Error(`Expected ${optionName} <value>.`);
}
return value;
}
import { parseFlagArgs, stringFlag } from "./arg-utils.mjs";
export function parseReportCliArgs(argv) {
const options = {
@@ -19,37 +9,27 @@ export function parseReportCliArgs(argv) {
jsonPath: null,
markdownPath: null,
};
const seen = new Set();
const setOnce = (flag, key, value) => {
if (seen.has(flag)) {
throw new Error(`${flag} was provided more than once.`);
}
seen.add(flag);
options[key] = value;
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") {
continue;
}
if (arg === "--root") {
setOnce(arg, "rootDir", readReportOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--json") {
setOnce(arg, "jsonPath", readReportOptionValue(argv, index, arg));
index += 1;
continue;
}
if (arg === "--markdown") {
setOnce(arg, "markdownPath", readReportOptionValue(argv, index, arg));
index += 1;
continue;
}
throw new Error(`Unsupported argument: ${arg}`);
}
return options;
return parseFlagArgs(
argv,
options,
[
["--root", "rootDir"],
["--json", "jsonPath"],
["--markdown", "markdownPath"],
].map(([flag, key]) =>
stringFlag(flag, key, {
allowInline: false,
missingValueMessage: `Expected ${flag} <value>.`,
rejectShortOptions: true,
}),
),
{
duplicateOptionMessage: (flag) => `${flag} was provided more than once.`,
onUnhandledArg(arg) {
throw new Error(`Unsupported argument: ${arg}`);
},
},
);
}
/**
-4
View File
@@ -1,7 +1,3 @@
/**
* Resolves the repository root by walking upward from the caller module.
*/
export function resolveRepoRoot(importMetaUrl: string): string;
/**
* Converts repo-relative source roots into absolute paths.
*/
+1 -20
View File
@@ -1,5 +1,5 @@
// Shared TypeScript AST and source-file helpers for guard scripts.
import { existsSync, promises as fs } from "node:fs";
import { promises as fs } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -14,25 +14,6 @@ function getTypeScript() {
const baseTestSuffixes = [".test.ts", ".test-utils.ts", ".test-harness.ts", ".e2e-harness.ts"];
/**
* Resolves the repository root by walking upward from the caller module.
*/
export function resolveRepoRoot(importMetaUrl) {
// Walk up from the caller's directory until we find the repo root (.git).
// This handles callers at any depth (scripts/*.mjs, scripts/lib/*.mjs, etc.)
// instead of assuming a fixed number of parent traversals.
let dir = path.dirname(fileURLToPath(importMetaUrl));
const { root } = path.parse(dir);
while (dir !== root) {
if (existsSync(path.join(dir, ".git"))) {
return dir;
}
dir = path.dirname(dir);
}
// Fallback: two levels up (original behavior).
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
}
/**
* Converts repo-relative source roots into absolute paths.
*/
+19 -15
View File
@@ -4,7 +4,8 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { booleanFlag, parseFlagArgs } from "./lib/arg-utils.mjs";
import {
deprecatedBarrelPluginSdkEntrypoints,
deprecatedPublicPluginSdkEntrypoints,
@@ -13,8 +14,9 @@ import {
privateLocalOnlyPluginSdkEntrypoints,
publicPluginSdkEntrypoints,
} from "./lib/plugin-sdk-entries.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const require = createRequire(import.meta.url);
let ts;
@@ -30,19 +32,21 @@ Options:
}
function parsePluginSdkSurfaceReportArgs(argv) {
const args = { check: false, help: false };
for (const arg of argv) {
if (arg === "--check") {
args.check = true;
continue;
}
if (arg === "--help" || arg === "-h") {
args.help = true;
continue;
}
throw new Error(`Unknown plugin SDK surface report option: ${arg}`);
}
return args;
return parseFlagArgs(
argv,
{ check: false, help: false },
[
booleanFlag("--check", "check", true, { repeatable: true }),
booleanFlag("--help", "help", true, { repeatable: true }),
booleanFlag("-h", "help", true, { repeatable: true }),
],
{
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`Unknown plugin SDK surface report option: ${arg}`);
},
},
);
}
const publicEntrypointSet = new Set(publicPluginSdkEntrypoints);
const localOnlyEntrypointSet = new Set(privateLocalOnlyPluginSdkEntrypoints);
@@ -12,9 +12,9 @@ import {
} from "./lib/local-heavy-check-runtime.mjs";
import { parsePositiveInt } from "./lib/numeric-options.mjs";
import { pluginSdkEntrypoints, productionPluginSdkEntrypoints } from "./lib/plugin-sdk-entries.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs";
const repoRoot = resolve(import.meta.dirname, "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const runTsgoScript = path.join(repoRoot, "scripts/run-tsgo.mjs");
const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]);
const VALID_MODES = new Set(["all", "package-boundary"]);
+2 -2
View File
@@ -12,8 +12,8 @@ import {
shouldAcquireLocalHeavyCheckLockForTsgo,
} from "./lib/local-heavy-check-runtime.mjs";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs";
const repoRoot = path.resolve(import.meta.dirname, "..");
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const repoRoot = resolveRepoRoot(import.meta.url);
const artifactRoot = path.resolve(repoRoot, ".artifacts/tsgo-profile");
const tsgoPath = resolveRepoToolBinPath("tsgo", { cwd: repoRoot });
+3 -2
View File
@@ -1,7 +1,8 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const MODEL_CATALOG_MIN_VERSION = "2026.7.0";
export const MODEL_CATALOG_MIN_MODELS = 200;
@@ -14,7 +15,7 @@ const PRICING_FETCH_TIMEOUT_MS = 60_000;
const MAX_PRICING_CATALOG_BYTES = 5 * 1024 * 1024;
const BUNDLE_SIZE_WARNING_BYTES = 2 * 1024 * 1024;
const CLIENT_BUNDLE_LIMIT_BYTES = 4 * 1024 * 1024;
const defaultRootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultRootDir = resolveRepoRoot(import.meta.url);
function requireOptionValue(args, index, flag) {
const value = args[index + 1]?.trim();
+41 -46
View File
@@ -4,8 +4,14 @@ import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
booleanFlag,
parseFlagArgs,
stringFlag,
stripLeadingPackageManagerSeparator,
} from "./lib/arg-utils.mjs";
interface Options {
type Options = {
beta: string;
model: string;
providerMode: string;
@@ -13,7 +19,7 @@ interface Options {
repo: string;
skipParallels: boolean;
skipTelegram: boolean;
}
};
export type RunOptions = {
capture?: boolean;
@@ -52,6 +58,8 @@ Options:
export function parseArgs(argv: string[]): Options {
const args = stripLeadingPackageManagerSeparator(argv);
const terminatorIndex = args.indexOf("--");
const cliArgs = terminatorIndex === -1 ? args : args.slice(0, terminatorIndex);
const options: Options = {
beta: "beta",
model: "openai/gpt-5.4",
@@ -61,39 +69,38 @@ export function parseArgs(argv: string[]): Options {
skipParallels: false,
skipTelegram: false,
};
parseArgv: for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--":
break parseArgv;
case "--beta":
options.beta = requireValue(args, ++i, arg);
break;
case "--model":
options.model = requireValue(args, ++i, arg);
break;
case "--provider-mode":
options.providerMode = requireValue(args, ++i, arg);
break;
case "--ref":
options.ref = requireValue(args, ++i, arg);
break;
case "--repo":
options.repo = requireValue(args, ++i, arg);
break;
case "--skip-parallels":
options.skipParallels = true;
break;
case "--skip-telegram":
options.skipTelegram = true;
break;
case "-h":
case "--help":
process.stdout.write(usage());
process.exit(0);
default:
const helpIndex = cliArgs.findIndex((arg) => arg === "-h" || arg === "--help");
parseFlagArgs(
helpIndex === -1 ? cliArgs : cliArgs.slice(0, helpIndex),
options,
[
...(
[
["--beta", "beta"],
["--model", "model"],
["--provider-mode", "providerMode"],
["--ref", "ref"],
["--repo", "repo"],
] as const
).map(([flag, key]) =>
stringFlag(flag, key, {
allowInline: false,
rejectShortOptions: true,
repeatable: true,
}),
),
booleanFlag("--skip-parallels", "skipParallels", true, { repeatable: true }),
booleanFlag("--skip-telegram", "skipTelegram", true, { repeatable: true }),
],
{
onUnhandledArg(arg) {
throw new Error(`unknown option: ${arg}`);
}
},
},
);
if (helpIndex !== -1) {
process.stdout.write(usage());
process.exit(0);
}
if (options.skipParallels && options.skipTelegram) {
throw new Error("--skip-parallels and --skip-telegram cannot be used together");
@@ -101,18 +108,6 @@ export function parseArgs(argv: string[]): Options {
return options;
}
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
return argv[0] === "--" ? argv.slice(1) : argv;
}
function requireValue(argv: string[], index: number, flag: string): string {
const value = argv[index];
if (!value || value.startsWith("-")) {
throw new Error(`${flag} requires a value`);
}
return value;
}
const CAPTURE_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
const DEFAULT_COMMAND_TIMEOUT_MS = readPositiveInt(
process.env.OPENCLAW_RELEASE_BETA_SMOKE_COMMAND_MS,
+51 -88
View File
@@ -17,7 +17,13 @@ import { tmpdir } from "node:os";
import { basename, dirname, join, resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url";
import { isDeepStrictEqual } from "node:util";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
import {
booleanFlag,
parseFlagArgs,
stringFlag,
stringListFlag,
stripLeadingPackageManagerSeparator,
} from "./lib/arg-utils.mjs";
import { readBoundedResponseText } from "./lib/bounded-response.mjs";
import {
dedicatedSectionVersionForTag,
@@ -109,14 +115,6 @@ Options:
`;
}
function requireValue(argv, index, flag) {
const value = argv[index];
if (!value || value.startsWith("-")) {
throw new Error(`${flag} requires a value`);
}
return value;
}
export function releaseBranchForTag(tag) {
if (tag.includes("-alpha.")) {
return "";
@@ -130,6 +128,8 @@ export function releaseBranchForTag(tag) {
*/
export function parseArgs(argv) {
const args = stripLeadingPackageManagerSeparator(argv);
const terminatorIndex = args.indexOf("--");
const cliArgs = terminatorIndex === -1 ? args : args.slice(0, terminatorIndex);
const options = {
repo: DEFAULT_REPO,
provider: DEFAULT_PROVIDER,
@@ -154,86 +154,49 @@ export function parseArgs(argv) {
windowsNodeInstallerDigests: "",
outputDir: "",
};
const seen = new Set();
const setOnce = (flag, key, value) => {
if (seen.has(flag)) {
throw new Error(`${flag} was provided more than once`);
}
seen.add(flag);
options[key] = value;
};
parseArgv: for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
switch (arg) {
case "--":
break parseArgv;
case "--tag":
setOnce(arg, "tag", requireValue(args, ++index, arg));
break;
case "--target-sha":
setOnce(arg, "targetSha", requireValue(args, ++index, arg));
break;
case "--workflow-ref":
setOnce(arg, "workflowRef", requireValue(args, ++index, arg));
break;
case "--repo":
setOnce(arg, "repo", requireValue(args, ++index, arg));
break;
case "--full-release-run":
setOnce(arg, "fullReleaseRunId", requireValue(args, ++index, arg));
break;
case "--npm-preflight-run":
setOnce(arg, "npmPreflightRunId", requireValue(args, ++index, arg));
break;
case "--windows-node-tag":
setOnce(arg, "windowsNodeTag", requireValue(args, ++index, arg));
break;
case "--skip-dispatch":
setOnce(arg, "skipDispatch", true);
break;
case "--skip-local-generated-check":
setOnce(arg, "skipLocalGeneratedCheck", true);
break;
case "--skip-parallels":
setOnce(arg, "skipParallels", true);
break;
case "--parallels-registry-package-artifact":
options.parallelsRegistryPackageArtifactDirs.push(requireValue(args, ++index, arg));
break;
case "--skip-telegram":
setOnce(arg, "skipTelegram", true);
break;
case "--telegram-provider-mode":
setOnce(arg, "telegramProviderMode", requireValue(args, ++index, arg));
break;
case "--provider":
setOnce(arg, "provider", requireValue(args, ++index, arg));
break;
case "--mode":
setOnce(arg, "mode", requireValue(args, ++index, arg));
break;
case "--release-profile":
setOnce(arg, "releaseProfile", requireValue(args, ++index, arg));
break;
case "--npm-dist-tag":
setOnce(arg, "npmDistTag", requireValue(args, ++index, arg));
break;
case "--plugin-publish-scope":
setOnce(arg, "pluginPublishScope", requireValue(args, ++index, arg));
break;
case "--plugins":
setOnce(arg, "plugins", requireValue(args, ++index, arg));
break;
case "--output-dir":
setOnce(arg, "outputDir", requireValue(args, ++index, arg));
break;
case "-h":
case "--help":
process.stdout.write(usage());
process.exit(0);
default:
const helpIndex = cliArgs.findIndex((arg) => arg === "-h" || arg === "--help");
parseFlagArgs(
helpIndex === -1 ? cliArgs : cliArgs.slice(0, helpIndex),
options,
[
...[
["--tag", "tag"],
["--target-sha", "targetSha"],
["--workflow-ref", "workflowRef"],
["--repo", "repo"],
["--full-release-run", "fullReleaseRunId"],
["--npm-preflight-run", "npmPreflightRunId"],
["--windows-node-tag", "windowsNodeTag"],
["--telegram-provider-mode", "telegramProviderMode"],
["--provider", "provider"],
["--mode", "mode"],
["--release-profile", "releaseProfile"],
["--npm-dist-tag", "npmDistTag"],
["--plugin-publish-scope", "pluginPublishScope"],
["--plugins", "plugins"],
["--output-dir", "outputDir"],
].map(([flag, key]) =>
stringFlag(flag, key, { allowInline: false, rejectShortOptions: true }),
),
stringListFlag(
"--parallels-registry-package-artifact",
"parallelsRegistryPackageArtifactDirs",
{ allowInline: false, rejectShortOptions: true },
),
booleanFlag("--skip-dispatch", "skipDispatch"),
booleanFlag("--skip-local-generated-check", "skipLocalGeneratedCheck"),
booleanFlag("--skip-parallels", "skipParallels"),
booleanFlag("--skip-telegram", "skipTelegram"),
],
{
onUnhandledArg(arg) {
throw new Error(`unknown option: ${arg}`);
}
},
},
);
if (helpIndex !== -1) {
process.stdout.write(usage());
process.exit(0);
}
if (!options.tag) {
throw new Error("--tag is required");
+46 -52
View File
@@ -13,11 +13,13 @@ import os from "node:os";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs";
import { resolveNpmRunner } from "./npm-runner.mjs";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const ROOT_DIR = resolveRepoRoot(import.meta.url);
const DEFAULT_OUTPUT_NAME = "openclaw-current.tgz";
const PACKAGE_URL_DOWNLOAD_TIMEOUT_MS = 60_000;
const PACKAGE_URL_MAX_BYTES = 250 * 1024 * 1024;
@@ -98,57 +100,49 @@ export function parseArgs(argv) {
trustedSourceId: "",
trustedSourcePolicy: TRUSTED_PACKAGE_SOURCE_POLICY,
};
const seen = new Set();
const setOnce = (flag, key, value) => {
if (seen.has(flag)) {
throw new Error(`${flag} was provided more than once`);
}
seen.add(flag);
options[key] = value;
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
const readValue = (name, readOptions = {}) => {
const value = argv[(index += 1)];
if (
value === undefined ||
(!readOptions.allowEmpty && value === "") ||
value.startsWith("-")
) {
throw new Error(`${name} requires a value`);
}
return value;
};
if (arg === "--artifact-dir") {
setOnce(arg, "artifactDir", readValue(arg));
} else if (arg === "--github-output") {
setOnce(arg, "githubOutput", readValue(arg));
} else if (arg === "--metadata") {
setOnce(arg, "metadata", readValue(arg));
} else if (arg === "--output-dir") {
setOnce(arg, "outputDir", readValue(arg));
} else if (arg === "--output-name") {
setOnce(arg, "outputName", readValue(arg));
} else if (arg === "--package-sha256") {
setOnce(arg, "packageSha256", readValue(arg, { allowEmpty: true }).toLowerCase());
} else if (arg === "--package-ref") {
setOnce(arg, "packageRef", readValue(arg, { allowEmpty: true }));
} else if (arg === "--package-spec") {
setOnce(arg, "packageSpec", readValue(arg, { allowEmpty: true }));
} else if (arg === "--package-url") {
setOnce(arg, "packageUrl", readValue(arg, { allowEmpty: true }));
} else if (arg === "--source") {
setOnce(arg, "source", readValue(arg));
} else if (arg === "--trusted-source-id") {
setOnce(arg, "trustedSourceId", readValue(arg, { allowEmpty: true }));
} else if (arg === "--trusted-source-policy") {
setOnce(arg, "trustedSourcePolicy", readValue(arg));
} else if (arg === "--help" || arg === "-h") {
options.help = true;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
parseFlagArgs(
argv,
options,
[
...[
["--artifact-dir", "artifactDir"],
["--github-output", "githubOutput"],
["--metadata", "metadata"],
["--output-dir", "outputDir"],
["--output-name", "outputName"],
["--source", "source"],
["--trusted-source-policy", "trustedSourcePolicy"],
].map(([flag, key]) =>
stringFlag(flag, key, { allowInline: false, rejectShortOptions: true }),
),
...[
["--package-ref", "packageRef"],
["--package-spec", "packageSpec"],
["--package-url", "packageUrl"],
["--trusted-source-id", "trustedSourceId"],
].map(([flag, key]) =>
stringFlag(flag, key, {
allowEmpty: true,
allowInline: false,
rejectShortOptions: true,
}),
),
stringFlag("--package-sha256", "packageSha256", {
allowEmpty: true,
allowInline: false,
rejectShortOptions: true,
transform: (value) => value.toLowerCase(),
}),
booleanFlag("--help", "help", true, { repeatable: true }),
booleanFlag("-h", "help", true, { repeatable: true }),
],
{
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`unknown argument: ${arg}`);
},
},
);
validateOutputName(options.outputName);
return options;
}
+2 -2
View File
@@ -4,9 +4,9 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const androidDir = path.join(repoRoot, "apps", "android");
const isMain = process.argv[1]
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+2 -2
View File
@@ -4,13 +4,13 @@ import { spawn } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { embeddedAgentVitestProjectOwners } from "../test/vitest/vitest.agents-paths.mjs";
import { toolingIsolatedTestFiles } from "../test/vitest/vitest.tooling-isolated-paths.mjs";
import { isUiTestTarget } from "../test/vitest/vitest.ui-paths.mjs";
import { boundaryTestFiles } from "../test/vitest/vitest.unit-paths.mjs";
import { runWithFailedTrailer, writeFailedTrailer } from "./lib/failed-trailer.mjs";
import { signalExitCode } from "./lib/managed-child-process.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { resolveLocalVitestEnv } from "./lib/vitest-local-scheduling.mjs";
import { spawnPnpmRunner } from "./pnpm-runner.mjs";
import {
@@ -127,7 +127,7 @@ const VITEST_DOTTED_OPTIONS_WITH_VALUE_PREFIXES = [
"--typecheck.",
];
const require = createRequire(import.meta.url);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const testProjectsRunnerPath = path.join(repoRoot, "scripts", "test-projects.mjs");
function isTruthyEnvValue(value) {
+3 -2
View File
@@ -4,11 +4,12 @@ import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { buildSync } from "esbuild";
import { copyBundledPluginMetadata } from "./copy-bundled-plugin-metadata.mjs";
import { assertRealOutputRoot } from "./lib/output-root-guard.mjs";
import { escapeRegExp } from "./lib/regexp.mjs";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import {
copyStaticExtensionAssets,
copyStaticExtensionAssetsToRuntimeOverlay,
@@ -21,7 +22,7 @@ import { writeOfficialChannelCatalog } from "./write-official-channel-catalog.mj
/** @internal Shared repository-script contract. */
export { listStaticExtensionAssetOutputs };
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const ROOT = resolveRepoRoot(import.meta.url);
const ROOT_RUNTIME_ALIAS_PATTERN = /^(?<base>.+\.(?:runtime|contract))-[A-Za-z0-9_-]+\.js$/u;
const ROOT_STABLE_RUNTIME_ALIAS_PATTERN = /^.+\.(?:runtime|contract)\.js$/u;
const ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN =
+3 -3
View File
@@ -5,9 +5,9 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
import { pathToFileURL } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
const rootDir = resolveRepoRoot(import.meta.url);
const REQUIRED_RESOURCE_FILES = ["a2ui.bundle.js", "index.html"];
export function getNativeA2uiResourcePaths(repoRoot = rootDir) {
+3 -2
View File
@@ -3,13 +3,14 @@ import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { pathToFileURL } from "node:url";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { installProcessWarningFilter } from "./process-warning-filter.mjs";
import { stageBundledPluginRuntime } from "./stage-bundled-plugin-runtime.mjs";
installProcessWarningFilter();
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const smokeEntryPath = path.join(repoRoot, "dist", "plugins", "build-smoke-entry.js");
assert.ok(fs.existsSync(smokeEntryPath), `missing build output: ${smokeEntryPath}`);
+38 -50
View File
@@ -2,6 +2,7 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { execGhApiRead, plainGhEnv } from "./lib/plain-gh.mjs";
@@ -31,14 +32,6 @@ const MAX_CI_REUSE_CANDIDATES = 5;
const CI_REUSE_RUN_LIST_LIMIT = 50;
const GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith("-")) {
throw new Error(`Expected ${optionName} <value>.`);
}
return value;
}
export function parseArgs(argv) {
const args = {
repo: "",
@@ -48,49 +41,44 @@ export function parseArgs(argv) {
output: "",
changelogOnly: false,
};
const seen = new Set();
const setOnce = (flag, key, value) => {
if (seen.has(flag)) {
throw new Error(`${flag} was provided more than once.`);
}
seen.add(flag);
args[key] = value;
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case "--repo":
setOnce(arg, "repo", readOptionValue(argv, index, arg));
index += 1;
break;
case "--sha":
setOnce(arg, "sha", readOptionValue(argv, index, arg));
index += 1;
break;
case "--pr": {
const value = Number(readOptionValue(argv, index, arg));
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error("Expected --pr <positive-integer>.");
}
setOnce(arg, "pr", value);
index += 1;
break;
}
case "--recent-sha":
setOnce(arg, "recentSha", readOptionValue(argv, index, arg));
index += 1;
break;
case "--output":
setOnce(arg, "output", readOptionValue(argv, index, arg));
index += 1;
break;
case "--changelog-only":
setOnce(arg, "changelogOnly", true);
break;
default:
parseFlagArgs(
argv,
args,
[
...[
["--repo", "repo"],
["--sha", "sha"],
["--recent-sha", "recentSha"],
["--output", "output"],
].map(([flag, key]) =>
stringFlag(flag, key, {
allowInline: false,
missingValueMessage: `Expected ${flag} <value>.`,
rejectShortOptions: true,
}),
),
stringFlag("--pr", "pr", {
allowInline: false,
missingValueMessage: "Expected --pr <value>.",
rejectShortOptions: true,
transform(value) {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
throw new Error("Expected --pr <positive-integer>.");
}
return parsed;
},
}),
booleanFlag("--changelog-only", "changelogOnly"),
],
{
duplicateOptionMessage: (flag) => `${flag} was provided more than once.`,
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`Unknown option: ${arg}`);
}
}
},
},
);
if (!args.repo || !args.sha || !args.pr || !args.output) {
throw new Error(
"Usage: node scripts/verify-pr-hosted-gates.mjs --repo <owner/repo> --sha <sha> --pr <number> [--recent-sha <sha>] --output <path>",
+15 -9
View File
@@ -1,5 +1,6 @@
// Runs the broad verification graph used by Crabbox/Testbox: check then test.
import { performance } from "node:perf_hooks";
import { booleanFlag, parseFlagArgs } from "./lib/arg-utils.mjs";
import { formatMs, printTimingSummary } from "./lib/check-timing-summary.mjs";
import { runManagedCommand } from "./lib/managed-child-process.mjs";
@@ -26,15 +27,20 @@ function usage() {
* Parses verify wrapper CLI args.
*/
function parseVerifyArgs(argv) {
const args = { help: false };
for (const arg of argv) {
if (arg === "--help" || arg === "-h") {
args.help = true;
} else {
throw new Error(`unknown argument: ${arg}\n\n${usage()}`);
}
}
return args;
return parseFlagArgs(
argv,
{ help: false },
[
booleanFlag("--help", "help", true, { repeatable: true }),
booleanFlag("-h", "help", true, { repeatable: true }),
],
{
ignoreDoubleDash: false,
onUnhandledArg(arg) {
throw new Error(`unknown argument: ${arg}\n\n${usage()}`);
},
},
);
}
async function runStage(stage) {
+16 -15
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { setTimeout as sleep } from "node:timers/promises";
import { parseArgs as parseNodeArgs } from "node:util";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { execGhJson } from "./lib/plain-gh.mjs";
const USAGE =
"Usage: node scripts/watch-pr-ci.mjs <pr-number> <head-sha> [--repo owner/repo] [--after run-id] [--attach-timeout 900] [--timeout 3600] [--interval 120]";
@@ -15,6 +15,10 @@ const FAILURE_CONCLUSIONS = new Set([
"TIMED_OUT",
]);
const ROLLUP_QUERY = `query($owner:String!,$name:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$pr){state mergeable headRefOid statusCheckRollup{state contexts(first:100,after:$cursor){totalCount pageInfo{hasNextPage endCursor} nodes{kind:__typename ... on CheckRun{name status conclusion databaseId checkSuite{workflowRun{databaseId workflow{databaseId}}}} ... on StatusContext{context state}}}}}}}`;
const GH_READ_OPTIONS = {
stdio: ["ignore", "pipe", "pipe"],
timeout: 60_000,
};
// Adapted from Node's MIT-licensed util.stripVTControlCharacters implementation.
const ANSI_ESCAPE_SEQUENCE = new RegExp(
"[\\u001B\\u009B][[\\]()#;?]*" +
@@ -207,18 +211,11 @@ export function classifyRollup(rollup) {
return { verdict: "PENDING", pendingCount, failingNames: [], supersededCount };
}
function ghJson(...args) {
return JSON.parse(
execFileSync("gh", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 60_000,
}),
);
}
const readPr = (pr, repo) =>
ghJson(...`pr view ${pr} --repo ${repo} --json state,mergeable,headRefOid`.split(" "));
execGhJson(
`pr view ${pr} --repo ${repo} --json state,mergeable,headRefOid`.split(" "),
GH_READ_OPTIONS,
);
export const buildFindRunArgs = (repo, sha) => [
"run",
"list",
@@ -237,9 +234,13 @@ export const buildFindRunArgs = (repo, sha) => [
];
export const selectRunAfter = (runs, after) =>
runs.find((run) => after === undefined || run.databaseId > after);
const findRun = (repo, sha, after) => selectRunAfter(ghJson(...buildFindRunArgs(repo, sha)), after);
const findRun = (repo, sha, after) =>
selectRunAfter(execGhJson(buildFindRunArgs(repo, sha), GH_READ_OPTIONS), after);
const readRun = (repo, runId) =>
ghJson(...`run view ${runId} --repo ${repo} --json status,conclusion`.split(" "));
execGhJson(
`run view ${runId} --repo ${repo} --json status,conclusion`.split(" "),
GH_READ_OPTIONS,
);
export function classifyRunAttachment(runId, run, after) {
if (run.conclusion === "skipped") {
@@ -317,7 +318,7 @@ function readRollup(pr, repo) {
if (cursor !== null) {
queryArgs.push("-f", `cursor=${cursor}`);
}
return ghJson(...queryArgs).data?.repository?.pullRequest;
return execGhJson(queryArgs, GH_READ_OPTIONS).data?.repository?.pullRequest;
});
}
+25
View File
@@ -38,6 +38,31 @@ describe("scripts/lib/arg-utils parseFlagArgs", () => {
expect(parsed.match).toEqual(["alpha", "beta"]);
});
it("supports split-only, empty, transformed, and last-value-wins string contracts", () => {
expect(() =>
parseFlagArgs(["--value=inline"], { value: "" }, [
stringFlag("--value", "value", { allowInline: false }),
]),
).toThrow("Unknown option: --value=inline");
expect(
parseFlagArgs(["--value", "", "--value", "SECOND"], { value: "" }, [
stringFlag("--value", "value", {
allowEmpty: true,
repeatable: true,
transform: (value) => value.toLowerCase(),
}),
]).value,
).toBe("second");
});
it("supports idempotent boolean flags", () => {
expect(
parseFlagArgs(["--verbose", "--verbose"], { verbose: false }, [
booleanFlag("--verbose", "verbose", true, { repeatable: true }),
]).verbose,
).toBe(true);
});
it("rejects duplicate single-value flags", () => {
expect(() =>
parseFlagArgs(["--label", "first", "--label=second"], { label: "" }, [
+8 -6
View File
@@ -21,13 +21,15 @@ describe("scripts/check", () => {
});
it("rejects unknown args before running check stages", () => {
const result = runCheck("--bogus");
for (const args of [["--bogus"], ["bogus", "--help"]]) {
const result = runCheck(...args);
expect(result.status).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("unknown argument: --bogus");
expect(result.stderr).toContain("Usage: node scripts/check.mjs");
expect(result.stderr).not.toContain("[check]");
expect(result.status).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr).toContain(`unknown argument: ${args[0]}`);
expect(result.stderr).toContain("Usage: node scripts/check.mjs");
expect(result.stderr).not.toContain("[check]");
}
});
it("runs pnpm commands through the managed child runner", async () => {
@@ -9,7 +9,6 @@ import {
releaseEvidenceVerificationArgs,
releaseEvidenceVerifierPath,
resolveRemoteTargetRefSha,
runGhRead,
shouldDeleteTemporaryWorkflowRef,
} from "../../scripts/full-release-validation-at-sha.mjs";
@@ -163,30 +162,9 @@ describe("full-release-validation-at-sha", () => {
});
it("bounds GitHub reads without applying a timeout to workflow dispatch", () => {
const calls: unknown[][] = [];
expect(
runGhRead(["api", "repos/openclaw/openclaw/actions/runs/123"], {
execFileSyncImpl: (...args: unknown[]) => {
calls.push(args);
return " result ";
},
}),
).toBe("result");
expect(calls).toEqual([
[
"gh",
["api", "repos/openclaw/openclaw/actions/runs/123"],
expect.objectContaining({
killSignal: "SIGKILL",
timeout: 60_000,
}),
],
]);
const source = readFileSync("scripts/full-release-validation-at-sha.mjs", "utf8");
expect(source).toContain(
'runGhRead(["api", `repos/openclaw/openclaw/actions/runs/${parentRunId}`])',
);
expect(source).toContain("timeout: GH_READ_TIMEOUT_MS");
expect(source.match(/GH_READ_OPTIONS/gu)).toHaveLength(3);
expect(source).toContain('const dispatchOutput = run("gh", dispatchArgs');
});
@@ -206,12 +206,14 @@ describe("generate-dependency-release-evidence", () => {
});
it("reports CLI argument errors without a Node stack trace", () => {
const result = runCli("--wat");
for (const args of [["--wat"], ["wat", "--help"]]) {
const result = runCli(...args);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("Unsupported argument: --wat");
expectNoNodeStack(result.stderr);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe(`Unsupported argument: ${args[0]}`);
expectNoNodeStack(result.stderr);
}
});
it("falls back to fetching tags when local previous-release resolution misses", () => {
+55
View File
@@ -6,6 +6,8 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
execGhApiRead,
execGhJson,
execGhRead,
execPlainGh,
plainGhEnv,
PLAIN_GH_SYSTEM_CANDIDATES,
@@ -114,6 +116,59 @@ describe("plain gh helpers", () => {
expect(output).toContain("OPENCLAW_GH_BIN_SET=");
});
it("shares bounded PATH-shim reads and JSON parsing", () => {
const calls: unknown[][] = [];
const execFileSyncImpl = (...args: unknown[]) => {
calls.push(args);
return '{"ok":true}';
};
expect(
execGhJson(
["api", "repos/openclaw/openclaw"],
{
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "inherit"],
timeout: 60_000,
},
{ execFileSyncImpl },
),
).toEqual({ ok: true });
expect(calls).toEqual([
[
"gh",
["api", "repos/openclaw/openclaw"],
expect.objectContaining({
encoding: "utf8",
killSignal: "SIGKILL",
maxBuffer: 32 * 1024 * 1024,
stdio: ["ignore", "pipe", "inherit"],
timeout: 60_000,
}),
],
]);
expect(
execGhRead(
["api", "rate_limit"],
{ encoding: "utf8" },
{ execFileSyncImpl: () => " result " },
),
).toBe(" result ");
const failure = new Error("gh read failed");
expect(() =>
execGhRead(
["api", "rate_limit"],
{},
{
execFileSyncImpl: () => {
throw failure;
},
},
),
).toThrow(failure);
});
it("runs the shell helper with color disabled", () => {
const ghPath = makeFakeGh();
const outputPath = path.join(path.dirname(path.dirname(ghPath)), "output.txt");
@@ -107,6 +107,11 @@ describe("plugin npm extended-stable workflow", () => {
it("overlays the complete trusted packaging helper dependency set", () => {
const parsed = workflow();
const lockGenerator = readFileSync("scripts/generate-npm-package-lock.mjs", "utf8");
expect(lockGenerator).toContain(
'path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")',
);
expect(lockGenerator).not.toContain("./lib/repo-root.mjs");
const preflightCheckout = step(
parsed.jobs?.preview_plugin_pack,
"Checkout trusted packaging helper",
+14 -12
View File
@@ -59,19 +59,21 @@ describe("plugin SDK surface report", () => {
});
it("rejects unknown CLI options before collecting SDK stats", () => {
const result = spawnSync(
process.execPath,
["scripts/plugin-sdk-surface-report.mjs", "--chekc"],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
for (const args of [["--chekc"], ["chekc", "--help"]]) {
const result = spawnSync(
process.execPath,
["scripts/plugin-sdk-surface-report.mjs", ...args],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("Unknown plugin SDK surface report option: --chekc");
expect(result.stderr).not.toContain("at ");
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe(`Unknown plugin SDK surface report option: ${args[0]}`);
expect(result.stderr).not.toContain("at ");
}
});
it("prints help before collecting SDK stats", () => {
+7 -1
View File
@@ -4,7 +4,7 @@ import { testForceTesting } from "../../scripts/test-force.js";
describe("scripts/test-force.ts", () => {
it("prints help without clearing ports or running tests", () => {
const args = testForceTesting.parseArgs(["--help"]);
const args = testForceTesting.parseArgs(["--help", "--bogus"]);
expect(args).toEqual({ help: true });
expect(testForceTesting.usage()).toContain("Usage: node --import tsx scripts/test-force.ts");
@@ -16,5 +16,11 @@ describe("scripts/test-force.ts", () => {
expect(() => testForceTesting.parseArgs(["--bogus"])).toThrow(
/unknown argument: --bogus[\s\S]*Usage: node --import tsx scripts\/test-force\.ts/u,
);
expect(() => testForceTesting.parseArgs(["bogus"])).toThrow(
/unknown argument: bogus[\s\S]*Usage: node --import tsx scripts\/test-force\.ts/u,
);
expect(() => testForceTesting.parseArgs(["bogus", "--help"])).toThrow(
/unknown argument: bogus[\s\S]*Usage: node --import tsx scripts\/test-force\.ts/u,
);
});
});
+1
View File
@@ -1931,6 +1931,7 @@ describe("scripts/test-projects changed-target routing", () => {
"scripts/lib/ts-topology/analyze.ts": ["test/scripts/ts-topology.test.ts"],
"scripts/lib/ts-topology/reports.ts": ["test/scripts/ts-topology.test.ts"],
"scripts/lib/ts-topology/scope.ts": ["test/scripts/ts-topology.test.ts"],
"scripts/lib/repo-root.mjs": ["test/scripts/ts-guard-utils.test.ts"],
"scripts/lib/ts-guard-utils.mjs": ["test/scripts/ts-guard-utils.test.ts"],
"scripts/lib/tsgo-sparse-guard.mjs": [
"test/scripts/run-tsgo.test.ts",
+18 -2
View File
@@ -1,9 +1,10 @@
// Ts Guard Utils tests cover ts guard utils script behavior.
import { existsSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import { resolveRepoRoot } from "../../scripts/lib/ts-guard-utils.mjs";
import { resolveRepoRoot } from "../../scripts/lib/repo-root.mjs";
/**
* Regression tests for resolveRepoRoot().
@@ -49,4 +50,19 @@ describe("resolveRepoRoot", () => {
expect(fromLib).toBe(fromScripts);
expect(fromScripts).toBe(fromExtension);
});
it("resolves an unpacked workspace without git metadata", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-repo-root-"));
try {
mkdirSync(path.join(root, "scripts", "nested"), { recursive: true });
writeFileSync(path.join(root, "package.json"), '{"name":"openclaw"}\n');
writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages: []\n");
expect(
resolveRepoRoot(pathToFileURL(path.join(root, "scripts", "nested", "tool.mjs")).href),
).toBe(root);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
+9 -7
View File
@@ -21,13 +21,15 @@ describe("scripts/verify", () => {
});
it("rejects unknown args before running verify stages", () => {
const result = runVerify("--bogus");
for (const args of [["--bogus"], ["bogus", "--help"]]) {
const result = runVerify(...args);
expect(result.status).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("unknown argument: --bogus");
expect(result.stderr).toContain("Usage: node scripts/verify.mjs");
expect(result.stderr).not.toContain("CRABBOX_PHASE:");
expect(result.stderr).not.toContain("[verify]");
expect(result.status).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr).toContain(`unknown argument: ${args[0]}`);
expect(result.stderr).toContain("Usage: node scripts/verify.mjs");
expect(result.stderr).not.toContain("CRABBOX_PHASE:");
expect(result.stderr).not.toContain("[verify]");
}
});
});