fix(scripts): parse forwarded package script options

This commit is contained in:
Vincent Koc
2026-05-30 17:35:27 +02:00
parent 19f22b5924
commit cbd8049b9f
16 changed files with 196 additions and 82 deletions

View File

@@ -10,6 +10,7 @@ import {
parsePositiveInt,
parsePositiveNumber,
} from "./lib/numeric-options.mjs";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
import { collectGatewayCpuObservations } from "./lib/plugin-gateway-gauntlet.mjs";
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
@@ -23,6 +24,7 @@ const DEFAULT_CPU_CORE_WARN = 0.9;
const DEFAULT_HOT_WALL_WARN_MS = 30_000;
function parseArgs(argv) {
const args = stripLeadingPackageManagerSeparator(argv);
const options = {
outputDir: path.join(
process.cwd(),
@@ -39,10 +41,10 @@ function parseArgs(argv) {
cpuCoreWarn: DEFAULT_CPU_CORE_WARN,
hotWallWarnMs: DEFAULT_HOT_WALL_WARN_MS,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
const readValue = () => {
const value = argv[index + 1];
const value = args[index + 1];
if (!value) {
throw new Error(`Missing value for ${arg}`);
}

View File

@@ -11,6 +11,7 @@ import {
parsePositiveInt,
parsePositiveNumber,
} from "./lib/numeric-options.mjs";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
import {
buildGauntletPrebuildEnv,
collectGatewayCpuObservations,
@@ -34,6 +35,7 @@ const COMMAND_OUTPUT_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu");
export function parseArgs(argv) {
const args = stripLeadingPackageManagerSeparator(argv);
const options = {
repoRoot: process.cwd(),
outputDir: path.join(
@@ -68,10 +70,10 @@ export function parseArgs(argv) {
};
const envIds = normalizeCsv(process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS);
options.pluginIds.push(...envIds);
parseArgv: for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
parseArgv: for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
const readValue = () => {
const value = argv[index + 1];
const value = args[index + 1];
if (!value) {
throw new Error(`Missing value for ${arg}`);
}

View File

@@ -152,61 +152,62 @@ Options:
}
export function parseArgs(argv: string[]): LinuxOptions {
const args = stripLeadingPackageManagerSeparator(argv);
const options = defaultOptions();
parseArgv: for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
parseArgv: for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--":
break parseArgv;
case "--vm":
options.vmName = ensureValue(argv, i, arg);
options.vmName = ensureValue(args, i, arg);
options.vmNameExplicit = true;
i++;
break;
case "--snapshot-hint":
options.snapshotHint = ensureValue(argv, i, arg);
options.snapshotHint = ensureValue(args, i, arg);
i++;
break;
case "--mode":
options.mode = parseMode(ensureValue(argv, i, arg));
options.mode = parseMode(ensureValue(args, i, arg));
i++;
break;
case "--provider":
options.provider = parseProvider(ensureValue(argv, i, arg));
options.provider = parseProvider(ensureValue(args, i, arg));
i++;
break;
case "--model":
options.modelId = ensureValue(argv, i, arg);
options.modelId = ensureValue(args, i, arg);
i++;
break;
case "--api-key-env":
case "--openai-api-key-env":
options.apiKeyEnv = ensureValue(argv, i, arg);
options.apiKeyEnv = ensureValue(args, i, arg);
i++;
break;
case "--install-url":
options.installUrl = ensureValue(argv, i, arg);
options.installUrl = ensureValue(args, i, arg);
i++;
break;
case "--host-port":
options.hostPort = Number(ensureValue(argv, i, arg));
options.hostPort = Number(ensureValue(args, i, arg));
options.hostPortExplicit = true;
i++;
break;
case "--host-ip":
options.hostIp = ensureValue(argv, i, arg);
options.hostIp = ensureValue(args, i, arg);
i++;
break;
case "--latest-version":
options.latestVersion = ensureValue(argv, i, arg);
options.latestVersion = ensureValue(args, i, arg);
i++;
break;
case "--install-version":
options.installVersion = ensureValue(argv, i, arg);
options.installVersion = ensureValue(args, i, arg);
i++;
break;
case "--target-package-spec":
options.targetPackageSpec = ensureValue(argv, i, arg);
options.targetPackageSpec = ensureValue(args, i, arg);
i++;
break;
case "--keep-server":
@@ -226,6 +227,10 @@ export function parseArgs(argv: string[]): LinuxOptions {
return options;
}
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
return argv[0] === "--" ? argv.slice(1) : argv;
}
class LinuxSmoke extends SmokeRunController<LinuxOptions> {
private auth: ProviderAuth;
private disableBonjour = parseBoolEnv(process.env.OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR);

View File

@@ -155,60 +155,61 @@ Options:
}
export function parseArgs(argv: string[]): MacosOptions {
const args = stripLeadingPackageManagerSeparator(argv);
const options = defaultOptions();
parseArgv: for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
parseArgv: for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--":
break parseArgv;
case "--vm":
options.vmName = ensureValue(argv, i, arg);
options.vmName = ensureValue(args, i, arg);
i++;
break;
case "--snapshot-hint":
options.snapshotHint = ensureValue(argv, i, arg);
options.snapshotHint = ensureValue(args, i, arg);
i++;
break;
case "--mode":
options.mode = parseMode(ensureValue(argv, i, arg));
options.mode = parseMode(ensureValue(args, i, arg));
i++;
break;
case "--provider":
options.provider = parseProvider(ensureValue(argv, i, arg));
options.provider = parseProvider(ensureValue(args, i, arg));
i++;
break;
case "--model":
options.modelId = ensureValue(argv, i, arg);
options.modelId = ensureValue(args, i, arg);
i++;
break;
case "--api-key-env":
case "--openai-api-key-env":
options.apiKeyEnv = ensureValue(argv, i, arg);
options.apiKeyEnv = ensureValue(args, i, arg);
i++;
break;
case "--install-url":
options.installUrl = ensureValue(argv, i, arg);
options.installUrl = ensureValue(args, i, arg);
i++;
break;
case "--host-port":
options.hostPort = parsePositiveInt(ensureValue(argv, i, arg), arg);
options.hostPort = parsePositiveInt(ensureValue(args, i, arg), arg);
options.hostPortExplicit = true;
i++;
break;
case "--host-ip":
options.hostIp = ensureValue(argv, i, arg);
options.hostIp = ensureValue(args, i, arg);
i++;
break;
case "--latest-version":
options.latestVersion = ensureValue(argv, i, arg);
options.latestVersion = ensureValue(args, i, arg);
i++;
break;
case "--install-version":
options.installVersion = ensureValue(argv, i, arg);
options.installVersion = ensureValue(args, i, arg);
i++;
break;
case "--target-package-spec":
options.targetPackageSpec = ensureValue(argv, i, arg);
options.targetPackageSpec = ensureValue(args, i, arg);
i++;
break;
case "--skip-latest-ref-check":
@@ -218,15 +219,15 @@ export function parseArgs(argv: string[]): MacosOptions {
options.keepServer = true;
break;
case "--discord-token-env":
options.discordTokenEnv = ensureValue(argv, i, arg);
options.discordTokenEnv = ensureValue(args, i, arg);
i++;
break;
case "--discord-guild-id":
options.discordGuildId = ensureValue(argv, i, arg);
options.discordGuildId = ensureValue(args, i, arg);
i++;
break;
case "--discord-channel-id":
options.discordChannelId = ensureValue(argv, i, arg);
options.discordChannelId = ensureValue(args, i, arg);
i++;
break;
case "--json":
@@ -243,6 +244,10 @@ export function parseArgs(argv: string[]): MacosOptions {
return options;
}
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
return argv[0] === "--" ? argv.slice(1) : argv;
}
class MacosSmoke {
private agentTimeoutSeconds: number;
private auth: ProviderAuth;

View File

@@ -128,6 +128,7 @@ Options:
}
export function parseArgs(argv: string[]): NpmUpdateOptions {
const args = stripLeadingPackageManagerSeparator(argv);
const options: NpmUpdateOptions = {
apiKeyEnv: undefined,
betaValidation: undefined,
@@ -139,25 +140,25 @@ export function parseArgs(argv: string[]): NpmUpdateOptions {
provider: "openai",
updateTarget: "",
};
parseArgv: for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
parseArgv: for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--":
break parseArgv;
case "--package-spec":
options.packageSpec = ensureValue(argv, i, arg);
options.packageSpec = ensureValue(args, i, arg);
i++;
break;
case "--update-target":
options.updateTarget = ensureValue(argv, i, arg);
options.updateTarget = ensureValue(args, i, arg);
i++;
break;
case "--fresh-target":
options.freshTargetSpec = ensureValue(argv, i, arg);
options.freshTargetSpec = ensureValue(args, i, arg);
i++;
break;
case "--beta-validation": {
const next = argv[i + 1];
const next = args[i + 1];
if (next && !next.startsWith("-")) {
options.betaValidation = next;
i++;
@@ -168,24 +169,24 @@ export function parseArgs(argv: string[]): NpmUpdateOptions {
}
case "--platform":
case "--only":
options.platforms = parsePlatformList(ensureValue(argv, i, arg));
options.platforms = parsePlatformList(ensureValue(args, i, arg));
i++;
break;
case "--provider":
options.provider = parseProvider(ensureValue(argv, i, arg));
options.provider = parseProvider(ensureValue(args, i, arg));
i++;
break;
case "--model":
options.modelId = ensureValue(argv, i, arg);
options.modelId = ensureValue(args, i, arg);
i++;
break;
case "--host-ip":
options.hostIp = ensureValue(argv, i, arg);
options.hostIp = ensureValue(args, i, arg);
i++;
break;
case "--api-key-env":
case "--openai-api-key-env":
options.apiKeyEnv = ensureValue(argv, i, arg);
options.apiKeyEnv = ensureValue(args, i, arg);
i++;
break;
case "--json":
@@ -202,6 +203,10 @@ export function parseArgs(argv: string[]): NpmUpdateOptions {
return options;
}
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
return argv[0] === "--" ? argv.slice(1) : argv;
}
function platformRecord<T>(value: T): Record<Platform, T> {
return { linux: value, macos: value, windows: value };
}

View File

@@ -137,6 +137,7 @@ Options:
}
export function parseArgs(argv: string[]): WindowsOptions {
const args = stripLeadingPackageManagerSeparator(argv);
const options = defaultOptions();
const valueHandlers: Record<string, (value: string) => void> = {
"--api-key-env": (value) => {
@@ -194,14 +195,14 @@ export function parseArgs(argv: string[]): WindowsOptions {
options.upgradeFromPackedMain = true;
},
};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--") {
break;
}
const valueHandler = valueHandlers[arg];
if (valueHandler) {
valueHandler(ensureValue(argv, i, arg));
valueHandler(ensureValue(args, i, arg));
i++;
continue;
}
@@ -219,6 +220,10 @@ export function parseArgs(argv: string[]): WindowsOptions {
return options;
}
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
return argv[0] === "--" ? argv.slice(1) : argv;
}
class WindowsSmoke extends SmokeRunController<WindowsOptions> {
private auth: ProviderAuth;
private artifact: PackageArtifact | null = null;

View File

@@ -20,6 +20,10 @@ export function readFlagValue(args, name) {
return undefined;
}
export function stripLeadingPackageManagerSeparator(argv) {
return argv[0] === "--" ? argv.slice(1) : argv;
}
function isMissingStringFlagValue(value, options = {}) {
if (!value) {
return true;

View File

@@ -6,6 +6,7 @@ import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { ensureExtensionMemoryBuild } from "./ensure-extension-memory-build.mjs";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
import { formatErrorMessage } from "./lib/error-format.mjs";
const DEFAULT_CONCURRENCY = 6;
@@ -53,6 +54,7 @@ function parsePositiveInt(raw, flagName) {
}
export function parseArgs(argv) {
const args = stripLeadingPackageManagerSeparator(argv);
const options = {
extensions: [],
concurrency: DEFAULT_CONCURRENCY,
@@ -63,14 +65,14 @@ export function parseArgs(argv) {
skipCombined: false,
};
parseArgv: for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
parseArgv: for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
switch (arg) {
case "--":
break parseArgv;
case "--extension":
case "-e": {
const next = argv[index + 1];
const next = args[index + 1];
if (!next) {
throw new Error(`${arg} requires a value`);
}
@@ -79,23 +81,23 @@ export function parseArgs(argv) {
break;
}
case "--concurrency":
options.concurrency = parsePositiveInt(argv[index + 1], arg);
options.concurrency = parsePositiveInt(args[index + 1], arg);
index += 1;
break;
case "--timeout-ms":
options.timeoutMs = parsePositiveInt(argv[index + 1], arg);
options.timeoutMs = parsePositiveInt(args[index + 1], arg);
index += 1;
break;
case "--combined-timeout-ms":
options.combinedTimeoutMs = parsePositiveInt(argv[index + 1], arg);
options.combinedTimeoutMs = parsePositiveInt(args[index + 1], arg);
index += 1;
break;
case "--top":
options.top = parsePositiveInt(argv[index + 1], arg);
options.top = parsePositiveInt(args[index + 1], arg);
index += 1;
break;
case "--json": {
const next = argv[index + 1];
const next = args[index + 1];
if (!next) {
throw new Error(`${arg} requires a value`);
}

View File

@@ -50,6 +50,7 @@ Options:
}
export function parseArgs(argv: string[]): Options {
const args = stripLeadingPackageManagerSeparator(argv);
const options: Options = {
beta: "beta",
model: "openai/gpt-5.4",
@@ -59,25 +60,25 @@ export function parseArgs(argv: string[]): Options {
skipParallels: false,
skipTelegram: false,
};
parseArgv: for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
parseArgv: for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--":
break parseArgv;
case "--beta":
options.beta = requireValue(argv, ++i, arg);
options.beta = requireValue(args, ++i, arg);
break;
case "--model":
options.model = requireValue(argv, ++i, arg);
options.model = requireValue(args, ++i, arg);
break;
case "--provider-mode":
options.providerMode = requireValue(argv, ++i, arg);
options.providerMode = requireValue(args, ++i, arg);
break;
case "--ref":
options.ref = requireValue(argv, ++i, arg);
options.ref = requireValue(args, ++i, arg);
break;
case "--repo":
options.repo = requireValue(argv, ++i, arg);
options.repo = requireValue(args, ++i, arg);
break;
case "--skip-parallels":
options.skipParallels = true;
@@ -99,6 +100,10 @@ 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("-")) {

View File

@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { basename, join } from "node:path";
import { fileURLToPath } from "node:url";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
const DEFAULT_REPO = "openclaw/openclaw";
const DEFAULT_PROVIDER = "openai";
@@ -49,6 +50,7 @@ function requireValue(argv, index, flag) {
}
export function parseArgs(argv) {
const args = stripLeadingPackageManagerSeparator(argv);
const options = {
repo: DEFAULT_REPO,
provider: DEFAULT_PROVIDER,
@@ -68,25 +70,25 @@ export function parseArgs(argv) {
npmPreflightRunId: "",
outputDir: "",
};
parseArgv: for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
parseArgv: for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
switch (arg) {
case "--":
break parseArgv;
case "--tag":
options.tag = requireValue(argv, ++index, arg);
options.tag = requireValue(args, ++index, arg);
break;
case "--workflow-ref":
options.workflowRef = requireValue(argv, ++index, arg);
options.workflowRef = requireValue(args, ++index, arg);
break;
case "--repo":
options.repo = requireValue(argv, ++index, arg);
options.repo = requireValue(args, ++index, arg);
break;
case "--full-release-run":
options.fullReleaseRunId = requireValue(argv, ++index, arg);
options.fullReleaseRunId = requireValue(args, ++index, arg);
break;
case "--npm-preflight-run":
options.npmPreflightRunId = requireValue(argv, ++index, arg);
options.npmPreflightRunId = requireValue(args, ++index, arg);
break;
case "--skip-dispatch":
options.skipDispatch = true;
@@ -101,28 +103,28 @@ export function parseArgs(argv) {
options.skipTelegram = true;
break;
case "--telegram-provider-mode":
options.telegramProviderMode = requireValue(argv, ++index, arg);
options.telegramProviderMode = requireValue(args, ++index, arg);
break;
case "--provider":
options.provider = requireValue(argv, ++index, arg);
options.provider = requireValue(args, ++index, arg);
break;
case "--mode":
options.mode = requireValue(argv, ++index, arg);
options.mode = requireValue(args, ++index, arg);
break;
case "--release-profile":
options.releaseProfile = requireValue(argv, ++index, arg);
options.releaseProfile = requireValue(args, ++index, arg);
break;
case "--npm-dist-tag":
options.npmDistTag = requireValue(argv, ++index, arg);
options.npmDistTag = requireValue(args, ++index, arg);
break;
case "--plugin-publish-scope":
options.pluginPublishScope = requireValue(argv, ++index, arg);
options.pluginPublishScope = requireValue(args, ++index, arg);
break;
case "--plugins":
options.plugins = requireValue(argv, ++index, arg);
options.plugins = requireValue(args, ++index, arg);
break;
case "--output-dir":
options.outputDir = requireValue(argv, ++index, arg);
options.outputDir = requireValue(args, ++index, arg);
break;
case "-h":
case "--help":

View File

@@ -26,6 +26,26 @@ describe("gateway CPU scenario guard", () => {
).toThrow("--skip-startup and --skip-qa cannot be used together");
});
it("accepts package-manager argument separators before script options", () => {
expect(
testing.parseArgs([
"--",
"--output-dir",
makeTempRoot(),
"--startup-case",
"default",
"--qa-scenario",
"channel-chat-baseline",
"--runs",
"2",
]),
).toMatchObject({
qaScenarios: ["channel-chat-baseline"],
runs: 2,
startupCases: ["default"],
});
});
it("rejects non-decimal numeric options", () => {
expect(() =>
testing.parseArgs(["--output-dir", makeTempRoot(), "--runs", "1e3"]),

View File

@@ -131,13 +131,18 @@ describe("Parallels smoke model selection", () => {
}
});
it("stops parsing smoke options after the argument terminator", () => {
it("accepts leading package-manager separators and still honors later terminators", () => {
expect(parseLinuxSmokeArgs(["--", "--mode", "upgrade"]).mode).toBe("upgrade");
expect(parseLinuxSmokeArgs(["--mode", "fresh", "--", "--mode", "upgrade"]).mode).toBe(
"fresh",
);
expect(parseMacosSmokeArgs(["--", "--mode", "upgrade"]).mode).toBe("upgrade");
expect(parseMacosSmokeArgs(["--mode", "fresh", "--", "--mode", "upgrade"]).mode).toBe(
"fresh",
);
expect(
parseNpmUpdateSmokeArgs(["--", "--package-spec", "openclaw@2026.5.1"]).packageSpec,
).toBe("openclaw@2026.5.1");
expect(
parseNpmUpdateSmokeArgs([
"--package-spec",
@@ -148,6 +153,9 @@ describe("Parallels smoke model selection", () => {
]).packageSpec,
).toBe("openclaw@2026.5.1");
expect(parseWindowsSmokeArgs(["--", "--upgrade-from-packed-main"]).upgradeFromPackedMain).toBe(
true,
);
expect(parseWindowsSmokeArgs(["--mode", "fresh", "--", "--upgrade-from-packed-main"]).upgradeFromPackedMain).toBe(
false,
);
});

View File

@@ -47,6 +47,24 @@ describe("plugin gateway gauntlet helpers", () => {
});
});
it("accepts package-manager argument separators before script options", () => {
expect(
parseArgs([
"--",
"--plugin",
"telegram",
"--limit",
"3",
"--qa-scenario",
"channel-chat-baseline",
]),
).toMatchObject({
limit: 3,
pluginIds: ["telegram"],
qaScenarios: ["channel-chat-baseline"],
});
});
it("discovers bundled plugin manifests into lifecycle matrix rows", async () => {
await writeManifest(
"alpha",

View File

@@ -30,6 +30,13 @@ describe("scripts/profile-extension-memory", () => {
});
});
it("accepts package-manager argument separators before script options", () => {
expect(parseArgs(["--", "--extension", "discord", "--skip-combined"])).toMatchObject({
extensions: ["discord"],
skipCombined: true,
});
});
it("rejects loose numeric flags before scanning built plugin artifacts", () => {
const cases = [
["--concurrency", "2abc"],

View File

@@ -25,6 +25,13 @@ describe("release-beta-smoke", () => {
});
});
it("accepts package-manager argument separators before script options", () => {
expect(parseArgs(["--", "--beta", "beta-a", "--skip-parallels"])).toMatchObject({
beta: "beta-a",
skipParallels: true,
});
});
it("parses workflow run urls when gh includes them in dispatch output", () => {
expect(
parseWorkflowRunIdFromOutput(

View File

@@ -30,6 +30,23 @@ describe("release candidate checklist", () => {
expect(options.pluginPublishScope).toBe("all-publishable");
});
it("accepts package-manager argument separators before script options", () => {
const options = parseArgs([
"--",
"--tag",
"v2026.5.14-beta.3",
"--full-release-run",
"111",
"--npm-preflight-run",
"222",
"--skip-dispatch",
"--skip-parallels",
]);
expect(options.tag).toBe("v2026.5.14-beta.3");
expect(options.skipParallels).toBe(true);
});
it("builds the gated release publish command from green evidence inputs", () => {
const options = {
...parseArgs([