diff --git a/scripts/ensure-cli-startup-build.mjs b/scripts/ensure-cli-startup-build.mjs index b8d32943faed..879960b67491 100644 --- a/scripts/ensure-cli-startup-build.mjs +++ b/scripts/ensure-cli-startup-build.mjs @@ -5,32 +5,18 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(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; -function positiveEnvInt(name, env, fallback) { - const raw = env[name]?.trim(); - if (raw === undefined || raw === "") { - return fallback; - } - if (!/^[1-9]\d*$/.test(raw)) { - throw new Error(`invalid ${name}: ${raw}`); - } - const value = Number(raw); - if (!Number.isSafeInteger(value)) { - throw new Error(`invalid ${name}: ${raw}`); - } - return value; -} - /** * Resolves the CLI startup build timeout from environment. */ export function resolveCliStartupBuildTimeoutMs(env = process.env) { - return positiveEnvInt("OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS", env, DEFAULT_BUILD_TIMEOUT_MS); + return readPositiveEnvInt("OPENCLAW_CLI_STARTUP_BUILD_TIMEOUT_MS", env, DEFAULT_BUILD_TIMEOUT_MS); } /** diff --git a/scripts/ensure-extension-memory-build.mjs b/scripts/ensure-extension-memory-build.mjs index da0e3dafbef6..533623d4da9c 100644 --- a/scripts/ensure-extension-memory-build.mjs +++ b/scripts/ensure-extension-memory-build.mjs @@ -9,30 +9,16 @@ 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)), ".."); const DEFAULT_BUILD_TIMEOUT_MS = 10 * 60 * 1000; -function positiveEnvInt(name, env, fallback) { - const raw = env[name]?.trim(); - if (raw === undefined || raw === "") { - return fallback; - } - if (!/^[1-9]\d*$/.test(raw)) { - throw new Error(`invalid ${name}: ${raw}`); - } - const value = Number(raw); - if (!Number.isSafeInteger(value)) { - throw new Error(`invalid ${name}: ${raw}`); - } - return value; -} - /** * Resolves the extension memory build timeout from environment. */ export function resolveExtensionMemoryBuildTimeoutMs(env = process.env) { - return positiveEnvInt( + return readPositiveEnvInt( "OPENCLAW_EXTENSION_MEMORY_BUILD_TIMEOUT_MS", env, DEFAULT_BUILD_TIMEOUT_MS, diff --git a/scripts/lib/npm-verify-exec.ts b/scripts/lib/npm-verify-exec.ts index c263d93303db..04c46fbcc36c 100644 --- a/scripts/lib/npm-verify-exec.ts +++ b/scripts/lib/npm-verify-exec.ts @@ -1,5 +1,6 @@ // Npm Verify Exec script supports OpenClaw repository automation. import { execFileSync, type ExecFileSyncOptionsWithStringEncoding } from "node:child_process"; +import { readPositiveEnvInt } from "./numeric-options.mjs"; export type NpmVerifyCommandInvocation = { command: string; @@ -14,21 +15,6 @@ type NpmVerifyExecOptions = ExecFileSyncOptionsWithStringEncoding & { const DEFAULT_NPM_VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1000; const DEFAULT_NPM_VERIFY_COMMAND_MAX_BUFFER_BYTES = 16 * 1024 * 1024; -function positiveEnvInt(name: string, fallback: number): number { - const raw = process.env[name]?.trim(); - if (raw === undefined || raw === "") { - return fallback; - } - if (!/^[1-9]\d*$/u.test(raw)) { - throw new Error(`invalid ${name}: ${raw}`); - } - const value = Number(raw); - if (!Number.isSafeInteger(value)) { - throw new Error(`invalid ${name}: ${raw}`); - } - return value; -} - export function runNpmVerifyCommand( invocation: NpmVerifyCommandInvocation, cwd: string, @@ -36,11 +22,16 @@ export function runNpmVerifyCommand( ): string { const timeoutMs = options.timeoutMs ?? - positiveEnvInt("OPENCLAW_NPM_VERIFY_COMMAND_TIMEOUT_MS", DEFAULT_NPM_VERIFY_COMMAND_TIMEOUT_MS); + readPositiveEnvInt( + "OPENCLAW_NPM_VERIFY_COMMAND_TIMEOUT_MS", + process.env, + DEFAULT_NPM_VERIFY_COMMAND_TIMEOUT_MS, + ); const maxBuffer = options.maxBufferBytes ?? - positiveEnvInt( + readPositiveEnvInt( "OPENCLAW_NPM_VERIFY_COMMAND_MAX_BUFFER_BYTES", + process.env, DEFAULT_NPM_VERIFY_COMMAND_MAX_BUFFER_BYTES, ); diff --git a/scripts/lib/numeric-options.d.mts b/scripts/lib/numeric-options.d.mts index f5f9606b5ef9..5ca044614f88 100644 --- a/scripts/lib/numeric-options.d.mts +++ b/scripts/lib/numeric-options.d.mts @@ -1,3 +1,4 @@ export function parsePositiveInt(raw: string, label: string): number; +export function readPositiveEnvInt(name: string, env: NodeJS.ProcessEnv, fallback: number): number; export function parseNonNegativeInt(raw: string, label: string): number; export function parsePositiveNumber(raw: string, label: string): number; diff --git a/scripts/lib/numeric-options.mjs b/scripts/lib/numeric-options.mjs index 339cdb9e5ee3..f0263c0a2ae8 100644 --- a/scripts/lib/numeric-options.mjs +++ b/scripts/lib/numeric-options.mjs @@ -12,6 +12,22 @@ export function parsePositiveInt(raw, label) { return value; } +/** Read a safe positive integer from an environment variable. */ +export function readPositiveEnvInt(name, env, fallback) { + const raw = env[name]?.trim(); + if (raw === undefined || raw === "") { + return fallback; + } + if (!/^[1-9]\d*$/u.test(raw)) { + throw new Error(`invalid ${name}: ${raw}`); + } + const value = Number(raw); + if (!Number.isSafeInteger(value)) { + throw new Error(`invalid ${name}: ${raw}`); + } + return value; +} + /** Parse a safe non-negative integer option. */ export function parseNonNegativeInt(raw, label) { const text = String(raw).trim(); diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 21147383d653..9b930efa3f2d 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -6,6 +6,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase } from "./lib/npm-publish-plan.mjs"; +import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; import { LOCAL_BUILD_METADATA_DIST_PATHS, PACKAGE_DIST_INVENTORY_RELATIVE_PATH, @@ -280,25 +281,10 @@ export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null return null; } -function positiveEnvInt(name: string, env: NodeJS.ProcessEnv, fallback: number): number { - const raw = env[name]?.trim(); - if (raw === undefined || raw === "") { - return fallback; - } - if (!/^[1-9]\d*$/u.test(raw)) { - throw new Error(`invalid ${name}: ${raw}`); - } - const value = Number(raw); - if (!Number.isSafeInteger(value)) { - throw new Error(`invalid ${name}: ${raw}`); - } - return value; -} - export function resolveNpmReleaseCheckCommandTimeoutMs( env: NodeJS.ProcessEnv = process.env, ): number { - return positiveEnvInt( + return readPositiveEnvInt( "OPENCLAW_NPM_RELEASE_CHECK_COMMAND_TIMEOUT_MS", env, DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS, diff --git a/scripts/openclaw-prepack.ts b/scripts/openclaw-prepack.ts index 885560164e86..94d4b065f802 100644 --- a/scripts/openclaw-prepack.ts +++ b/scripts/openclaw-prepack.ts @@ -6,6 +6,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs"; import { basename, delimiter, join } from "node:path"; import { pathToFileURL } from "node:url"; import { formatErrorMessage } from "../src/infra/errors.ts"; +import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; import { writePackageDistInventoryForPublish } from "./lib/package-dist-inventory.ts"; import { preparePackageChangelog } from "./package-changelog.mjs"; import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs"; @@ -175,23 +176,8 @@ function ensurePreparedArtifacts(): void { process.exit(1); } -function positiveEnvInt(name: string, env: NodeJS.ProcessEnv, fallback: number): number { - const raw = env[name]?.trim(); - if (raw === undefined || raw === "") { - return fallback; - } - if (!/^[1-9]\d*$/u.test(raw)) { - throw new Error(`invalid ${name}: ${raw}`); - } - const value = Number(raw); - if (!Number.isSafeInteger(value)) { - throw new Error(`invalid ${name}: ${raw}`); - } - return value; -} - export function resolvePrepackCommandTimeoutMs(env: NodeJS.ProcessEnv = process.env): number { - return positiveEnvInt( + return readPositiveEnvInt( "OPENCLAW_PREPACK_COMMAND_TIMEOUT_MS", env, DEFAULT_PREPACK_COMMAND_TIMEOUT_MS, diff --git a/scripts/release-check.ts b/scripts/release-check.ts index b6cd7a2e5dbd..6fbc11cc74bd 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -32,6 +32,7 @@ import { listBundledPluginPackArtifacts, } from "./lib/bundled-plugin-build-entries.mjs"; import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs"; +import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; import { isLegacyPluginDependencyInstallStagePath, LOCAL_BUILD_METADATA_DIST_PATHS, @@ -201,21 +202,6 @@ const PACKED_PLUGIN_SDK_TYPESCRIPT_SMOKE_FIXTURE = resolve( "scripts/fixtures/packed-plugin-sdk-type-smoke.ts", ); -function positiveEnvInt(name: string, fallback: number): number { - const raw = process.env[name]?.trim(); - if (raw === undefined || raw === "") { - return fallback; - } - if (!/^[1-9]\d*$/u.test(raw)) { - throw new Error(`invalid ${name}: ${raw}`); - } - const value = Number(raw); - if (!Number.isSafeInteger(value)) { - throw new Error(`invalid ${name}: ${raw}`); - } - return value; -} - export function runReleaseCheckCommand( invocation: ReleaseCheckCommandInvocation, options: { @@ -235,16 +221,18 @@ export function runReleaseCheckCommand( killSignal: "SIGKILL", maxBuffer: options.maxBuffer ?? - positiveEnvInt( + readPositiveEnvInt( "OPENCLAW_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES", + process.env, DEFAULT_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES, ), shell: invocation.shell ?? options.shell, stdio: options.stdio, timeout: options.timeoutMs ?? - positiveEnvInt( + readPositiveEnvInt( "OPENCLAW_RELEASE_CHECK_COMMAND_TIMEOUT_MS", + process.env, DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS, ), windowsVerbatimArguments: invocation.windowsVerbatimArguments, diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index a5a07c43ed1c..fbe1d9b215c0 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -1360,6 +1360,8 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ], ], ["scripts/lib/npm-verify-exec.ts", ["test/scripts/npm-verify-exec.test.ts"]], + ["scripts/lib/numeric-options.mjs", ["test/scripts/numeric-options.test.ts"]], + ["scripts/lib/numeric-options.d.mts", ["test/scripts/numeric-options.test.ts"]], ["scripts/lib/openclaw-test-state.mjs", ["test/scripts/openclaw-test-state.test.ts"]], [ "scripts/lib/workspace-bootstrap-smoke.mjs", diff --git a/test/scripts/numeric-options.test.ts b/test/scripts/numeric-options.test.ts new file mode 100644 index 000000000000..cba1c8251d66 --- /dev/null +++ b/test/scripts/numeric-options.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { readPositiveEnvInt } from "../../scripts/lib/numeric-options.mjs"; + +describe("readPositiveEnvInt", () => { + it("uses the fallback for missing or blank values", () => { + expect(readPositiveEnvInt("LIMIT", {}, 42)).toBe(42); + expect(readPositiveEnvInt("LIMIT", { LIMIT: " " }, 42)).toBe(42); + }); + + it("reads strict positive safe integers", () => { + expect(readPositiveEnvInt("LIMIT", { LIMIT: " 123 " }, 42)).toBe(123); + }); + + it.each(["0", "-1", "1.5", "1e3", "0x10", "9007199254740992"])( + "rejects invalid value %s", + (raw) => { + expect(() => readPositiveEnvInt("LIMIT", { LIMIT: raw }, 42)).toThrow( + `invalid LIMIT: ${raw}`, + ); + }, + ); +});