mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics. Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit. * fix: preserve standalone script coercions Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
committed by
GitHub
parent
66fe424590
commit
b080dd1e76
@@ -0,0 +1,69 @@
|
||||
type StringOptions = {
|
||||
allowEmpty?: boolean;
|
||||
allowInline?: boolean;
|
||||
missingValueMessage?: string;
|
||||
rejectShortOptions?: boolean;
|
||||
repeatable?: boolean;
|
||||
transform?: (value: string) => unknown;
|
||||
};
|
||||
|
||||
type ConsumedFlag<T extends Record<string, unknown>> = {
|
||||
flag?: string;
|
||||
nextIndex: number;
|
||||
repeatable?: boolean;
|
||||
apply(target: T): void;
|
||||
};
|
||||
|
||||
type FlagSpec<T extends Record<string, unknown>> = {
|
||||
consume(argv: readonly string[], index: number, args: T): ConsumedFlag<T> | null;
|
||||
};
|
||||
|
||||
type ParseOptions<T extends Record<string, unknown>> = {
|
||||
allowUnknownOptions?: boolean;
|
||||
duplicateOptionMessage?: (flag: string) => string;
|
||||
ignoreDoubleDash?: boolean;
|
||||
onUnhandledArg?: (arg: string, args: T) => "handled" | void;
|
||||
};
|
||||
|
||||
export type BoundedUnsignedDecimalResult =
|
||||
| { kind: "syntax" }
|
||||
| { kind: "below" }
|
||||
| { kind: "above" }
|
||||
| { kind: "value"; value: number };
|
||||
|
||||
export function readFlagValue(args: readonly string[], name: string): string | undefined;
|
||||
export function stripLeadingPackageManagerSeparator(argv: string[]): string[];
|
||||
export function parseStrictBooleanArg(value: unknown, label: string): boolean;
|
||||
export function classifyBoundedUnsignedDecimal(
|
||||
value: unknown,
|
||||
min: number,
|
||||
max: number,
|
||||
): BoundedUnsignedDecimalResult;
|
||||
export function parsePermissiveBooleanToken(value: unknown): boolean | undefined;
|
||||
export function stringFlag<T extends Record<string, unknown>>(
|
||||
flag: string,
|
||||
key: string,
|
||||
options?: StringOptions,
|
||||
): FlagSpec<T>;
|
||||
export function stringListFlag<T extends Record<string, unknown>>(
|
||||
flag: string,
|
||||
key: string,
|
||||
options?: Omit<StringOptions, "repeatable" | "transform">,
|
||||
): FlagSpec<T>;
|
||||
export function intFlag<T extends Record<string, unknown>>(
|
||||
flag: string,
|
||||
key: string,
|
||||
options?: { min?: number },
|
||||
): FlagSpec<T>;
|
||||
export function booleanFlag<T extends Record<string, unknown>>(
|
||||
flag: string,
|
||||
key: string,
|
||||
value?: unknown,
|
||||
options?: { repeatable?: boolean },
|
||||
): FlagSpec<T>;
|
||||
export function parseFlagArgs<T extends Record<string, unknown>>(
|
||||
argv: readonly string[],
|
||||
args: T,
|
||||
specs: readonly FlagSpec<T>[],
|
||||
options?: ParseOptions<T>,
|
||||
): T;
|
||||
@@ -47,6 +47,9 @@
|
||||
* onUnhandledArg?: (arg: string, args: T) => "handled" | void,
|
||||
* }} ParseOptions
|
||||
*/
|
||||
/**
|
||||
* @typedef {{ kind: "syntax" } | { kind: "below" } | { kind: "above" } | { kind: "value", value: number }} BoundedUnsignedDecimalResult
|
||||
*/
|
||||
/** @param {string} message */
|
||||
function failFlagParse(message) {
|
||||
throw new Error(message);
|
||||
@@ -170,6 +173,57 @@ function readFlagOptionValue(argv, index, flag) {
|
||||
}
|
||||
return { nextIndex: index + 1, value };
|
||||
}
|
||||
/**
|
||||
* Parse the exact lowercase Boolean language used by strict script arguments.
|
||||
* @param {unknown} value
|
||||
* @param {string} label
|
||||
*/
|
||||
export function parseStrictBooleanArg(value, label) {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`${label} must be true or false.`);
|
||||
}
|
||||
/**
|
||||
* Classify an ASCII unsigned-decimal token against inclusive bounds.
|
||||
* @param {unknown} value
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @returns {BoundedUnsignedDecimalResult}
|
||||
*/
|
||||
export function classifyBoundedUnsignedDecimal(value, min, max) {
|
||||
if (typeof value !== "string" || !/^\d+$/u.test(value)) {
|
||||
return { kind: "syntax" };
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (parsed < min) {
|
||||
return { kind: "below" };
|
||||
}
|
||||
if (parsed > max) {
|
||||
return { kind: "above" };
|
||||
}
|
||||
return { kind: "value", value: parsed };
|
||||
}
|
||||
const PERMISSIVE_BOOLEAN_TRUE_TOKENS = new Set(["1", "on", "true", "yes"]);
|
||||
const PERMISSIVE_BOOLEAN_FALSE_TOKENS = new Set(["0", "false", "no", "off"]);
|
||||
/**
|
||||
* Parse the normalized Boolean token language shared by repository scripts.
|
||||
* @param {unknown} value
|
||||
* @returns {boolean | undefined}
|
||||
*/
|
||||
export function parsePermissiveBooleanToken(value) {
|
||||
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (PERMISSIVE_BOOLEAN_TRUE_TOKENS.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
return PERMISSIVE_BOOLEAN_FALSE_TOKENS.has(normalized) ? false : undefined;
|
||||
}
|
||||
/**
|
||||
* @param {string} raw
|
||||
* @param {string} flag
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { basename, dirname, resolve, win32 as pathWin32 } from "node:path";
|
||||
import { parsePermissiveBooleanToken } from "../arg-utils.mts";
|
||||
import { trimForSummary } from "./shared.ts";
|
||||
|
||||
type CrossOsSuite = "packaged-fresh" | "installer-fresh" | "packaged-upgrade" | "dev-update";
|
||||
@@ -314,11 +315,9 @@ function parseBooleanEnv(name: string, fallback: boolean, env = process.env): bo
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
if (/^(1|true|yes|on)$/iu.test(raw)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(0|false|no|off)$/iu.test(raw)) {
|
||||
return false;
|
||||
const parsed = parsePermissiveBooleanToken(raw);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
throw new Error(`${name} must be a boolean. Got: ${JSON.stringify(raw)}`);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { dirname } from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "../../windows-cmd-helpers.mjs";
|
||||
import { toStringifiedError } from "../error-format.mts";
|
||||
import { resolveWindowsTaskkillPath } from "../windows-taskkill.mjs";
|
||||
import type {
|
||||
Cleanup,
|
||||
@@ -559,8 +560,7 @@ export async function startStaticFileServer(params: {
|
||||
server.close((error) => {
|
||||
void (async () => {
|
||||
const closeLogError = await finishStaticFileServerLog(logStream, logStreamError).catch(
|
||||
(logError: unknown): Error =>
|
||||
logError instanceof Error ? logError : new Error(String(logError)),
|
||||
(logError: unknown): Error => toStringifiedError(logError),
|
||||
);
|
||||
if (error) {
|
||||
rejectPromise(error);
|
||||
|
||||
@@ -51,4 +51,8 @@ export const BANNED_INTERNAL_PLUGIN_SDK_FACADE_MODULES: BannedInternalPluginSdkF
|
||||
modulePath: "src/plugin-sdk/inbound-envelope",
|
||||
canonical: "openclaw/plugin-sdk/channel-inbound",
|
||||
},
|
||||
{
|
||||
modulePath: "src/plugin-sdk/text-runtime",
|
||||
canonical: "the focused typed public Plugin SDK subpath for the imported helper",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import path from "node:path";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { redactSensitiveText } from "../../src/logging/redact.js";
|
||||
import { parsePermissiveBooleanToken } from "./arg-utils.mts";
|
||||
|
||||
export { parseStrictIntegerOption } from "./strict-integer-option.ts";
|
||||
|
||||
@@ -50,15 +51,13 @@ export function parseBooleanEnv(params: {
|
||||
name: string;
|
||||
raw: string | undefined;
|
||||
}): boolean {
|
||||
const raw = params.raw?.trim().toLowerCase();
|
||||
const raw = params.raw?.trim();
|
||||
if (!raw) {
|
||||
return params.fallback;
|
||||
}
|
||||
if (["1", "true", "yes", "on"].includes(raw)) {
|
||||
return true;
|
||||
}
|
||||
if (["0", "false", "no", "off"].includes(raw)) {
|
||||
return false;
|
||||
const parsed = parsePermissiveBooleanToken(raw);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
throw new Error(
|
||||
`${params.name} must be one of 1,0,true,false,yes,no,on,off; got ${JSON.stringify(params.raw)}`,
|
||||
|
||||
@@ -12,6 +12,11 @@ export function coerceErrorMessage(value: unknown): string {
|
||||
return value instanceof Error ? value.message : String(value);
|
||||
}
|
||||
|
||||
/** Preserve Error values and stringify every other value without workspace dependencies. */
|
||||
export function toStringifiedError(value: unknown): Error {
|
||||
return value instanceof Error ? value : new Error(String(value));
|
||||
}
|
||||
|
||||
/** Preserve structured non-Error failures without requiring built workspace packages. */
|
||||
export function toErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
if (value instanceof Error) {
|
||||
|
||||
@@ -11,9 +11,9 @@ export type LocalVitestScheduling = {
|
||||
};
|
||||
|
||||
import os from "node:os";
|
||||
import { parsePermissiveBooleanToken } from "./arg-utils.mts";
|
||||
|
||||
const MAX_LOCAL_FULL_SUITE_PARALLELISM = 10;
|
||||
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
|
||||
|
||||
@@ -37,13 +37,12 @@ function isSystemThrottleDisabled(env: Record<string, string | undefined>) {
|
||||
return normalized === "1" || normalized === "true";
|
||||
}
|
||||
|
||||
function isTruthyEnvValue(value: string | undefined) {
|
||||
return TRUTHY_ENV_VALUES.has(value?.trim().toLowerCase() ?? "");
|
||||
}
|
||||
|
||||
/** @internal Shared repository-script contract. */
|
||||
export function isCiLikeEnv(env: Record<string, string | undefined> = process.env) {
|
||||
return isTruthyEnvValue(env.CI) || isTruthyEnvValue(env.GITHUB_ACTIONS);
|
||||
return (
|
||||
parsePermissiveBooleanToken(env.CI) === true ||
|
||||
parsePermissiveBooleanToken(env.GITHUB_ACTIONS) === true
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal Shared repository-script contract. */
|
||||
|
||||
Reference in New Issue
Block a user