openclaw/cli-root-options.mjs
Peter Steinberger 2ce0573b02
fix(gateway): preserve active work during recovered Node restarts (#153435)
* fix(gateway): preserve work during recovered Node restarts

Let Unix Node recovery wrappers respect the existing Gateway stop budget.
Share startup-safe command classification and deadline facts, and resolve
managed restart intent against the live serving owner in its write
transaction with current update authority.

Preserve the existing public PID intent API, record format, and Windows
behavior. Keep cold lifecycle test preparation outside timed hooks.

Inspired by @ly85206559's wrapper-grace approach in #147054; this repair
is independently authored from main. Related: #146956. Windows cooperative
shutdown remains a follow-up.

* refactor(gateway): stabilize restart callback and test setup

Declare receiver-independent restart intent callbacks as arrows and retain typed signal spy handles for lint-safe assertions. Move cold lifecycle imports into test collection so worker preparation cannot consume timed setup hooks.
2026-09-19 22:41:17 -07:00

176 lines
5.2 KiB
JavaScript

/** CLI token that stops root option scanning and leaves following args positional. */
export const FLAG_TERMINATOR = "--";
const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
const ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level", "--container"]);
/** Returns whether a token can be consumed as a root option value. */
export function isValueToken(arg) {
if (!arg || arg === FLAG_TERMINATOR) {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
/** Count root-option tokens conservatively for route matching. */
export function consumeRootOptionToken(args, index) {
const arg = args[index];
if (!arg) {
return 0;
}
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
return 1;
}
if (
arg.startsWith("--profile=") ||
arg.startsWith("--log-level=") ||
arg.startsWith("--container=")
) {
return 1;
}
if (ROOT_VALUE_FLAGS.has(arg)) {
return isValueToken(args[index + 1]) ? 2 : 1;
}
return 0;
}
/** Consume required root values by their Commander role before startup policy is selected. */
export function consumeRootCommandOptionToken(args, index) {
return consumeKnownOptionToken(args, index, ROOT_BOOLEAN_FLAGS, ROOT_VALUE_FLAGS, "command-path");
}
/** Read positional command tokens while accepting root options at any pre-terminator position. */
export function getRootOptionAwareCommandPath(argv, depth) {
const args = argv.slice(2);
const path = [];
let literal = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg) {
break;
}
if (!literal && arg === FLAG_TERMINATOR) {
// A leading terminator still leaves a command to discover; later operands belong to callers.
if (path.length > 0) {
break;
}
literal = true;
continue;
}
const consumed = literal ? 0 : consumeRootCommandOptionToken(args, index);
if (consumed > 0) {
index += consumed - 1;
continue;
}
if (!literal && arg.startsWith("-")) {
continue;
}
path.push(arg);
if (path.length >= depth) {
break;
}
}
return path;
}
function consumeKnownOptionToken(args, index, booleanFlags, valueFlags, mode) {
const arg = args[index];
if (!arg || arg === FLAG_TERMINATOR || !arg.startsWith("-")) {
return 0;
}
const equalsIndex = arg.indexOf("=");
const flag = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex);
if (booleanFlags.has(flag)) {
return equalsIndex === -1 ? 1 : 0;
}
if (!valueFlags.has(flag)) {
return 0;
}
if (equalsIndex !== -1) {
return mode === "command-path" || arg.slice(equalsIndex + 1).trim() ? 1 : 0;
}
// Required Commander values include empty strings, flag-looking tokens, and `--`.
// Discovery must consume them before choosing startup policy; routes still validate values.
if (mode === "command-path") {
return args[index + 1] !== undefined ? 2 : 0;
}
return isValueToken(args[index + 1]) ? 2 : 0;
}
/** Parse command positionals while consuming known root and command options. */
export function getCommandPositionalsWithRootOptions(argv, options) {
return parseCommandArgsWithRootOptions(argv, options, false);
}
/** Preserve the leaf's raw arguments after consuming its root and parent options. */
export function getCommandArgsWithRootOptions(argv, options) {
return parseCommandArgsWithRootOptions(argv, options, true);
}
function parseCommandArgsWithRootOptions(argv, options, returnTail) {
const args = argv.slice(2);
const booleanFlags = new Set(options.booleanFlags ?? []);
const valueFlags = new Set(options.valueFlags ?? []);
const positionals = [];
let commandIndex = 0;
let literal = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === undefined) {
break;
}
if (!literal && arg === FLAG_TERMINATOR) {
if (options.mode !== "command-path") {
break;
}
literal = true;
continue;
}
const rootConsumed = literal
? 0
: options.mode === "command-path"
? consumeRootCommandOptionToken(args, index)
: consumeRootOptionToken(args, index);
if (rootConsumed > 0) {
index += rootConsumed - 1;
continue;
}
if (!literal && arg.startsWith("-")) {
const optionConsumed = consumeKnownOptionToken(
args,
index,
booleanFlags,
valueFlags,
options.mode,
);
if (optionConsumed === 0 || commandIndex === 0) {
return null;
}
index += optionConsumed - 1;
continue;
}
if (commandIndex < options.commandPath.length) {
if (arg !== options.commandPath[commandIndex]) {
return null;
}
commandIndex += 1;
if (returnTail && commandIndex === options.commandPath.length) {
const tail = args.slice(index + 1);
// A downstream parser must not reactivate flags after an earlier literal boundary.
return literal ? [FLAG_TERMINATOR, ...tail] : tail;
}
continue;
}
positionals.push(arg);
if (positionals.length === options.maxPositionals) {
return positionals;
}
}
return commandIndex < options.commandPath.length ? null : positionals;
}