mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 03:21:38 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -7,23 +7,35 @@ export function hasHelpOrVersion(argv: string[]): boolean {
|
||||
}
|
||||
|
||||
function isValueToken(arg: string | undefined): boolean {
|
||||
if (!arg) return false;
|
||||
if (arg === FLAG_TERMINATOR) return false;
|
||||
if (!arg.startsWith("-")) return true;
|
||||
if (!arg) {
|
||||
return false;
|
||||
}
|
||||
if (arg === FLAG_TERMINATOR) {
|
||||
return false;
|
||||
}
|
||||
if (!arg.startsWith("-")) {
|
||||
return true;
|
||||
}
|
||||
return /^-\d+(?:\.\d+)?$/.test(arg);
|
||||
}
|
||||
|
||||
function parsePositiveInt(value: string): number | undefined {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) return undefined;
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function hasFlag(argv: string[], name: string): boolean {
|
||||
const args = argv.slice(2);
|
||||
for (const arg of args) {
|
||||
if (arg === FLAG_TERMINATOR) break;
|
||||
if (arg === name) return true;
|
||||
if (arg === FLAG_TERMINATOR) {
|
||||
break;
|
||||
}
|
||||
if (arg === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -32,7 +44,9 @@ export function getFlagValue(argv: string[], name: string): string | null | unde
|
||||
const args = argv.slice(2);
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (arg === FLAG_TERMINATOR) break;
|
||||
if (arg === FLAG_TERMINATOR) {
|
||||
break;
|
||||
}
|
||||
if (arg === name) {
|
||||
const next = args[i + 1];
|
||||
return isValueToken(next) ? next : null;
|
||||
@@ -46,14 +60,20 @@ export function getFlagValue(argv: string[], name: string): string | null | unde
|
||||
}
|
||||
|
||||
export function getVerboseFlag(argv: string[], options?: { includeDebug?: boolean }): boolean {
|
||||
if (hasFlag(argv, "--verbose")) return true;
|
||||
if (options?.includeDebug && hasFlag(argv, "--debug")) return true;
|
||||
if (hasFlag(argv, "--verbose")) {
|
||||
return true;
|
||||
}
|
||||
if (options?.includeDebug && hasFlag(argv, "--debug")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPositiveIntFlagValue(argv: string[], name: string): number | null | undefined {
|
||||
const raw = getFlagValue(argv, name);
|
||||
if (raw === null || raw === undefined) return raw;
|
||||
if (raw === null || raw === undefined) {
|
||||
return raw;
|
||||
}
|
||||
return parsePositiveInt(raw);
|
||||
}
|
||||
|
||||
@@ -62,11 +82,19 @@ export function getCommandPath(argv: string[], depth = 2): string[] {
|
||||
const path: string[] = [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg) continue;
|
||||
if (arg === "--") break;
|
||||
if (arg.startsWith("-")) continue;
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
break;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
path.push(arg);
|
||||
if (path.length >= depth) break;
|
||||
if (path.length >= depth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
@@ -97,7 +125,9 @@ export function buildParseArgv(params: {
|
||||
const executable = (normalizedArgv[0]?.split(/[/\\]/).pop() ?? "").toLowerCase();
|
||||
const looksLikeNode =
|
||||
normalizedArgv.length >= 2 && (isNodeExecutable(executable) || isBunExecutable(executable));
|
||||
if (looksLikeNode) return normalizedArgv;
|
||||
if (looksLikeNode) {
|
||||
return normalizedArgv;
|
||||
}
|
||||
return ["node", programName || "openclaw", ...normalizedArgv];
|
||||
}
|
||||
|
||||
@@ -118,11 +148,19 @@ function isBunExecutable(executable: string): boolean {
|
||||
}
|
||||
|
||||
export function shouldMigrateStateFromPath(path: string[]): boolean {
|
||||
if (path.length === 0) return true;
|
||||
if (path.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const [primary, secondary] = path;
|
||||
if (primary === "health" || primary === "status" || primary === "sessions") return false;
|
||||
if (primary === "memory" && secondary === "status") return false;
|
||||
if (primary === "agent") return false;
|
||||
if (primary === "health" || primary === "status" || primary === "sessions") {
|
||||
return false;
|
||||
}
|
||||
if (primary === "memory" && secondary === "status") {
|
||||
return false;
|
||||
}
|
||||
if (primary === "agent") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ const graphemeSegmenter =
|
||||
: null;
|
||||
|
||||
function splitGraphemes(value: string): string[] {
|
||||
if (!graphemeSegmenter) return Array.from(value);
|
||||
if (!graphemeSegmenter) {
|
||||
return Array.from(value);
|
||||
}
|
||||
try {
|
||||
return Array.from(graphemeSegmenter.segment(value), (seg) => seg.segment);
|
||||
} catch {
|
||||
@@ -74,12 +76,20 @@ const LOBSTER_ASCII = [
|
||||
|
||||
export function formatCliBannerArt(options: BannerOptions = {}): string {
|
||||
const rich = options.richTty ?? isRich();
|
||||
if (!rich) return LOBSTER_ASCII.join("\n");
|
||||
if (!rich) {
|
||||
return LOBSTER_ASCII.join("\n");
|
||||
}
|
||||
|
||||
const colorChar = (ch: string) => {
|
||||
if (ch === "█") return theme.accentBright(ch);
|
||||
if (ch === "░") return theme.accentDim(ch);
|
||||
if (ch === "▀") return theme.accent(ch);
|
||||
if (ch === "█") {
|
||||
return theme.accentBright(ch);
|
||||
}
|
||||
if (ch === "░") {
|
||||
return theme.accentDim(ch);
|
||||
}
|
||||
if (ch === "▀") {
|
||||
return theme.accent(ch);
|
||||
}
|
||||
return theme.muted(ch);
|
||||
};
|
||||
|
||||
@@ -99,11 +109,19 @@ export function formatCliBannerArt(options: BannerOptions = {}): string {
|
||||
}
|
||||
|
||||
export function emitCliBanner(version: string, options: BannerOptions = {}) {
|
||||
if (bannerEmitted) return;
|
||||
if (bannerEmitted) {
|
||||
return;
|
||||
}
|
||||
const argv = options.argv ?? process.argv;
|
||||
if (!process.stdout.isTTY) return;
|
||||
if (hasJsonFlag(argv)) return;
|
||||
if (hasVersionFlag(argv)) return;
|
||||
if (!process.stdout.isTTY) {
|
||||
return;
|
||||
}
|
||||
if (hasJsonFlag(argv)) {
|
||||
return;
|
||||
}
|
||||
if (hasVersionFlag(argv)) {
|
||||
return;
|
||||
}
|
||||
const line = formatCliBannerLine(version, options);
|
||||
process.stdout.write(`\n${line}\n\n`);
|
||||
bannerEmitted = true;
|
||||
|
||||
@@ -19,7 +19,9 @@ export function registerBrowserElementCommands(
|
||||
.action(async (ref: string | undefined, opts, cmd) => {
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const refValue = requireRef(ref);
|
||||
if (!refValue) return;
|
||||
if (!refValue) {
|
||||
return;
|
||||
}
|
||||
const modifiers = opts.modifiers
|
||||
? String(opts.modifiers)
|
||||
.split(",")
|
||||
@@ -62,7 +64,9 @@ export function registerBrowserElementCommands(
|
||||
.action(async (ref: string | undefined, text: string, opts, cmd) => {
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const refValue = requireRef(ref);
|
||||
if (!refValue) return;
|
||||
if (!refValue) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await callBrowserAct({
|
||||
parent,
|
||||
@@ -146,7 +150,9 @@ export function registerBrowserElementCommands(
|
||||
.action(async (ref: string | undefined, opts, cmd) => {
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const refValue = requireRef(ref);
|
||||
if (!refValue) return;
|
||||
if (!refValue) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await callBrowserAct({
|
||||
parent,
|
||||
|
||||
@@ -56,9 +56,13 @@ export async function readFields(opts: {
|
||||
fieldsFile?: string;
|
||||
}): Promise<BrowserFormField[]> {
|
||||
const payload = opts.fieldsFile ? await readFile(opts.fieldsFile) : (opts.fields ?? "");
|
||||
if (!payload.trim()) throw new Error("fields are required");
|
||||
if (!payload.trim()) {
|
||||
throw new Error("fields are required");
|
||||
}
|
||||
const parsed = JSON.parse(payload) as unknown;
|
||||
if (!Array.isArray(parsed)) throw new Error("fields must be an array");
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("fields must be an array");
|
||||
}
|
||||
return parsed.map((entry, index) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
throw new Error(`fields[${index}] must be an object`);
|
||||
|
||||
@@ -63,8 +63,11 @@ describe("browser extension install", () => {
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(dir);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
||||
else process.env.OPENCLAW_STATE_DIR = prev;
|
||||
if (prev === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = prev;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,6 +120,8 @@ export function registerBrowserExtensionCommands(
|
||||
const displayPath = shortenHomePath(dir);
|
||||
defaultRuntime.log(displayPath);
|
||||
const copied = await copyToClipboard(dir).catch(() => false);
|
||||
if (copied) defaultRuntime.error(info("Copied to clipboard."));
|
||||
if (copied) {
|
||||
defaultRuntime.error(info("Copied to clipboard."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ type BrowserRequestParams = {
|
||||
};
|
||||
|
||||
function normalizeQuery(query: BrowserRequestParams["query"]): Record<string, string> | undefined {
|
||||
if (!query) return undefined;
|
||||
if (!query) {
|
||||
return undefined;
|
||||
}
|
||||
const out: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === undefined) continue;
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
out[key] = String(value);
|
||||
}
|
||||
return Object.keys(out).length ? out : undefined;
|
||||
|
||||
@@ -116,7 +116,9 @@ export function registerBrowserStateCommands(
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
||||
if (typeof v === "string") headers[k] = v;
|
||||
if (typeof v === "string") {
|
||||
headers[k] = v;
|
||||
}
|
||||
}
|
||||
const result = await callBrowserRequest(
|
||||
parent,
|
||||
|
||||
@@ -8,7 +8,9 @@ function dedupe(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const resolved: string[] = [];
|
||||
for (const value of values) {
|
||||
if (!value || seen.has(value)) continue;
|
||||
if (!value || seen.has(value)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(value);
|
||||
resolved.push(value);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,23 @@ const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(openclaw)\b/;
|
||||
|
||||
export function resolveCliName(argv: string[] = process.argv): string {
|
||||
const argv1 = argv[1];
|
||||
if (!argv1) return DEFAULT_CLI_NAME;
|
||||
if (!argv1) {
|
||||
return DEFAULT_CLI_NAME;
|
||||
}
|
||||
const base = path.basename(argv1).trim();
|
||||
if (KNOWN_CLI_NAMES.has(base)) return base;
|
||||
if (KNOWN_CLI_NAMES.has(base)) {
|
||||
return base;
|
||||
}
|
||||
return DEFAULT_CLI_NAME;
|
||||
}
|
||||
|
||||
export function replaceCliName(command: string, cliName = resolveCliName()): string {
|
||||
if (!command.trim()) return command;
|
||||
if (!CLI_PREFIX_RE.test(command)) return command;
|
||||
if (!command.trim()) {
|
||||
return command;
|
||||
}
|
||||
if (!CLI_PREFIX_RE.test(command)) {
|
||||
return command;
|
||||
}
|
||||
return command.replace(CLI_PREFIX_RE, (_match, runner: string | undefined) => {
|
||||
return `${runner ?? ""}${cliName}`;
|
||||
});
|
||||
|
||||
@@ -56,7 +56,9 @@ export function resolveOptionFromCommand<T>(
|
||||
let current: Command | null | undefined = command;
|
||||
while (current) {
|
||||
const opts = current.opts?.() ?? {};
|
||||
if (opts[key] !== undefined) return opts[key];
|
||||
if (opts[key] !== undefined) {
|
||||
return opts[key];
|
||||
}
|
||||
current = current.parent ?? undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -12,8 +12,12 @@ export function formatCliCommand(
|
||||
const cliName = resolveCliName();
|
||||
const normalizedCommand = replaceCliName(command, cliName);
|
||||
const profile = normalizeProfileName(env.OPENCLAW_PROFILE);
|
||||
if (!profile) return normalizedCommand;
|
||||
if (!CLI_PREFIX_RE.test(normalizedCommand)) return normalizedCommand;
|
||||
if (!profile) {
|
||||
return normalizedCommand;
|
||||
}
|
||||
if (!CLI_PREFIX_RE.test(normalizedCommand)) {
|
||||
return normalizedCommand;
|
||||
}
|
||||
if (PROFILE_FLAG_RE.test(normalizedCommand) || DEV_FLAG_RE.test(normalizedCommand)) {
|
||||
return normalizedCommand;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ export function registerCompletionCli(program: Command) {
|
||||
const entries = getSubCliEntries();
|
||||
for (const entry of entries) {
|
||||
// Skip completion command itself to avoid cycle if we were to add it to the list
|
||||
if (entry.name === "completion") continue;
|
||||
if (entry.name === "completion") {
|
||||
continue;
|
||||
}
|
||||
await registerSubCliByName(program, entry.name);
|
||||
}
|
||||
|
||||
@@ -84,7 +86,9 @@ export async function installCompletion(shell: string, yes: boolean, binName = "
|
||||
|
||||
const content = await fs.readFile(profilePath, "utf-8");
|
||||
if (content.includes(`${binName} completion`)) {
|
||||
if (!yes) console.log(`Completion already installed in ${profilePath}`);
|
||||
if (!yes) {
|
||||
console.log(`Completion already installed in ${profilePath}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,7 +322,9 @@ function generateFishCompletion(program: Command): string {
|
||||
const visit = (cmd: Command, parents: string[]) => {
|
||||
const cmdName = cmd.name();
|
||||
const fullPath = [...parents];
|
||||
if (parents.length > 0) fullPath.push(cmdName); // Only push if not root, or consistent root handling
|
||||
if (parents.length > 0) {
|
||||
fullPath.push(cmdName);
|
||||
} // Only push if not root, or consistent root handling
|
||||
|
||||
// Fish uses 'seen_subcommand_from' to determine context.
|
||||
// For root: complete -c openclaw -n "__fish_use_subcommand" -a "subcmd" -d "desc"
|
||||
@@ -339,8 +345,12 @@ function generateFishCompletion(program: Command): string {
|
||||
?.replace(/^-/, "");
|
||||
const desc = opt.description.replace(/'/g, "'\\''");
|
||||
let line = `complete -c ${rootCmd} -n "__fish_use_subcommand"`;
|
||||
if (short) line += ` -s ${short}`;
|
||||
if (long) line += ` -l ${long}`;
|
||||
if (short) {
|
||||
line += ` -s ${short}`;
|
||||
}
|
||||
if (long) {
|
||||
line += ` -l ${long}`;
|
||||
}
|
||||
line += ` -d '${desc}'\n`;
|
||||
script += line;
|
||||
}
|
||||
@@ -368,8 +378,12 @@ function generateFishCompletion(program: Command): string {
|
||||
?.replace(/^-/, "");
|
||||
const desc = opt.description.replace(/'/g, "'\\''");
|
||||
let line = `complete -c ${rootCmd} -n "__fish_seen_subcommand_from ${cmdName}"`;
|
||||
if (short) line += ` -s ${short}`;
|
||||
if (long) line += ` -l ${long}`;
|
||||
if (short) {
|
||||
line += ` -s ${short}`;
|
||||
}
|
||||
if (long) {
|
||||
line += ` -l ${long}`;
|
||||
}
|
||||
line += ` -d '${desc}'\n`;
|
||||
script += line;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ function isIndexSegment(raw: string): boolean {
|
||||
|
||||
function parsePath(raw: string): PathSegment[] {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return [];
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
const parts: string[] = [];
|
||||
let current = "";
|
||||
let i = 0;
|
||||
@@ -25,23 +27,33 @@ function parsePath(raw: string): PathSegment[] {
|
||||
const ch = trimmed[i];
|
||||
if (ch === "\\") {
|
||||
const next = trimmed[i + 1];
|
||||
if (next) current += next;
|
||||
if (next) {
|
||||
current += next;
|
||||
}
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === ".") {
|
||||
if (current) parts.push(current);
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
current = "";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "[") {
|
||||
if (current) parts.push(current);
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
current = "";
|
||||
const close = trimmed.indexOf("]", i);
|
||||
if (close === -1) throw new Error(`Invalid path (missing "]"): ${raw}`);
|
||||
if (close === -1) {
|
||||
throw new Error(`Invalid path (missing "]"): ${raw}`);
|
||||
}
|
||||
const inside = trimmed.slice(i + 1, close).trim();
|
||||
if (!inside) throw new Error(`Invalid path (empty "[]"): ${raw}`);
|
||||
if (!inside) {
|
||||
throw new Error(`Invalid path (empty "[]"): ${raw}`);
|
||||
}
|
||||
parts.push(inside);
|
||||
i = close + 1;
|
||||
continue;
|
||||
@@ -49,7 +61,9 @@ function parsePath(raw: string): PathSegment[] {
|
||||
current += ch;
|
||||
i += 1;
|
||||
}
|
||||
if (current) parts.push(current);
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
return parts.map((part) => part.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -73,9 +87,13 @@ function parseValue(raw: string, opts: { json?: boolean }): unknown {
|
||||
function getAtPath(root: unknown, path: PathSegment[]): { found: boolean; value?: unknown } {
|
||||
let current: unknown = root;
|
||||
for (const segment of path) {
|
||||
if (!current || typeof current !== "object") return { found: false };
|
||||
if (!current || typeof current !== "object") {
|
||||
return { found: false };
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
if (!isIndexSegment(segment)) return { found: false };
|
||||
if (!isIndexSegment(segment)) {
|
||||
return { found: false };
|
||||
}
|
||||
const index = Number.parseInt(segment, 10);
|
||||
if (!Number.isFinite(index) || index < 0 || index >= current.length) {
|
||||
return { found: false };
|
||||
@@ -84,7 +102,9 @@ function getAtPath(root: unknown, path: PathSegment[]): { found: boolean; value?
|
||||
continue;
|
||||
}
|
||||
const record = current as Record<string, unknown>;
|
||||
if (!(segment in record)) return { found: false };
|
||||
if (!(segment in record)) {
|
||||
return { found: false };
|
||||
}
|
||||
current = record[segment];
|
||||
}
|
||||
return { found: true, value: current };
|
||||
@@ -138,37 +158,55 @@ function unsetAtPath(root: Record<string, unknown>, path: PathSegment[]): boolea
|
||||
let current: unknown = root;
|
||||
for (let i = 0; i < path.length - 1; i += 1) {
|
||||
const segment = path[i];
|
||||
if (!current || typeof current !== "object") return false;
|
||||
if (!current || typeof current !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
if (!isIndexSegment(segment)) return false;
|
||||
if (!isIndexSegment(segment)) {
|
||||
return false;
|
||||
}
|
||||
const index = Number.parseInt(segment, 10);
|
||||
if (!Number.isFinite(index) || index < 0 || index >= current.length) return false;
|
||||
if (!Number.isFinite(index) || index < 0 || index >= current.length) {
|
||||
return false;
|
||||
}
|
||||
current = current[index];
|
||||
continue;
|
||||
}
|
||||
const record = current as Record<string, unknown>;
|
||||
if (!(segment in record)) return false;
|
||||
if (!(segment in record)) {
|
||||
return false;
|
||||
}
|
||||
current = record[segment];
|
||||
}
|
||||
|
||||
const last = path[path.length - 1];
|
||||
if (Array.isArray(current)) {
|
||||
if (!isIndexSegment(last)) return false;
|
||||
if (!isIndexSegment(last)) {
|
||||
return false;
|
||||
}
|
||||
const index = Number.parseInt(last, 10);
|
||||
if (!Number.isFinite(index) || index < 0 || index >= current.length) return false;
|
||||
if (!Number.isFinite(index) || index < 0 || index >= current.length) {
|
||||
return false;
|
||||
}
|
||||
current.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
if (!current || typeof current !== "object") return false;
|
||||
if (!current || typeof current !== "object") {
|
||||
return false;
|
||||
}
|
||||
const record = current as Record<string, unknown>;
|
||||
if (!(last in record)) return false;
|
||||
if (!(last in record)) {
|
||||
return false;
|
||||
}
|
||||
delete record[last];
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadValidConfig() {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.valid) return snapshot;
|
||||
if (snapshot.valid) {
|
||||
return snapshot;
|
||||
}
|
||||
defaultRuntime.error(`Config invalid at ${shortenHomePath(snapshot.path)}.`);
|
||||
for (const issue of snapshot.issues) {
|
||||
defaultRuntime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
|
||||
@@ -264,7 +302,9 @@ export function registerConfigCli(program: Command) {
|
||||
.action(async (path: string, value: string, opts) => {
|
||||
try {
|
||||
const parsedPath = parsePath(path);
|
||||
if (parsedPath.length === 0) throw new Error("Path is empty.");
|
||||
if (parsedPath.length === 0) {
|
||||
throw new Error("Path is empty.");
|
||||
}
|
||||
const parsedValue = parseValue(value, opts);
|
||||
const snapshot = await loadValidConfig();
|
||||
const next = snapshot.config as Record<string, unknown>;
|
||||
@@ -284,7 +324,9 @@ export function registerConfigCli(program: Command) {
|
||||
.action(async (path: string) => {
|
||||
try {
|
||||
const parsedPath = parsePath(path);
|
||||
if (parsedPath.length === 0) throw new Error("Path is empty.");
|
||||
if (parsedPath.length === 0) {
|
||||
throw new Error("Path is empty.");
|
||||
}
|
||||
const snapshot = await loadValidConfig();
|
||||
const next = snapshot.config as Record<string, unknown>;
|
||||
const removed = unsetAtPath(next, parsedPath);
|
||||
|
||||
@@ -2,7 +2,9 @@ import { Command } from "commander";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const callGatewayFromCli = vi.fn(async (method: string, _opts: unknown, params?: unknown) => {
|
||||
if (method === "cron.status") return { enabled: true };
|
||||
if (method === "cron.status") {
|
||||
return { enabled: true };
|
||||
}
|
||||
return { ok: true, params };
|
||||
});
|
||||
|
||||
|
||||
@@ -111,12 +111,16 @@ export function registerCronAddCommand(cron: Command) {
|
||||
}
|
||||
if (at) {
|
||||
const atMs = parseAtMs(at);
|
||||
if (!atMs) throw new Error("Invalid --at; use ISO time or duration like 20m");
|
||||
if (!atMs) {
|
||||
throw new Error("Invalid --at; use ISO time or duration like 20m");
|
||||
}
|
||||
return { kind: "at" as const, atMs };
|
||||
}
|
||||
if (every) {
|
||||
const everyMs = parseDurationMs(every);
|
||||
if (!everyMs) throw new Error("Invalid --every; use e.g. 10m, 1h, 1d");
|
||||
if (!everyMs) {
|
||||
throw new Error("Invalid --every; use e.g. 10m, 1h, 1d");
|
||||
}
|
||||
return { kind: "every" as const, everyMs };
|
||||
}
|
||||
return {
|
||||
@@ -150,7 +154,9 @@ export function registerCronAddCommand(cron: Command) {
|
||||
if (chosen !== 1) {
|
||||
throw new Error("Choose exactly one payload: --system-event or --message");
|
||||
}
|
||||
if (systemEvent) return { kind: "systemEvent" as const, text: systemEvent };
|
||||
if (systemEvent) {
|
||||
return { kind: "systemEvent" as const, text: systemEvent };
|
||||
}
|
||||
const timeoutSeconds = parsePositiveIntOrUndefined(opts.timeoutSeconds);
|
||||
return {
|
||||
kind: "agentTurn" as const,
|
||||
@@ -197,7 +203,9 @@ export function registerCronAddCommand(cron: Command) {
|
||||
|
||||
const nameRaw = typeof opts.name === "string" ? opts.name : "";
|
||||
const name = nameRaw.trim();
|
||||
if (!name) throw new Error("--name is required");
|
||||
if (!name) {
|
||||
throw new Error("--name is required");
|
||||
}
|
||||
|
||||
const description =
|
||||
typeof opts.description === "string" && opts.description.trim()
|
||||
|
||||
@@ -16,7 +16,9 @@ const assignIf = (
|
||||
value: unknown,
|
||||
shouldAssign: boolean,
|
||||
) => {
|
||||
if (shouldAssign) target[key] = value;
|
||||
if (shouldAssign) {
|
||||
target[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
export function registerCronEditCommand(cron: Command) {
|
||||
@@ -74,19 +76,36 @@ export function registerCronEditCommand(cron: Command) {
|
||||
}
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (typeof opts.name === "string") patch.name = opts.name;
|
||||
if (typeof opts.description === "string") patch.description = opts.description;
|
||||
if (opts.enable && opts.disable)
|
||||
if (typeof opts.name === "string") {
|
||||
patch.name = opts.name;
|
||||
}
|
||||
if (typeof opts.description === "string") {
|
||||
patch.description = opts.description;
|
||||
}
|
||||
if (opts.enable && opts.disable) {
|
||||
throw new Error("Choose --enable or --disable, not both");
|
||||
if (opts.enable) patch.enabled = true;
|
||||
if (opts.disable) patch.enabled = false;
|
||||
}
|
||||
if (opts.enable) {
|
||||
patch.enabled = true;
|
||||
}
|
||||
if (opts.disable) {
|
||||
patch.enabled = false;
|
||||
}
|
||||
if (opts.deleteAfterRun && opts.keepAfterRun) {
|
||||
throw new Error("Choose --delete-after-run or --keep-after-run, not both");
|
||||
}
|
||||
if (opts.deleteAfterRun) patch.deleteAfterRun = true;
|
||||
if (opts.keepAfterRun) patch.deleteAfterRun = false;
|
||||
if (typeof opts.session === "string") patch.sessionTarget = opts.session;
|
||||
if (typeof opts.wake === "string") patch.wakeMode = opts.wake;
|
||||
if (opts.deleteAfterRun) {
|
||||
patch.deleteAfterRun = true;
|
||||
}
|
||||
if (opts.keepAfterRun) {
|
||||
patch.deleteAfterRun = false;
|
||||
}
|
||||
if (typeof opts.session === "string") {
|
||||
patch.sessionTarget = opts.session;
|
||||
}
|
||||
if (typeof opts.wake === "string") {
|
||||
patch.wakeMode = opts.wake;
|
||||
}
|
||||
if (opts.agent && opts.clearAgent) {
|
||||
throw new Error("Use --agent or --clear-agent, not both");
|
||||
}
|
||||
@@ -98,14 +117,20 @@ export function registerCronEditCommand(cron: Command) {
|
||||
}
|
||||
|
||||
const scheduleChosen = [opts.at, opts.every, opts.cron].filter(Boolean).length;
|
||||
if (scheduleChosen > 1) throw new Error("Choose at most one schedule change");
|
||||
if (scheduleChosen > 1) {
|
||||
throw new Error("Choose at most one schedule change");
|
||||
}
|
||||
if (opts.at) {
|
||||
const atMs = parseAtMs(String(opts.at));
|
||||
if (!atMs) throw new Error("Invalid --at");
|
||||
if (!atMs) {
|
||||
throw new Error("Invalid --at");
|
||||
}
|
||||
patch.schedule = { kind: "at", atMs };
|
||||
} else if (opts.every) {
|
||||
const everyMs = parseDurationMs(String(opts.every));
|
||||
if (!everyMs) throw new Error("Invalid --every");
|
||||
if (!everyMs) {
|
||||
throw new Error("Invalid --every");
|
||||
}
|
||||
patch.schedule = { kind: "every", everyMs };
|
||||
} else if (opts.cron) {
|
||||
patch.schedule = {
|
||||
|
||||
@@ -15,7 +15,9 @@ export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
||||
enabled?: boolean;
|
||||
storePath?: string;
|
||||
};
|
||||
if (res?.enabled === true) return;
|
||||
if (res?.enabled === true) {
|
||||
return;
|
||||
}
|
||||
const store = typeof res?.storePath === "string" ? res.storePath : "";
|
||||
defaultRuntime.error(
|
||||
[
|
||||
@@ -33,11 +35,17 @@ export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
||||
|
||||
export function parseDurationMs(input: string): number | null {
|
||||
const raw = input.trim();
|
||||
if (!raw) return null;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
|
||||
if (!match) return null;
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const n = Number.parseFloat(match[1] ?? "");
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
return null;
|
||||
}
|
||||
const unit = (match[2] ?? "").toLowerCase();
|
||||
const factor =
|
||||
unit === "ms"
|
||||
@@ -54,11 +62,17 @@ export function parseDurationMs(input: string): number | null {
|
||||
|
||||
export function parseAtMs(input: string): number | null {
|
||||
const raw = input.trim();
|
||||
if (!raw) return null;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const absolute = parseAbsoluteTimeMs(raw);
|
||||
if (absolute) return absolute;
|
||||
if (absolute) {
|
||||
return absolute;
|
||||
}
|
||||
const dur = parseDurationMs(raw);
|
||||
if (dur) return Date.now() + dur;
|
||||
if (dur) {
|
||||
return Date.now() + dur;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -74,48 +88,76 @@ const CRON_AGENT_PAD = 10;
|
||||
const pad = (value: string, width: number) => value.padEnd(width);
|
||||
|
||||
const truncate = (value: string, width: number) => {
|
||||
if (value.length <= width) return value;
|
||||
if (width <= 3) return value.slice(0, width);
|
||||
if (value.length <= width) {
|
||||
return value;
|
||||
}
|
||||
if (width <= 3) {
|
||||
return value.slice(0, width);
|
||||
}
|
||||
return `${value.slice(0, width - 3)}...`;
|
||||
};
|
||||
|
||||
const formatIsoMinute = (ms: number) => {
|
||||
const d = new Date(ms);
|
||||
if (Number.isNaN(d.getTime())) return "-";
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return "-";
|
||||
}
|
||||
const iso = d.toISOString();
|
||||
return `${iso.slice(0, 10)} ${iso.slice(11, 16)}Z`;
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
if (ms < 60_000) return `${Math.max(1, Math.round(ms / 1000))}s`;
|
||||
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
|
||||
if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h`;
|
||||
if (ms < 60_000) {
|
||||
return `${Math.max(1, Math.round(ms / 1000))}s`;
|
||||
}
|
||||
if (ms < 3_600_000) {
|
||||
return `${Math.round(ms / 60_000)}m`;
|
||||
}
|
||||
if (ms < 86_400_000) {
|
||||
return `${Math.round(ms / 3_600_000)}h`;
|
||||
}
|
||||
return `${Math.round(ms / 86_400_000)}d`;
|
||||
};
|
||||
|
||||
const formatSpan = (ms: number) => {
|
||||
if (ms < 60_000) return "<1m";
|
||||
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
|
||||
if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h`;
|
||||
if (ms < 60_000) {
|
||||
return "<1m";
|
||||
}
|
||||
if (ms < 3_600_000) {
|
||||
return `${Math.round(ms / 60_000)}m`;
|
||||
}
|
||||
if (ms < 86_400_000) {
|
||||
return `${Math.round(ms / 3_600_000)}h`;
|
||||
}
|
||||
return `${Math.round(ms / 86_400_000)}d`;
|
||||
};
|
||||
|
||||
const formatRelative = (ms: number | null | undefined, nowMs: number) => {
|
||||
if (!ms) return "-";
|
||||
if (!ms) {
|
||||
return "-";
|
||||
}
|
||||
const delta = ms - nowMs;
|
||||
const label = formatSpan(Math.abs(delta));
|
||||
return delta >= 0 ? `in ${label}` : `${label} ago`;
|
||||
};
|
||||
|
||||
const formatSchedule = (schedule: CronSchedule) => {
|
||||
if (schedule.kind === "at") return `at ${formatIsoMinute(schedule.atMs)}`;
|
||||
if (schedule.kind === "every") return `every ${formatDuration(schedule.everyMs)}`;
|
||||
if (schedule.kind === "at") {
|
||||
return `at ${formatIsoMinute(schedule.atMs)}`;
|
||||
}
|
||||
if (schedule.kind === "every") {
|
||||
return `every ${formatDuration(schedule.everyMs)}`;
|
||||
}
|
||||
return schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
|
||||
};
|
||||
|
||||
const formatStatus = (job: CronJob) => {
|
||||
if (!job.enabled) return "disabled";
|
||||
if (job.state.runningAtMs) return "running";
|
||||
if (!job.enabled) {
|
||||
return "disabled";
|
||||
}
|
||||
if (job.state.runningAtMs) {
|
||||
return "running";
|
||||
}
|
||||
return job.state.lastStatus ?? "idle";
|
||||
};
|
||||
|
||||
@@ -158,10 +200,18 @@ export function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
|
||||
const agentLabel = pad(truncate(job.agentId ?? "default", CRON_AGENT_PAD), CRON_AGENT_PAD);
|
||||
|
||||
const coloredStatus = (() => {
|
||||
if (statusRaw === "ok") return colorize(rich, theme.success, statusLabel);
|
||||
if (statusRaw === "error") return colorize(rich, theme.error, statusLabel);
|
||||
if (statusRaw === "running") return colorize(rich, theme.warn, statusLabel);
|
||||
if (statusRaw === "skipped") return colorize(rich, theme.muted, statusLabel);
|
||||
if (statusRaw === "ok") {
|
||||
return colorize(rich, theme.success, statusLabel);
|
||||
}
|
||||
if (statusRaw === "error") {
|
||||
return colorize(rich, theme.error, statusLabel);
|
||||
}
|
||||
if (statusRaw === "running") {
|
||||
return colorize(rich, theme.warn, statusLabel);
|
||||
}
|
||||
if (statusRaw === "skipped") {
|
||||
return colorize(rich, theme.muted, statusLabel);
|
||||
}
|
||||
return colorize(rich, theme.muted, statusLabel);
|
||||
})();
|
||||
|
||||
|
||||
@@ -96,21 +96,29 @@ describe("daemon-cli coverage", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv.OPENCLAW_STATE_DIR !== undefined)
|
||||
if (originalEnv.OPENCLAW_STATE_DIR !== undefined) {
|
||||
process.env.OPENCLAW_STATE_DIR = originalEnv.OPENCLAW_STATE_DIR;
|
||||
else delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
}
|
||||
|
||||
if (originalEnv.OPENCLAW_CONFIG_PATH !== undefined)
|
||||
if (originalEnv.OPENCLAW_CONFIG_PATH !== undefined) {
|
||||
process.env.OPENCLAW_CONFIG_PATH = originalEnv.OPENCLAW_CONFIG_PATH;
|
||||
else delete process.env.OPENCLAW_CONFIG_PATH;
|
||||
} else {
|
||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
||||
}
|
||||
|
||||
if (originalEnv.OPENCLAW_GATEWAY_PORT !== undefined)
|
||||
if (originalEnv.OPENCLAW_GATEWAY_PORT !== undefined) {
|
||||
process.env.OPENCLAW_GATEWAY_PORT = originalEnv.OPENCLAW_GATEWAY_PORT;
|
||||
else delete process.env.OPENCLAW_GATEWAY_PORT;
|
||||
} else {
|
||||
delete process.env.OPENCLAW_GATEWAY_PORT;
|
||||
}
|
||||
|
||||
if (originalEnv.OPENCLAW_PROFILE !== undefined)
|
||||
if (originalEnv.OPENCLAW_PROFILE !== undefined) {
|
||||
process.env.OPENCLAW_PROFILE = originalEnv.OPENCLAW_PROFILE;
|
||||
else delete process.env.OPENCLAW_PROFILE;
|
||||
} else {
|
||||
delete process.env.OPENCLAW_PROFILE;
|
||||
}
|
||||
});
|
||||
|
||||
it("probes gateway status by default", async () => {
|
||||
|
||||
@@ -30,7 +30,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
hints?: string[];
|
||||
warnings?: string[];
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "install", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
@@ -97,8 +99,11 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
token: opts.token || cfg.gateway?.auth?.token || process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||
runtime: runtimeRaw,
|
||||
warn: (message) => {
|
||||
if (json) warnings.push(message);
|
||||
else defaultRuntime.log(message);
|
||||
if (json) {
|
||||
warnings.push(message);
|
||||
} else {
|
||||
defaultRuntime.log(message);
|
||||
}
|
||||
},
|
||||
config: cfg,
|
||||
});
|
||||
|
||||
@@ -23,12 +23,17 @@ export async function runDaemonUninstall(opts: DaemonLifecycleOptions = {}) {
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "uninstall", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) emit({ ok: false, error: message });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
@@ -91,12 +96,17 @@ export async function runDaemonStart(opts: DaemonLifecycleOptions = {}) {
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "start", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (json) emit({ ok: false, error: message, hints });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message, hints });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
@@ -167,12 +177,17 @@ export async function runDaemonStop(opts: DaemonLifecycleOptions = {}) {
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "stop", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) emit({ ok: false, error: message });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
@@ -237,12 +252,17 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "restart", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (json) emit({ ok: false, error: message, hints });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message, hints });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,31 +8,43 @@ import { getResolvedLoggerSettings } from "../../logging.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
|
||||
export function parsePort(raw: unknown): number | null {
|
||||
if (raw === undefined || raw === null) return null;
|
||||
if (raw === undefined || raw === null) {
|
||||
return null;
|
||||
}
|
||||
const value =
|
||||
typeof raw === "string"
|
||||
? raw
|
||||
: typeof raw === "number" || typeof raw === "bigint"
|
||||
? raw.toString()
|
||||
: null;
|
||||
if (value === null) return null;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parsePortFromArgs(programArguments: string[] | undefined): number | null {
|
||||
if (!programArguments?.length) return null;
|
||||
if (!programArguments?.length) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < programArguments.length; i += 1) {
|
||||
const arg = programArguments[i];
|
||||
if (arg === "--port") {
|
||||
const next = programArguments[i + 1];
|
||||
const parsed = parsePort(next);
|
||||
if (parsed) return parsed;
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (arg?.startsWith("--port=")) {
|
||||
const parsed = parsePort(arg.split("=", 2)[1]);
|
||||
if (parsed) return parsed;
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -46,7 +58,9 @@ export function pickProbeHostForBind(
|
||||
if (bindMode === "custom" && customBindHost?.trim()) {
|
||||
return customBindHost.trim();
|
||||
}
|
||||
if (bindMode === "tailnet") return tailnetIPv4 ?? "127.0.0.1";
|
||||
if (bindMode === "tailnet") {
|
||||
return tailnetIPv4 ?? "127.0.0.1";
|
||||
}
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
@@ -59,11 +73,15 @@ const SAFE_DAEMON_ENV_KEYS = [
|
||||
];
|
||||
|
||||
export function filterDaemonEnv(env: Record<string, string> | undefined): Record<string, string> {
|
||||
if (!env) return {};
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
const filtered: Record<string, string> = {};
|
||||
for (const key of SAFE_DAEMON_ENV_KEYS) {
|
||||
const value = env[key];
|
||||
if (!value?.trim()) continue;
|
||||
if (!value?.trim()) {
|
||||
continue;
|
||||
}
|
||||
filtered[key] = value.trim();
|
||||
}
|
||||
return filtered;
|
||||
@@ -76,7 +94,9 @@ export function safeDaemonEnv(env: Record<string, string> | undefined): string[]
|
||||
|
||||
export function normalizeListenerAddress(raw: string): string {
|
||||
let value = raw.trim();
|
||||
if (!value) return value;
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
value = value.replace(/^TCP\s+/i, "");
|
||||
value = value.replace(/\s+\(LISTEN\)\s*$/i, "");
|
||||
return value.trim();
|
||||
@@ -97,21 +117,35 @@ export function formatRuntimeStatus(
|
||||
}
|
||||
| undefined,
|
||||
) {
|
||||
if (!runtime) return null;
|
||||
if (!runtime) {
|
||||
return null;
|
||||
}
|
||||
const status = runtime.status ?? "unknown";
|
||||
const details: string[] = [];
|
||||
if (runtime.pid) details.push(`pid ${runtime.pid}`);
|
||||
if (runtime.pid) {
|
||||
details.push(`pid ${runtime.pid}`);
|
||||
}
|
||||
if (runtime.state && runtime.state.toLowerCase() !== status) {
|
||||
details.push(`state ${runtime.state}`);
|
||||
}
|
||||
if (runtime.subState) details.push(`sub ${runtime.subState}`);
|
||||
if (runtime.subState) {
|
||||
details.push(`sub ${runtime.subState}`);
|
||||
}
|
||||
if (runtime.lastExitStatus !== undefined) {
|
||||
details.push(`last exit ${runtime.lastExitStatus}`);
|
||||
}
|
||||
if (runtime.lastExitReason) details.push(`reason ${runtime.lastExitReason}`);
|
||||
if (runtime.lastRunResult) details.push(`last run ${runtime.lastRunResult}`);
|
||||
if (runtime.lastRunTime) details.push(`last run time ${runtime.lastRunTime}`);
|
||||
if (runtime.detail) details.push(runtime.detail);
|
||||
if (runtime.lastExitReason) {
|
||||
details.push(`reason ${runtime.lastExitReason}`);
|
||||
}
|
||||
if (runtime.lastRunResult) {
|
||||
details.push(`last run ${runtime.lastRunResult}`);
|
||||
}
|
||||
if (runtime.lastRunTime) {
|
||||
details.push(`last run time ${runtime.lastRunTime}`);
|
||||
}
|
||||
if (runtime.detail) {
|
||||
details.push(runtime.detail);
|
||||
}
|
||||
return details.length > 0 ? `${status} (${details.join(", ")})` : status;
|
||||
}
|
||||
|
||||
@@ -119,7 +153,9 @@ export function renderRuntimeHints(
|
||||
runtime: { missingUnit?: boolean; status?: string } | undefined,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
if (!runtime) return [];
|
||||
if (!runtime) {
|
||||
return [];
|
||||
}
|
||||
const hints: string[] = [];
|
||||
const fileLog = (() => {
|
||||
try {
|
||||
@@ -130,11 +166,15 @@ export function renderRuntimeHints(
|
||||
})();
|
||||
if (runtime.missingUnit) {
|
||||
hints.push(`Service not installed. Run: ${formatCliCommand("openclaw gateway install", env)}`);
|
||||
if (fileLog) hints.push(`File logs: ${fileLog}`);
|
||||
if (fileLog) {
|
||||
hints.push(`File logs: ${fileLog}`);
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
if (runtime.status === "stopped") {
|
||||
if (fileLog) hints.push(`File logs: ${fileLog}`);
|
||||
if (fileLog) {
|
||||
hints.push(`File logs: ${fileLog}`);
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
const logs = resolveGatewayLogPaths(env);
|
||||
hints.push(`Launchd stdout (if installed): ${logs.stdoutPath}`);
|
||||
|
||||
@@ -96,8 +96,12 @@ export type DaemonStatus = {
|
||||
};
|
||||
|
||||
function shouldReportPortUsage(status: PortUsageStatus | undefined, rpcOk?: boolean) {
|
||||
if (status !== "busy") return false;
|
||||
if (rpcOk === true) return false;
|
||||
if (status !== "busy") {
|
||||
return false;
|
||||
}
|
||||
if (rpcOk === true) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -271,7 +275,9 @@ export async function gatherDaemonStatus(
|
||||
}
|
||||
|
||||
export function renderPortDiagnosticsForCli(status: DaemonStatus, rpcOk?: boolean): string[] {
|
||||
if (!status.port || !shouldReportPortUsage(status.port.status, rpcOk)) return [];
|
||||
if (!status.port || !shouldReportPortUsage(status.port.status, rpcOk)) {
|
||||
return [];
|
||||
}
|
||||
return formatPortDiagnostics({
|
||||
port: status.port.port,
|
||||
status: status.port.status,
|
||||
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
|
||||
function sanitizeDaemonStatusForJson(status: DaemonStatus): DaemonStatus {
|
||||
const command = status.service.command;
|
||||
if (!command?.environment) return status;
|
||||
if (!command?.environment) {
|
||||
return status;
|
||||
}
|
||||
const safeEnv = filterDaemonEnv(command.environment);
|
||||
const nextCommand = {
|
||||
...command,
|
||||
@@ -189,7 +191,9 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
||||
defaultRuntime.log(`${label("RPC probe:")} ${okText("ok")}`);
|
||||
} else {
|
||||
defaultRuntime.error(`${label("RPC probe:")} ${errorText("failed")}`);
|
||||
if (rpc.url) defaultRuntime.error(`${label("RPC target:")} ${rpc.url}`);
|
||||
if (rpc.url) {
|
||||
defaultRuntime.error(`${label("RPC target:")} ${rpc.url}`);
|
||||
}
|
||||
const lines = String(rpc.error ?? "unknown")
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -52,11 +52,17 @@ type DevicePairingList = {
|
||||
|
||||
function formatAge(msAgo: number) {
|
||||
const s = Math.max(0, Math.floor(msAgo / 1000));
|
||||
if (s < 60) return `${s}s`;
|
||||
if (s < 60) {
|
||||
return `${s}s`;
|
||||
}
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
if (m < 60) {
|
||||
return `${m}m`;
|
||||
}
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h`;
|
||||
if (h < 24) {
|
||||
return `${h}h`;
|
||||
}
|
||||
const d = Math.floor(h / 24);
|
||||
return `${d}d`;
|
||||
}
|
||||
@@ -98,7 +104,9 @@ function parseDevicePairingList(value: unknown): DevicePairingList {
|
||||
}
|
||||
|
||||
function formatTokenSummary(tokens: DeviceTokenSummary[] | undefined) {
|
||||
if (!tokens || tokens.length === 0) return "none";
|
||||
if (!tokens || tokens.length === 0) {
|
||||
return "none";
|
||||
}
|
||||
const parts = tokens
|
||||
.map((t) => `${t.role}${t.revokedAtMs ? " (revoked)" : ""}`)
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
|
||||
@@ -12,14 +12,22 @@ import { renderTable } from "../terminal/table.js";
|
||||
|
||||
function parseLimit(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
if (value <= 0) return null;
|
||||
if (value <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.floor(value);
|
||||
}
|
||||
if (typeof value !== "string") return null;
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const raw = value.trim();
|
||||
if (!raw) return null;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -60,7 +68,9 @@ export function registerDirectoryCli(program: Command) {
|
||||
});
|
||||
const channelId = selection.channel;
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
if (!plugin) throw new Error(`Unsupported channel: ${String(channelId)}`);
|
||||
if (!plugin) {
|
||||
throw new Error(`Unsupported channel: ${String(channelId)}`);
|
||||
}
|
||||
const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
return { cfg, channelId, accountId, plugin };
|
||||
};
|
||||
@@ -73,7 +83,9 @@ export function registerDirectoryCli(program: Command) {
|
||||
account: opts.account as string | undefined,
|
||||
});
|
||||
const fn = plugin.directory?.self;
|
||||
if (!fn) throw new Error(`Channel ${channelId} does not support directory self`);
|
||||
if (!fn) {
|
||||
throw new Error(`Channel ${channelId} does not support directory self`);
|
||||
}
|
||||
const result = await fn({ cfg, accountId, runtime: defaultRuntime });
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
@@ -113,7 +125,9 @@ export function registerDirectoryCli(program: Command) {
|
||||
account: opts.account as string | undefined,
|
||||
});
|
||||
const fn = plugin.directory?.listPeers;
|
||||
if (!fn) throw new Error(`Channel ${channelId} does not support directory peers`);
|
||||
if (!fn) {
|
||||
throw new Error(`Channel ${channelId} does not support directory peers`);
|
||||
}
|
||||
const result = await fn({
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -158,7 +172,9 @@ export function registerDirectoryCli(program: Command) {
|
||||
account: opts.account as string | undefined,
|
||||
});
|
||||
const fn = plugin.directory?.listGroups;
|
||||
if (!fn) throw new Error(`Channel ${channelId} does not support directory groups`);
|
||||
if (!fn) {
|
||||
throw new Error(`Channel ${channelId} does not support directory groups`);
|
||||
}
|
||||
const result = await fn({
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -206,9 +222,13 @@ export function registerDirectoryCli(program: Command) {
|
||||
account: opts.account as string | undefined,
|
||||
});
|
||||
const fn = plugin.directory?.listGroupMembers;
|
||||
if (!fn) throw new Error(`Channel ${channelId} does not support group members listing`);
|
||||
if (!fn) {
|
||||
throw new Error(`Channel ${channelId} does not support group members listing`);
|
||||
}
|
||||
const groupId = String(opts.groupId ?? "").trim();
|
||||
if (!groupId) throw new Error("Missing --group-id");
|
||||
if (!groupId) {
|
||||
throw new Error("Missing --group-id");
|
||||
}
|
||||
const result = await fn({
|
||||
cfg,
|
||||
accountId,
|
||||
|
||||
@@ -19,7 +19,9 @@ function run(cmd: string, args: string[], opts?: RunOpts): string {
|
||||
encoding: "utf-8",
|
||||
stdio: opts?.inherit ? "inherit" : "pipe",
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
if (!opts?.allowFailure && res.status !== 0) {
|
||||
const errText =
|
||||
typeof res.stderr === "string" && res.stderr.trim()
|
||||
@@ -46,7 +48,9 @@ function writeFileSudoIfNeeded(filePath: string, content: string): void {
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "ignore", "inherit"],
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
if (res.status !== 0) {
|
||||
throw new Error(`sudo tee ${filePath} failed: exit ${res.status ?? "unknown"}`);
|
||||
}
|
||||
@@ -67,7 +71,9 @@ function mkdirSudoIfNeeded(dirPath: string): void {
|
||||
}
|
||||
|
||||
function zoneFileNeedsBootstrap(zonePath: string): boolean {
|
||||
if (!fs.existsSync(zonePath)) return true;
|
||||
if (!fs.existsSync(zonePath)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(zonePath, "utf-8");
|
||||
return !/\bSOA\b/.test(content) || !/\bNS\b/.test(content);
|
||||
@@ -79,13 +85,17 @@ function zoneFileNeedsBootstrap(zonePath: string): boolean {
|
||||
function detectBrewPrefix(): string {
|
||||
const out = run("brew", ["--prefix"]);
|
||||
const prefix = out.trim();
|
||||
if (!prefix) throw new Error("failed to resolve Homebrew prefix");
|
||||
if (!prefix) {
|
||||
throw new Error("failed to resolve Homebrew prefix");
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
function ensureImportLine(corefilePath: string, importGlob: string): boolean {
|
||||
const existing = fs.readFileSync(corefilePath, "utf-8");
|
||||
if (existing.includes(importGlob)) return false;
|
||||
if (existing.includes(importGlob)) {
|
||||
return false;
|
||||
}
|
||||
const next = `${existing.replace(/\s*$/, "")}\n\nimport ${importGlob}\n`;
|
||||
writeFileSudoIfNeeded(corefilePath, next);
|
||||
return true;
|
||||
|
||||
@@ -34,11 +34,17 @@ type ExecApprovalsCliOpts = NodesRpcOpts & {
|
||||
|
||||
function formatAge(msAgo: number) {
|
||||
const s = Math.max(0, Math.floor(msAgo / 1000));
|
||||
if (s < 60) return `${s}s`;
|
||||
if (s < 60) {
|
||||
return `${s}s`;
|
||||
}
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
if (m < 60) {
|
||||
return `${m}m`;
|
||||
}
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h`;
|
||||
if (h < 24) {
|
||||
return `${h}h`;
|
||||
}
|
||||
const d = Math.floor(h / 24);
|
||||
return `${d}d`;
|
||||
}
|
||||
@@ -52,9 +58,13 @@ async function readStdin(): Promise<string> {
|
||||
}
|
||||
|
||||
async function resolveTargetNodeId(opts: ExecApprovalsCliOpts): Promise<string | null> {
|
||||
if (opts.gateway) return null;
|
||||
if (opts.gateway) {
|
||||
return null;
|
||||
}
|
||||
const raw = opts.node?.trim() ?? "";
|
||||
if (!raw) return null;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
return await resolveNodeId(opts as NodesRpcOpts, raw);
|
||||
}
|
||||
|
||||
@@ -125,7 +135,9 @@ function renderApprovalsSnapshot(snapshot: ExecApprovalsSnapshot, targetLabel: s
|
||||
const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : [];
|
||||
for (const entry of allowlist) {
|
||||
const pattern = entry?.pattern?.trim() ?? "";
|
||||
if (!pattern) continue;
|
||||
if (!pattern) {
|
||||
continue;
|
||||
}
|
||||
const lastUsedAt = typeof entry.lastUsedAt === "number" ? entry.lastUsedAt : null;
|
||||
allowlistRows.push({
|
||||
Target: targetLabel,
|
||||
|
||||
@@ -32,16 +32,22 @@ async function withEnvOverride<T>(
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
for (const key of Object.keys(overrides)) {
|
||||
saved[key] = process.env[key];
|
||||
if (overrides[key] === undefined) delete process.env[key];
|
||||
else process.env[key] = overrides[key];
|
||||
if (overrides[key] === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = overrides[key];
|
||||
}
|
||||
}
|
||||
vi.resetModules();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
for (const key of Object.keys(saved)) {
|
||||
if (saved[key] === undefined) delete process.env[key];
|
||||
else process.env[key] = saved[key];
|
||||
if (saved[key] === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = saved[key];
|
||||
}
|
||||
}
|
||||
vi.resetModules();
|
||||
}
|
||||
@@ -284,10 +290,14 @@ describe("gateway-cli coverage", () => {
|
||||
),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
for (const listener of process.listeners("SIGTERM")) {
|
||||
if (!beforeSigterm.has(listener)) process.removeListener("SIGTERM", listener);
|
||||
if (!beforeSigterm.has(listener)) {
|
||||
process.removeListener("SIGTERM", listener);
|
||||
}
|
||||
}
|
||||
for (const listener of process.listeners("SIGINT")) {
|
||||
if (!beforeSigint.has(listener)) process.removeListener("SIGINT", listener);
|
||||
if (!beforeSigint.has(listener)) {
|
||||
process.removeListener("SIGINT", listener);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -21,9 +21,13 @@ const DEV_TEMPLATE_DIR = path.resolve(
|
||||
async function loadDevTemplate(name: string, fallback: string): Promise<string> {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(path.join(DEV_TEMPLATE_DIR, name), "utf-8");
|
||||
if (!raw.startsWith("---")) return raw;
|
||||
if (!raw.startsWith("---")) {
|
||||
return raw;
|
||||
}
|
||||
const endIndex = raw.indexOf("\n---", 3);
|
||||
if (endIndex === -1) return raw;
|
||||
if (endIndex === -1) {
|
||||
return raw;
|
||||
}
|
||||
return raw.slice(endIndex + "\n---".length).replace(/^\s+/, "");
|
||||
} catch {
|
||||
return fallback;
|
||||
@@ -33,7 +37,9 @@ async function loadDevTemplate(name: string, fallback: string): Promise<string>
|
||||
const resolveDevWorkspaceDir = (env: NodeJS.ProcessEnv = process.env): string => {
|
||||
const baseDir = resolveDefaultAgentWorkspaceDir(env, os.homedir);
|
||||
const profile = env.OPENCLAW_PROFILE?.trim().toLowerCase();
|
||||
if (profile === "dev") return baseDir;
|
||||
if (profile === "dev") {
|
||||
return baseDir;
|
||||
}
|
||||
return `${baseDir}-${DEV_AGENT_WORKSPACE_SUFFIX}`;
|
||||
};
|
||||
|
||||
@@ -45,7 +51,9 @@ async function writeFileIfMissing(filePath: string, content: string) {
|
||||
});
|
||||
} catch (err) {
|
||||
const anyErr = err as { code?: string };
|
||||
if (anyErr.code !== "EEXIST") throw err;
|
||||
if (anyErr.code !== "EEXIST") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +100,9 @@ export async function ensureDevGatewayConfig(opts: { reset?: boolean }) {
|
||||
const io = createConfigIO();
|
||||
const configPath = io.configPath;
|
||||
const configExists = fs.existsSync(configPath);
|
||||
if (!opts.reset && configExists) return;
|
||||
if (!opts.reset && configExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeConfigFile({
|
||||
gateway: {
|
||||
|
||||
@@ -7,7 +7,9 @@ export type GatewayDiscoverOpts = {
|
||||
};
|
||||
|
||||
export function parseDiscoverTimeoutMs(raw: unknown, fallbackMs: number): number {
|
||||
if (raw === undefined || raw === null) return fallbackMs;
|
||||
if (raw === undefined || raw === null) {
|
||||
return fallbackMs;
|
||||
}
|
||||
const value =
|
||||
typeof raw === "string"
|
||||
? raw.trim()
|
||||
@@ -17,7 +19,9 @@ export function parseDiscoverTimeoutMs(raw: unknown, fallbackMs: number): number
|
||||
if (value === null) {
|
||||
throw new Error("invalid --timeout");
|
||||
}
|
||||
if (!value) return fallbackMs;
|
||||
if (!value) {
|
||||
return fallbackMs;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
throw new Error(`invalid --timeout: ${value}`);
|
||||
@@ -48,7 +52,9 @@ export function dedupeBeacons(beacons: GatewayBonjourBeacon[]): GatewayBonjourBe
|
||||
String(b.port ?? ""),
|
||||
String(b.gatewayPort ?? ""),
|
||||
].join("|");
|
||||
if (seen.has(key)) continue;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
out.push(b);
|
||||
}
|
||||
|
||||
@@ -31,9 +31,13 @@ import {
|
||||
import { addGatewayRunCommand } from "./run.js";
|
||||
|
||||
function styleHealthChannelLine(line: string, rich: boolean): string {
|
||||
if (!rich) return line;
|
||||
if (!rich) {
|
||||
return line;
|
||||
}
|
||||
const colon = line.indexOf(":");
|
||||
if (colon === -1) return line;
|
||||
if (colon === -1) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const label = line.slice(0, colon + 1);
|
||||
const detail = line.slice(colon + 1).trimStart();
|
||||
@@ -42,13 +46,27 @@ function styleHealthChannelLine(line: string, rich: boolean): string {
|
||||
const applyPrefix = (prefix: string, color: (value: string) => string) =>
|
||||
`${label} ${color(detail.slice(0, prefix.length))}${detail.slice(prefix.length)}`;
|
||||
|
||||
if (normalized.startsWith("failed")) return applyPrefix("failed", theme.error);
|
||||
if (normalized.startsWith("ok")) return applyPrefix("ok", theme.success);
|
||||
if (normalized.startsWith("linked")) return applyPrefix("linked", theme.success);
|
||||
if (normalized.startsWith("configured")) return applyPrefix("configured", theme.success);
|
||||
if (normalized.startsWith("not linked")) return applyPrefix("not linked", theme.warn);
|
||||
if (normalized.startsWith("not configured")) return applyPrefix("not configured", theme.muted);
|
||||
if (normalized.startsWith("unknown")) return applyPrefix("unknown", theme.warn);
|
||||
if (normalized.startsWith("failed")) {
|
||||
return applyPrefix("failed", theme.error);
|
||||
}
|
||||
if (normalized.startsWith("ok")) {
|
||||
return applyPrefix("ok", theme.success);
|
||||
}
|
||||
if (normalized.startsWith("linked")) {
|
||||
return applyPrefix("linked", theme.success);
|
||||
}
|
||||
if (normalized.startsWith("configured")) {
|
||||
return applyPrefix("configured", theme.success);
|
||||
}
|
||||
if (normalized.startsWith("not linked")) {
|
||||
return applyPrefix("not linked", theme.warn);
|
||||
}
|
||||
if (normalized.startsWith("not configured")) {
|
||||
return applyPrefix("not configured", theme.muted);
|
||||
}
|
||||
if (normalized.startsWith("unknown")) {
|
||||
return applyPrefix("unknown", theme.warn);
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
@@ -62,10 +80,14 @@ function runGatewayCommand(action: () => Promise<void>, label?: string) {
|
||||
}
|
||||
|
||||
function parseDaysOption(raw: unknown, fallback = 30): number {
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(1, Math.floor(raw));
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||
return Math.max(1, Math.floor(raw));
|
||||
}
|
||||
if (typeof raw === "string" && raw.trim() !== "") {
|
||||
const parsed = Number(raw);
|
||||
if (Number.isFinite(parsed)) return Math.max(1, Math.floor(parsed));
|
||||
if (Number.isFinite(parsed)) {
|
||||
return Math.max(1, Math.floor(parsed));
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
@@ -324,7 +346,9 @@ export function registerGatewayCli(program: Command) {
|
||||
`Found ${deduped.length} gateway(s) · domains: ${domains.join(", ")}`,
|
||||
),
|
||||
);
|
||||
if (deduped.length === 0) return;
|
||||
if (deduped.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const beacon of deduped) {
|
||||
for (const line of renderBeaconLines(beacon, rich)) {
|
||||
|
||||
@@ -8,30 +8,48 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
|
||||
export function parsePort(raw: unknown): number | null {
|
||||
if (raw === undefined || raw === null) return null;
|
||||
if (raw === undefined || raw === null) {
|
||||
return null;
|
||||
}
|
||||
const value =
|
||||
typeof raw === "string"
|
||||
? raw
|
||||
: typeof raw === "number" || typeof raw === "bigint"
|
||||
? raw.toString()
|
||||
: null;
|
||||
if (value === null) return null;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export const toOptionString = (value: unknown): string | undefined => {
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "bigint") return value.toString();
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "bigint") {
|
||||
return value.toString();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export function describeUnknownError(err: unknown): string {
|
||||
if (err instanceof Error) return err.message;
|
||||
if (typeof err === "string") return err;
|
||||
if (typeof err === "number" || typeof err === "bigint") return err.toString();
|
||||
if (typeof err === "boolean") return err ? "true" : "false";
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
if (typeof err === "string") {
|
||||
return err;
|
||||
}
|
||||
if (typeof err === "number" || typeof err === "bigint") {
|
||||
return err.toString();
|
||||
}
|
||||
if (typeof err === "boolean") {
|
||||
return err ? "true" : "false";
|
||||
}
|
||||
if (err && typeof err === "object") {
|
||||
if ("message" in err && typeof err.message === "string") {
|
||||
return err.message;
|
||||
@@ -94,7 +112,9 @@ export async function maybeExplainGatewayServiceStop() {
|
||||
} catch {
|
||||
loaded = null;
|
||||
}
|
||||
if (loaded === false) return;
|
||||
if (loaded === false) {
|
||||
return;
|
||||
}
|
||||
defaultRuntime.error(
|
||||
loaded
|
||||
? `Gateway service appears ${service.loadedText}. Stop it first.`
|
||||
|
||||
@@ -67,7 +67,9 @@ describe("gateway SIGTERM", () => {
|
||||
let child: ReturnType<typeof spawn> | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (!child || child.killed) return;
|
||||
if (!child || child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch {
|
||||
@@ -124,7 +126,9 @@ describe("gateway SIGTERM", () => {
|
||||
});
|
||||
|
||||
const proc = child;
|
||||
if (!proc) throw new Error("failed to spawn gateway");
|
||||
if (!proc) {
|
||||
throw new Error("failed to spawn gateway");
|
||||
}
|
||||
|
||||
child.stdout?.setEncoding("utf8");
|
||||
child.stderr?.setEncoding("utf8");
|
||||
@@ -148,7 +152,9 @@ describe("gateway SIGTERM", () => {
|
||||
`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
|
||||
);
|
||||
}
|
||||
if (result.code === null && result.signal === "SIGTERM") return;
|
||||
if (result.code === null && result.signal === "SIGTERM") {
|
||||
return;
|
||||
}
|
||||
expect(result.signal).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,9 @@ export function formatHelpExample(command: string, description: string): string
|
||||
}
|
||||
|
||||
export function formatHelpExampleLine(command: string, description: string): string {
|
||||
if (!description) return ` ${theme.command(command)}`;
|
||||
if (!description) {
|
||||
return ` ${theme.command(command)}`;
|
||||
}
|
||||
return ` ${theme.command(command)} ${theme.muted(`# ${description}`)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,12 @@ function buildHooksReport(config: OpenClawConfig): HookStatusReport {
|
||||
}
|
||||
|
||||
function formatHookStatus(hook: HookStatusEntry): string {
|
||||
if (hook.eligible) return theme.success("✓ ready");
|
||||
if (hook.disabled) return theme.warn("⏸ disabled");
|
||||
if (hook.eligible) {
|
||||
return theme.success("✓ ready");
|
||||
}
|
||||
if (hook.disabled) {
|
||||
return theme.warn("⏸ disabled");
|
||||
}
|
||||
return theme.error("✗ missing");
|
||||
}
|
||||
|
||||
@@ -78,7 +82,9 @@ function formatHookName(hook: HookStatusEntry): string {
|
||||
}
|
||||
|
||||
function formatHookSource(hook: HookStatusEntry): string {
|
||||
if (!hook.managedByPlugin) return hook.source;
|
||||
if (!hook.managedByPlugin) {
|
||||
return hook.source;
|
||||
}
|
||||
return `plugin:${hook.pluginId ?? "unknown"}`;
|
||||
}
|
||||
|
||||
@@ -326,13 +332,24 @@ export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptio
|
||||
lines.push(theme.heading("Hooks not ready:"));
|
||||
for (const hook of notEligible) {
|
||||
const reasons = [];
|
||||
if (hook.disabled) reasons.push("disabled");
|
||||
if (hook.missing.bins.length > 0) reasons.push(`bins: ${hook.missing.bins.join(", ")}`);
|
||||
if (hook.missing.anyBins.length > 0)
|
||||
if (hook.disabled) {
|
||||
reasons.push("disabled");
|
||||
}
|
||||
if (hook.missing.bins.length > 0) {
|
||||
reasons.push(`bins: ${hook.missing.bins.join(", ")}`);
|
||||
}
|
||||
if (hook.missing.anyBins.length > 0) {
|
||||
reasons.push(`anyBins: ${hook.missing.anyBins.join(", ")}`);
|
||||
if (hook.missing.env.length > 0) reasons.push(`env: ${hook.missing.env.join(", ")}`);
|
||||
if (hook.missing.config.length > 0) reasons.push(`config: ${hook.missing.config.join(", ")}`);
|
||||
if (hook.missing.os.length > 0) reasons.push(`os: ${hook.missing.os.join(", ")}`);
|
||||
}
|
||||
if (hook.missing.env.length > 0) {
|
||||
reasons.push(`env: ${hook.missing.env.join(", ")}`);
|
||||
}
|
||||
if (hook.missing.config.length > 0) {
|
||||
reasons.push(`config: ${hook.missing.config.join(", ")}`);
|
||||
}
|
||||
if (hook.missing.os.length > 0) {
|
||||
reasons.push(`os: ${hook.missing.os.join(", ")}`);
|
||||
}
|
||||
lines.push(` ${hook.emoji ?? "🔗"} ${hook.name} - ${reasons.join("; ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ type LogsCliOptions = {
|
||||
};
|
||||
|
||||
function parsePositiveInt(value: string | undefined, fallback: number): number {
|
||||
if (!value) return fallback;
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
@@ -58,10 +60,16 @@ async function fetchLogs(
|
||||
}
|
||||
|
||||
function formatLogTimestamp(value?: string, mode: "pretty" | "plain" = "plain") {
|
||||
if (!value) return "";
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return value;
|
||||
if (mode === "pretty") return parsed.toISOString().slice(11, 19);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return value;
|
||||
}
|
||||
if (mode === "pretty") {
|
||||
return parsed.toISOString().slice(11, 19);
|
||||
}
|
||||
return parsed.toISOString();
|
||||
}
|
||||
|
||||
@@ -73,7 +81,9 @@ function formatLogLine(
|
||||
},
|
||||
): string {
|
||||
const parsed = parseLogLine(raw);
|
||||
if (!parsed) return raw;
|
||||
if (!parsed) {
|
||||
return raw;
|
||||
}
|
||||
const label = parsed.subsystem ?? parsed.module ?? "";
|
||||
const time = formatLogTimestamp(parsed.time, opts.pretty ? "pretty" : "plain");
|
||||
const level = parsed.level ?? "";
|
||||
@@ -162,8 +172,12 @@ function emitGatewayError(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!errorLine(colorize(rich, theme.error, message))) return;
|
||||
if (!errorLine(details.message)) return;
|
||||
if (!errorLine(colorize(rich, theme.error, message))) {
|
||||
return;
|
||||
}
|
||||
if (!errorLine(details.message)) {
|
||||
return;
|
||||
}
|
||||
errorLine(colorize(rich, theme.muted, hint));
|
||||
}
|
||||
|
||||
@@ -288,7 +302,9 @@ export function registerLogsCli(program: Command) {
|
||||
: cursor;
|
||||
first = false;
|
||||
|
||||
if (!opts.follow) return;
|
||||
if (!opts.follow) {
|
||||
return;
|
||||
}
|
||||
await delay(interval);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,13 +60,17 @@ function formatSourceLabel(source: string, workspaceDir: string, agentId: string
|
||||
|
||||
function resolveAgent(cfg: ReturnType<typeof loadConfig>, agent?: string) {
|
||||
const trimmed = agent?.trim();
|
||||
if (trimmed) return trimmed;
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return resolveDefaultAgentId(cfg);
|
||||
}
|
||||
|
||||
function resolveAgentIds(cfg: ReturnType<typeof loadConfig>, agent?: string): string[] {
|
||||
const trimmed = agent?.trim();
|
||||
if (trimmed) return [trimmed];
|
||||
if (trimmed) {
|
||||
return [trimmed];
|
||||
}
|
||||
const list = cfg.agents?.list ?? [];
|
||||
if (list.length > 0) {
|
||||
return list.map((entry) => entry.id).filter(Boolean);
|
||||
@@ -84,7 +88,9 @@ async function checkReadableFile(pathname: string): Promise<{ exists: boolean; i
|
||||
return { exists: true };
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "ENOENT") return { exists: false };
|
||||
if (code === "ENOENT") {
|
||||
return { exists: false };
|
||||
}
|
||||
return {
|
||||
exists: true,
|
||||
issue: `${shortenHomePath(pathname)} not readable (${code ?? "error"})`,
|
||||
@@ -125,16 +131,24 @@ async function scanMemoryFiles(
|
||||
|
||||
const primary = await checkReadableFile(memoryFile);
|
||||
const alt = await checkReadableFile(altMemoryFile);
|
||||
if (primary.issue) issues.push(primary.issue);
|
||||
if (alt.issue) issues.push(alt.issue);
|
||||
if (primary.issue) {
|
||||
issues.push(primary.issue);
|
||||
}
|
||||
if (alt.issue) {
|
||||
issues.push(alt.issue);
|
||||
}
|
||||
|
||||
const resolvedExtraPaths = normalizeExtraMemoryPaths(workspaceDir, extraPaths);
|
||||
for (const extraPath of resolvedExtraPaths) {
|
||||
try {
|
||||
const stat = await fs.lstat(extraPath);
|
||||
if (stat.isSymbolicLink()) continue;
|
||||
if (stat.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
const extraCheck = await checkReadableFile(extraPath);
|
||||
if (extraCheck.issue) issues.push(extraCheck.issue);
|
||||
if (extraCheck.issue) {
|
||||
issues.push(extraCheck.issue);
|
||||
}
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "ENOENT") {
|
||||
@@ -185,8 +199,12 @@ async function scanMemoryFiles(
|
||||
} else {
|
||||
const files = new Set<string>(listedOk ? listed : []);
|
||||
if (!listedOk) {
|
||||
if (primary.exists) files.add(memoryFile);
|
||||
if (alt.exists) files.add(altMemoryFile);
|
||||
if (primary.exists) {
|
||||
files.add(memoryFile);
|
||||
}
|
||||
if (alt.exists) {
|
||||
files.add(altMemoryFile);
|
||||
}
|
||||
}
|
||||
totalFiles = files.size;
|
||||
}
|
||||
@@ -274,7 +292,9 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
||||
total: syncUpdate.total,
|
||||
label: syncUpdate.label,
|
||||
});
|
||||
if (syncUpdate.label) progress.setLabel(syncUpdate.label);
|
||||
if (syncUpdate.label) {
|
||||
progress.setLabel(syncUpdate.label);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -532,10 +552,14 @@ export function registerMemoryCli(program: Command) {
|
||||
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
||||
};
|
||||
const formatEta = () => {
|
||||
if (lastTotal <= 0 || lastCompleted <= 0) return null;
|
||||
if (lastTotal <= 0 || lastCompleted <= 0) {
|
||||
return null;
|
||||
}
|
||||
const elapsedMs = Math.max(1, Date.now() - startedAt);
|
||||
const rate = lastCompleted / elapsedMs;
|
||||
if (!Number.isFinite(rate) || rate <= 0) return null;
|
||||
if (!Number.isFinite(rate) || rate <= 0) {
|
||||
return null;
|
||||
}
|
||||
const remainingMs = Math.max(0, (lastTotal - lastCompleted) / rate);
|
||||
const seconds = Math.floor(remainingMs / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
@@ -564,7 +588,9 @@ export function registerMemoryCli(program: Command) {
|
||||
reason: "cli",
|
||||
force: opts.force,
|
||||
progress: (syncUpdate) => {
|
||||
if (syncUpdate.label) lastLabel = syncUpdate.label;
|
||||
if (syncUpdate.label) {
|
||||
lastLabel = syncUpdate.label;
|
||||
}
|
||||
lastCompleted = syncUpdate.completed;
|
||||
lastTotal = syncUpdate.total;
|
||||
update({
|
||||
|
||||
@@ -113,7 +113,9 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
hints?: string[];
|
||||
warnings?: string[];
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "install", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
@@ -127,7 +129,9 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
if (hints?.length) {
|
||||
for (const hint of hints) defaultRuntime.log(`Tip: ${hint}`);
|
||||
for (const hint of hints) {
|
||||
defaultRuntime.log(`Tip: ${hint}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
@@ -187,8 +191,11 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
displayName: opts.displayName,
|
||||
runtime: runtimeRaw,
|
||||
warn: (message) => {
|
||||
if (json) warnings.push(message);
|
||||
else defaultRuntime.log(message);
|
||||
if (json) {
|
||||
warnings.push(message);
|
||||
} else {
|
||||
defaultRuntime.log(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -235,12 +242,17 @@ export async function runNodeDaemonUninstall(opts: NodeDaemonLifecycleOptions =
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "uninstall", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) emit({ ok: false, error: message });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
@@ -286,12 +298,17 @@ export async function runNodeDaemonStart(opts: NodeDaemonLifecycleOptions = {})
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "start", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (json) emit({ ok: false, error: message, hints });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message, hints });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
@@ -363,12 +380,17 @@ export async function runNodeDaemonRestart(opts: NodeDaemonLifecycleOptions = {}
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "restart", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (json) emit({ ok: false, error: message, hints });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message, hints });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
@@ -439,12 +461,17 @@ export async function runNodeDaemonStop(opts: NodeDaemonLifecycleOptions = {}) {
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
emitDaemonActionJson({ action: "stop", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) emit({ ok: false, error: message });
|
||||
else defaultRuntime.error(message);
|
||||
if (json) {
|
||||
emit({ ok: false, error: message });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ export function validateA2UIJsonl(jsonl: string) {
|
||||
|
||||
lines.forEach((line, idx) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
messageCount += 1;
|
||||
let obj: unknown;
|
||||
try {
|
||||
|
||||
@@ -22,7 +22,9 @@ export function runNodesCommand(label: string, action: () => Promise<void>) {
|
||||
const { error, warn } = getNodesTheme();
|
||||
defaultRuntime.error(error(`nodes ${label} failed: ${message}`));
|
||||
const hint = unauthorizedHintForMessage(message);
|
||||
if (hint) defaultRuntime.error(warn(hint));
|
||||
if (hint) {
|
||||
defaultRuntime.error(warn(hint));
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,11 +2,17 @@ import type { NodeListNode, PairedNode, PairingList, PendingRequest } from "./ty
|
||||
|
||||
export function formatAge(msAgo: number) {
|
||||
const s = Math.max(0, Math.floor(msAgo / 1000));
|
||||
if (s < 60) return `${s}s`;
|
||||
if (s < 60) {
|
||||
return `${s}s`;
|
||||
}
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
if (m < 60) {
|
||||
return `${m}m`;
|
||||
}
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h`;
|
||||
if (h < 24) {
|
||||
return `${h}h`;
|
||||
}
|
||||
const d = Math.floor(h / 24);
|
||||
return `${d}d`;
|
||||
}
|
||||
@@ -24,12 +30,16 @@ export function parseNodeList(value: unknown): NodeListNode[] {
|
||||
}
|
||||
|
||||
export function formatPermissions(raw: unknown) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return null;
|
||||
}
|
||||
const entries = Object.entries(raw as Record<string, unknown>)
|
||||
.map(([key, value]) => [String(key).trim(), value === true] as const)
|
||||
.filter(([key]) => key.length > 0)
|
||||
.toSorted((a, b) => a[0].localeCompare(b[0]));
|
||||
if (entries.length === 0) return null;
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const parts = entries.map(([key, granted]) => `${key}=${granted ? "yes" : "no"}`);
|
||||
return `[${parts.join(", ")}]`;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ const parseFacing = (value: string): CameraFacing => {
|
||||
const v = String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (v === "front" || v === "back") return v;
|
||||
if (v === "front" || v === "back") {
|
||||
return v;
|
||||
}
|
||||
throw new Error(`invalid facing: ${value} (expected front|back)`);
|
||||
};
|
||||
|
||||
|
||||
@@ -112,7 +112,9 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
height: opts.height ? Number.parseFloat(opts.height) : undefined,
|
||||
};
|
||||
const params: Record<string, unknown> = {};
|
||||
if (opts.target) params.url = String(opts.target);
|
||||
if (opts.target) {
|
||||
params.url = String(opts.target);
|
||||
}
|
||||
if (
|
||||
Number.isFinite(placement.x) ||
|
||||
Number.isFinite(placement.y) ||
|
||||
@@ -176,7 +178,9 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
.action(async (jsArg: string | undefined, opts: NodesRpcOpts) => {
|
||||
await runNodesCommand("canvas eval", async () => {
|
||||
const js = opts.js ?? jsArg;
|
||||
if (!js) throw new Error("missing --js or <js>");
|
||||
if (!js) {
|
||||
throw new Error("missing --js or <js>");
|
||||
}
|
||||
const raw = await invokeCanvas(opts, "canvas.eval", {
|
||||
javaScript: js,
|
||||
});
|
||||
@@ -188,8 +192,9 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: { result?: string } }).payload
|
||||
: undefined;
|
||||
if (payload?.result) defaultRuntime.log(payload.result);
|
||||
else {
|
||||
if (payload?.result) {
|
||||
defaultRuntime.log(payload.result);
|
||||
} else {
|
||||
const { ok } = getNodesTheme();
|
||||
defaultRuntime.log(ok("canvas eval ok"));
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ function normalizeExecAsk(value?: string | null): ExecAsk | null {
|
||||
}
|
||||
|
||||
function mergePathPrepend(existing: string | undefined, prepend: string[]) {
|
||||
if (prepend.length === 0) return existing;
|
||||
if (prepend.length === 0) {
|
||||
return existing;
|
||||
}
|
||||
const partsExisting = (existing ?? "")
|
||||
.split(path.delimiter)
|
||||
.map((part) => part.trim())
|
||||
@@ -66,7 +68,9 @@ function mergePathPrepend(existing: string | undefined, prepend: string[]) {
|
||||
const merged: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const part of [...prepend, ...partsExisting]) {
|
||||
if (seen.has(part)) continue;
|
||||
if (seen.has(part)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(part);
|
||||
merged.push(part);
|
||||
}
|
||||
@@ -78,10 +82,16 @@ function applyPathPrepend(
|
||||
prepend: string[] | undefined,
|
||||
options?: { requireExisting?: boolean },
|
||||
) {
|
||||
if (!Array.isArray(prepend) || prepend.length === 0) return;
|
||||
if (options?.requireExisting && !env.PATH) return;
|
||||
if (!Array.isArray(prepend) || prepend.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (options?.requireExisting && !env.PATH) {
|
||||
return;
|
||||
}
|
||||
const merged = mergePathPrepend(env.PATH, prepend);
|
||||
if (merged) env.PATH = merged;
|
||||
if (merged) {
|
||||
env.PATH = merged;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExecDefaults(
|
||||
@@ -341,8 +351,12 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
const timedOut = payload?.timedOut === true;
|
||||
const success = payload?.success === true;
|
||||
|
||||
if (stdout) process.stdout.write(stdout);
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
}
|
||||
if (stderr) {
|
||||
process.stderr.write(stderr);
|
||||
}
|
||||
if (timedOut) {
|
||||
const { error } = getNodesTheme();
|
||||
defaultRuntime.error(error("run timed out"));
|
||||
|
||||
@@ -10,8 +10,12 @@ import { shortenHomeInString } from "../../utils.js";
|
||||
|
||||
function formatVersionLabel(raw: string) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return raw;
|
||||
if (trimmed.toLowerCase().startsWith("v")) return trimmed;
|
||||
if (!trimmed) {
|
||||
return raw;
|
||||
}
|
||||
if (trimmed.toLowerCase().startsWith("v")) {
|
||||
return trimmed;
|
||||
}
|
||||
return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed;
|
||||
}
|
||||
|
||||
@@ -23,9 +27,13 @@ function resolveNodeVersions(node: {
|
||||
}) {
|
||||
const core = node.coreVersion?.trim() || undefined;
|
||||
const ui = node.uiVersion?.trim() || undefined;
|
||||
if (core || ui) return { core, ui };
|
||||
if (core || ui) {
|
||||
return { core, ui };
|
||||
}
|
||||
const legacy = node.version?.trim();
|
||||
if (!legacy) return { core: undefined, ui: undefined };
|
||||
if (!legacy) {
|
||||
return { core: undefined, ui: undefined };
|
||||
}
|
||||
const platform = node.platform?.trim().toLowerCase() ?? "";
|
||||
const headless =
|
||||
platform === "darwin" || platform === "linux" || platform === "win32" || platform === "windows";
|
||||
@@ -40,15 +48,23 @@ function formatNodeVersions(node: {
|
||||
}) {
|
||||
const { core, ui } = resolveNodeVersions(node);
|
||||
const parts: string[] = [];
|
||||
if (core) parts.push(`core ${formatVersionLabel(core)}`);
|
||||
if (ui) parts.push(`ui ${formatVersionLabel(ui)}`);
|
||||
if (core) {
|
||||
parts.push(`core ${formatVersionLabel(core)}`);
|
||||
}
|
||||
if (ui) {
|
||||
parts.push(`ui ${formatVersionLabel(ui)}`);
|
||||
}
|
||||
return parts.length > 0 ? parts.join(" · ") : null;
|
||||
}
|
||||
|
||||
function formatPathEnv(raw?: string): string | null {
|
||||
if (typeof raw !== "string") return null;
|
||||
if (typeof raw !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const parts = trimmed.split(":").filter(Boolean);
|
||||
const display =
|
||||
parts.length <= 3 ? trimmed : `${parts.slice(0, 2).join(":")}:…:${parts.slice(-1)[0]}`;
|
||||
@@ -56,7 +72,9 @@ function formatPathEnv(raw?: string): string | null {
|
||||
}
|
||||
|
||||
function parseSinceMs(raw: unknown, label: string): number | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
if (raw === undefined || raw === null) {
|
||||
return undefined;
|
||||
}
|
||||
const value =
|
||||
typeof raw === "string" ? raw.trim() : typeof raw === "number" ? String(raw).trim() : null;
|
||||
if (value === null) {
|
||||
@@ -64,7 +82,9 @@ function parseSinceMs(raw: unknown, label: string): number | undefined {
|
||||
defaultRuntime.exit(1);
|
||||
return undefined;
|
||||
}
|
||||
if (!value) return undefined;
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return parseDurationMs(value);
|
||||
} catch (err) {
|
||||
@@ -104,7 +124,9 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
)
|
||||
: null;
|
||||
const filtered = nodes.filter((n) => {
|
||||
if (connectedOnly && !n.connected) return false;
|
||||
if (connectedOnly && !n.connected) {
|
||||
return false;
|
||||
}
|
||||
if (sinceMs !== undefined) {
|
||||
const paired = lastConnectedById?.get(n.nodeId);
|
||||
const lastConnectedAtMs =
|
||||
@@ -113,8 +135,12 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
: typeof n.connectedAtMs === "number"
|
||||
? n.connectedAtMs
|
||||
: undefined;
|
||||
if (typeof lastConnectedAtMs !== "number") return false;
|
||||
if (now - lastConnectedAtMs > sinceMs) return false;
|
||||
if (typeof lastConnectedAtMs !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (now - lastConnectedAtMs > sinceMs) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -131,7 +157,9 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
defaultRuntime.log(
|
||||
`Known: ${filtered.length}${filteredLabel} · Paired: ${pairedCount} · Connected: ${connectedCount}`,
|
||||
);
|
||||
if (filtered.length === 0) return;
|
||||
if (filtered.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = filtered.map((n) => {
|
||||
const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId;
|
||||
@@ -261,7 +289,9 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
defaultRuntime.log(muted("- (none reported)"));
|
||||
return;
|
||||
}
|
||||
for (const c of commands) defaultRuntime.log(`- ${c}`);
|
||||
for (const c of commands) {
|
||||
defaultRuntime.log(`- ${c}`);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
@@ -294,7 +324,9 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
const filteredPaired = paired.filter((node) => {
|
||||
if (connectedOnly) {
|
||||
const live = connectedById?.get(node.nodeId);
|
||||
if (!live?.connected) return false;
|
||||
if (!live?.connected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (sinceMs !== undefined) {
|
||||
const live = connectedById?.get(node.nodeId);
|
||||
@@ -304,8 +336,12 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
: typeof live?.connectedAtMs === "number"
|
||||
? live.connectedAtMs
|
||||
: undefined;
|
||||
if (typeof lastConnectedAtMs !== "number") return false;
|
||||
if (now - lastConnectedAtMs > sinceMs) return false;
|
||||
if (typeof lastConnectedAtMs !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (now - lastConnectedAtMs > sinceMs) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -57,7 +57,9 @@ function normalizeNodeKey(value: string) {
|
||||
|
||||
export async function resolveNodeId(opts: NodesRpcOpts, query: string) {
|
||||
const q = String(query ?? "").trim();
|
||||
if (!q) throw new Error("node required");
|
||||
if (!q) {
|
||||
throw new Error("node required");
|
||||
}
|
||||
|
||||
let nodes: NodeListNode[] = [];
|
||||
try {
|
||||
@@ -77,15 +79,25 @@ export async function resolveNodeId(opts: NodesRpcOpts, query: string) {
|
||||
|
||||
const qNorm = normalizeNodeKey(q);
|
||||
const matches = nodes.filter((n) => {
|
||||
if (n.nodeId === q) return true;
|
||||
if (typeof n.remoteIp === "string" && n.remoteIp === q) return true;
|
||||
if (n.nodeId === q) {
|
||||
return true;
|
||||
}
|
||||
if (typeof n.remoteIp === "string" && n.remoteIp === q) {
|
||||
return true;
|
||||
}
|
||||
const name = typeof n.displayName === "string" ? n.displayName : "";
|
||||
if (name && normalizeNodeKey(name) === qNorm) return true;
|
||||
if (q.length >= 6 && n.nodeId.startsWith(q)) return true;
|
||||
if (name && normalizeNodeKey(name) === qNorm) {
|
||||
return true;
|
||||
}
|
||||
if (q.length >= 6 && n.nodeId.startsWith(q)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matches.length === 1) return matches[0].nodeId;
|
||||
if (matches.length === 1) {
|
||||
return matches[0].nodeId;
|
||||
}
|
||||
if (matches.length === 0) {
|
||||
const known = nodes
|
||||
.map((n) => n.displayName || n.remoteIp || n.nodeId)
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { parseTimeoutMs } from "./parse-timeout.js";
|
||||
|
||||
export function parseEnvPairs(pairs: unknown): Record<string, string> | undefined {
|
||||
if (!Array.isArray(pairs) || pairs.length === 0) return undefined;
|
||||
if (!Array.isArray(pairs) || pairs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const env: Record<string, string> = {};
|
||||
for (const pair of pairs) {
|
||||
if (typeof pair !== "string") continue;
|
||||
if (typeof pair !== "string") {
|
||||
continue;
|
||||
}
|
||||
const idx = pair.indexOf("=");
|
||||
if (idx <= 0) continue;
|
||||
if (idx <= 0) {
|
||||
continue;
|
||||
}
|
||||
const key = pair.slice(0, idx).trim();
|
||||
if (!key) continue;
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
env[key] = pair.slice(idx + 1);
|
||||
}
|
||||
return Object.keys(env).length > 0 ? env : undefined;
|
||||
|
||||
@@ -9,9 +9,15 @@ const pairingIdLabels: Record<string, string> = {
|
||||
discord: "discordUserId",
|
||||
};
|
||||
const normalizeChannelId = vi.fn((raw: string) => {
|
||||
if (!raw) return null;
|
||||
if (raw === "imsg") return "imessage";
|
||||
if (["telegram", "discord", "imessage"].includes(raw)) return raw;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (raw === "imsg") {
|
||||
return "imessage";
|
||||
}
|
||||
if (["telegram", "discord", "imessage"].includes(raw)) {
|
||||
return raw;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const getPairingAdapter = vi.fn((channel: string) => ({
|
||||
|
||||
@@ -25,7 +25,9 @@ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!value) throw new Error("Channel required");
|
||||
if (!value) {
|
||||
throw new Error("Channel required");
|
||||
}
|
||||
|
||||
const normalized = normalizeChannelId(value);
|
||||
if (normalized) {
|
||||
@@ -36,7 +38,9 @@ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel
|
||||
}
|
||||
|
||||
// Allow extension channels: validate format but don't require registry
|
||||
if (/^[a-z][a-z0-9_-]{0,63}$/.test(value)) return value as PairingChannel;
|
||||
if (/^[a-z][a-z0-9_-]{0,63}$/.test(value)) {
|
||||
return value as PairingChannel;
|
||||
}
|
||||
throw new Error(`Invalid channel: ${value}`);
|
||||
}
|
||||
|
||||
@@ -136,7 +140,9 @@ export function registerPairingCli(program: Command) {
|
||||
`${theme.success("Approved")} ${theme.muted(channel)} sender ${theme.command(approved.id)}.`,
|
||||
);
|
||||
|
||||
if (!opts.notify) return;
|
||||
if (!opts.notify) {
|
||||
return;
|
||||
}
|
||||
await notifyApproved(channel, approved.id).catch((err) => {
|
||||
defaultRuntime.log(theme.warn(`Failed to notify requester: ${String(err)}`));
|
||||
});
|
||||
|
||||
@@ -6,10 +6,14 @@ export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): num
|
||||
const trimmed = String(raw ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!trimmed) throw new Error("invalid duration (empty)");
|
||||
if (!trimmed) {
|
||||
throw new Error("invalid duration (empty)");
|
||||
}
|
||||
|
||||
const m = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(trimmed);
|
||||
if (!m) throw new Error(`invalid duration: ${raw}`);
|
||||
if (!m) {
|
||||
throw new Error(`invalid duration: ${raw}`);
|
||||
}
|
||||
|
||||
const value = Number(m[1]);
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
@@ -28,6 +32,8 @@ export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): num
|
||||
? 3_600_000
|
||||
: 86_400_000;
|
||||
const ms = Math.round(value * multiplier);
|
||||
if (!Number.isFinite(ms)) throw new Error(`invalid duration: ${raw}`);
|
||||
if (!Number.isFinite(ms)) {
|
||||
throw new Error(`invalid duration: ${raw}`);
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export function parseTimeoutMs(raw: unknown): number | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
if (raw === undefined || raw === null) {
|
||||
return undefined;
|
||||
}
|
||||
let value = Number.NaN;
|
||||
if (typeof raw === "number") {
|
||||
value = raw;
|
||||
@@ -7,7 +9,9 @@ export function parseTimeoutMs(raw: unknown): number | undefined {
|
||||
value = Number(raw);
|
||||
} else if (typeof raw === "string") {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
value = Number.parseInt(trimmed, 10);
|
||||
}
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
|
||||
@@ -8,7 +8,9 @@ const log = createSubsystemLogger("plugins");
|
||||
let pluginRegistryLoaded = false;
|
||||
|
||||
export function ensurePluginRegistryLoaded(): void {
|
||||
if (pluginRegistryLoaded) return;
|
||||
if (pluginRegistryLoaded) {
|
||||
return;
|
||||
}
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const logger: PluginLogger = {
|
||||
|
||||
@@ -58,11 +58,15 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string {
|
||||
` source: ${theme.muted(shortenHomeInString(plugin.source))}`,
|
||||
` origin: ${plugin.origin}`,
|
||||
];
|
||||
if (plugin.version) parts.push(` version: ${plugin.version}`);
|
||||
if (plugin.version) {
|
||||
parts.push(` version: ${plugin.version}`);
|
||||
}
|
||||
if (plugin.providerIds.length > 0) {
|
||||
parts.push(` providers: ${plugin.providerIds.join(", ")}`);
|
||||
}
|
||||
if (plugin.error) parts.push(theme.error(` error: ${plugin.error}`));
|
||||
if (plugin.error) {
|
||||
parts.push(theme.error(` error: ${plugin.error}`));
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
@@ -85,7 +89,9 @@ function applySlotSelectionForPlugin(
|
||||
}
|
||||
|
||||
function logSlotWarnings(warnings: string[]) {
|
||||
if (warnings.length === 0) return;
|
||||
if (warnings.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const warning of warnings) {
|
||||
defaultRuntime.log(theme.warn(warning));
|
||||
}
|
||||
@@ -200,12 +206,16 @@ export function registerPluginsCli(program: Command) {
|
||||
if (plugin.name && plugin.name !== plugin.id) {
|
||||
lines.push(theme.muted(`id: ${plugin.id}`));
|
||||
}
|
||||
if (plugin.description) lines.push(plugin.description);
|
||||
if (plugin.description) {
|
||||
lines.push(plugin.description);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(`${theme.muted("Status:")} ${plugin.status}`);
|
||||
lines.push(`${theme.muted("Source:")} ${shortenHomeInString(plugin.source)}`);
|
||||
lines.push(`${theme.muted("Origin:")} ${plugin.origin}`);
|
||||
if (plugin.version) lines.push(`${theme.muted("Version:")} ${plugin.version}`);
|
||||
if (plugin.version) {
|
||||
lines.push(`${theme.muted("Version:")} ${plugin.version}`);
|
||||
}
|
||||
if (plugin.toolNames.length > 0) {
|
||||
lines.push(`${theme.muted("Tools:")} ${plugin.toolNames.join(", ")}`);
|
||||
}
|
||||
@@ -224,18 +234,27 @@ export function registerPluginsCli(program: Command) {
|
||||
if (plugin.services.length > 0) {
|
||||
lines.push(`${theme.muted("Services:")} ${plugin.services.join(", ")}`);
|
||||
}
|
||||
if (plugin.error) lines.push(`${theme.error("Error:")} ${plugin.error}`);
|
||||
if (plugin.error) {
|
||||
lines.push(`${theme.error("Error:")} ${plugin.error}`);
|
||||
}
|
||||
if (install) {
|
||||
lines.push("");
|
||||
lines.push(`${theme.muted("Install:")} ${install.source}`);
|
||||
if (install.spec) lines.push(`${theme.muted("Spec:")} ${install.spec}`);
|
||||
if (install.sourcePath)
|
||||
if (install.spec) {
|
||||
lines.push(`${theme.muted("Spec:")} ${install.spec}`);
|
||||
}
|
||||
if (install.sourcePath) {
|
||||
lines.push(`${theme.muted("Source path:")} ${shortenHomePath(install.sourcePath)}`);
|
||||
if (install.installPath)
|
||||
}
|
||||
if (install.installPath) {
|
||||
lines.push(`${theme.muted("Install path:")} ${shortenHomePath(install.installPath)}`);
|
||||
if (install.version) lines.push(`${theme.muted("Recorded version:")} ${install.version}`);
|
||||
if (install.installedAt)
|
||||
}
|
||||
if (install.version) {
|
||||
lines.push(`${theme.muted("Recorded version:")} ${install.version}`);
|
||||
}
|
||||
if (install.installedAt) {
|
||||
lines.push(`${theme.muted("Installed at:")} ${install.installedAt}`);
|
||||
}
|
||||
}
|
||||
defaultRuntime.log(lines.join("\n"));
|
||||
});
|
||||
@@ -514,7 +533,9 @@ export function registerPluginsCli(program: Command) {
|
||||
}
|
||||
}
|
||||
if (diags.length > 0) {
|
||||
if (lines.length > 0) lines.push("");
|
||||
if (lines.length > 0) {
|
||||
lines.push("");
|
||||
}
|
||||
lines.push(theme.warn("Diagnostics:"));
|
||||
for (const diag of diags) {
|
||||
const target = diag.pluginId ? `${diag.pluginId}: ` : "";
|
||||
|
||||
@@ -19,13 +19,17 @@ export function parseLsofOutput(output: string): PortProcess[] {
|
||||
let current: Partial<PortProcess> = {};
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("p")) {
|
||||
if (current.pid) results.push(current as PortProcess);
|
||||
if (current.pid) {
|
||||
results.push(current as PortProcess);
|
||||
}
|
||||
current = { pid: Number.parseInt(line.slice(1), 10) };
|
||||
} else if (line.startsWith("c")) {
|
||||
current.command = line.slice(1);
|
||||
}
|
||||
}
|
||||
if (current.pid) results.push(current as PortProcess);
|
||||
if (current.pid) {
|
||||
results.push(current as PortProcess);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -42,7 +46,9 @@ export function listPortListeners(port: number): PortProcess[] {
|
||||
if (code === "ENOENT") {
|
||||
throw new Error("lsof not found; required for --force", { cause: err });
|
||||
}
|
||||
if (status === 1) return []; // no listeners
|
||||
if (status === 1) {
|
||||
return [];
|
||||
} // no listeners
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||
|
||||
export function isValidProfileName(value: string): boolean {
|
||||
if (!value) return false;
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
// Keep it path-safe + shell-friendly.
|
||||
return PROFILE_NAME_RE.test(value);
|
||||
}
|
||||
|
||||
export function normalizeProfileName(raw?: string | null): string | null {
|
||||
const profile = raw?.trim();
|
||||
if (!profile) return null;
|
||||
if (profile.toLowerCase() === "default") return null;
|
||||
if (!isValidProfileName(profile)) return null;
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
if (profile.toLowerCase() === "default") {
|
||||
return null;
|
||||
}
|
||||
if (!isValidProfileName(profile)) {
|
||||
return null;
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -12,21 +12,27 @@ describe("parseCliProfileArgs", () => {
|
||||
"--dev",
|
||||
"--allow-unconfigured",
|
||||
]);
|
||||
if (!res.ok) throw new Error(res.error);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBeNull();
|
||||
expect(res.argv).toEqual(["node", "openclaw", "gateway", "--dev", "--allow-unconfigured"]);
|
||||
});
|
||||
|
||||
it("still accepts global --dev before subcommand", () => {
|
||||
const res = parseCliProfileArgs(["node", "openclaw", "--dev", "gateway"]);
|
||||
if (!res.ok) throw new Error(res.error);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBe("dev");
|
||||
expect(res.argv).toEqual(["node", "openclaw", "gateway"]);
|
||||
});
|
||||
|
||||
it("parses --profile value and strips it", () => {
|
||||
const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "status"]);
|
||||
if (!res.ok) throw new Error(res.error);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBe("work");
|
||||
expect(res.argv).toEqual(["node", "openclaw", "status"]);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,9 @@ function takeValue(
|
||||
}
|
||||
|
||||
export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
if (argv.length < 2) return { ok: true, profile: null, argv };
|
||||
if (argv.length < 2) {
|
||||
return { ok: true, profile: null, argv };
|
||||
}
|
||||
|
||||
const out: string[] = argv.slice(0, 2);
|
||||
let profile: string | null = null;
|
||||
@@ -34,7 +36,9 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
const args = argv.slice(2);
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (arg === undefined) continue;
|
||||
if (arg === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sawCommand) {
|
||||
out.push(arg);
|
||||
@@ -56,8 +60,12 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
}
|
||||
const next = args[i + 1];
|
||||
const { value, consumedNext } = takeValue(arg, next);
|
||||
if (consumedNext) i += 1;
|
||||
if (!value) return { ok: false, error: "--profile requires a value" };
|
||||
if (consumedNext) {
|
||||
i += 1;
|
||||
}
|
||||
if (!value) {
|
||||
return { ok: false, error: "--profile requires a value" };
|
||||
}
|
||||
if (!isValidProfileName(value)) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -93,13 +101,17 @@ export function applyCliProfileEnv(params: {
|
||||
const env = params.env ?? (process.env as Record<string, string | undefined>);
|
||||
const homedir = params.homedir ?? os.homedir;
|
||||
const profile = params.profile.trim();
|
||||
if (!profile) return;
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convenience only: fill defaults, never override explicit env values.
|
||||
env.OPENCLAW_PROFILE = profile;
|
||||
|
||||
const stateDir = env.OPENCLAW_STATE_DIR?.trim() || resolveProfileStateDir(profile, homedir);
|
||||
if (!env.OPENCLAW_STATE_DIR?.trim()) env.OPENCLAW_STATE_DIR = stateDir;
|
||||
if (!env.OPENCLAW_STATE_DIR?.trim()) {
|
||||
env.OPENCLAW_STATE_DIR = stateDir;
|
||||
}
|
||||
|
||||
if (!env.OPENCLAW_CONFIG_PATH?.trim()) {
|
||||
env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json");
|
||||
|
||||
@@ -84,8 +84,12 @@ describe("gateway --force helpers", () => {
|
||||
(execFileSync as unknown as vi.Mock).mockImplementation(() => {
|
||||
call += 1;
|
||||
// 1st call: initial listeners to kill; 2nd call: still listed; 3rd call: gone.
|
||||
if (call === 1) return ["p42", "cnode", ""].join("\n");
|
||||
if (call === 2) return ["p42", "cnode", ""].join("\n");
|
||||
if (call === 1) {
|
||||
return ["p42", "cnode", ""].join("\n");
|
||||
}
|
||||
if (call === 2) {
|
||||
return ["p42", "cnode", ""].join("\n");
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
@@ -116,7 +120,9 @@ describe("gateway --force helpers", () => {
|
||||
(execFileSync as unknown as vi.Mock).mockImplementation(() => {
|
||||
call += 1;
|
||||
// 1st call: initial kill list; then keep showing until after SIGKILL.
|
||||
if (call <= 6) return ["p42", "cnode", ""].join("\n");
|
||||
if (call <= 6) {
|
||||
return ["p42", "cnode", ""].join("\n");
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ const routeHealth: RouteSpec = {
|
||||
const json = hasFlag(argv, "--json");
|
||||
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
||||
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
|
||||
if (timeoutMs === null) return false;
|
||||
if (timeoutMs === null) {
|
||||
return false;
|
||||
}
|
||||
await healthCommand({ json, timeoutMs, verbose }, defaultRuntime);
|
||||
return true;
|
||||
},
|
||||
@@ -60,7 +62,9 @@ const routeStatus: RouteSpec = {
|
||||
const usage = hasFlag(argv, "--usage");
|
||||
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
||||
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
|
||||
if (timeoutMs === null) return false;
|
||||
if (timeoutMs === null) {
|
||||
return false;
|
||||
}
|
||||
await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime);
|
||||
return true;
|
||||
},
|
||||
@@ -71,9 +75,13 @@ const routeSessions: RouteSpec = {
|
||||
run: async (argv) => {
|
||||
const json = hasFlag(argv, "--json");
|
||||
const store = getFlagValue(argv, "--store");
|
||||
if (store === null) return false;
|
||||
if (store === null) {
|
||||
return false;
|
||||
}
|
||||
const active = getFlagValue(argv, "--active");
|
||||
if (active === null) return false;
|
||||
if (active === null) {
|
||||
return false;
|
||||
}
|
||||
await sessionsCommand({ json, store, active }, defaultRuntime);
|
||||
return true;
|
||||
},
|
||||
@@ -93,7 +101,9 @@ const routeMemoryStatus: RouteSpec = {
|
||||
match: (path) => path[0] === "memory" && path[1] === "status",
|
||||
run: async (argv) => {
|
||||
const agent = getFlagValue(argv, "--agent");
|
||||
if (agent === null) return false;
|
||||
if (agent === null) {
|
||||
return false;
|
||||
}
|
||||
const json = hasFlag(argv, "--json");
|
||||
const deep = hasFlag(argv, "--deep");
|
||||
const index = hasFlag(argv, "--index");
|
||||
@@ -166,9 +176,13 @@ export function registerProgramCommands(
|
||||
|
||||
export function findRoutedCommand(path: string[]): RouteSpec | null {
|
||||
for (const entry of commandRegistry) {
|
||||
if (!entry.routes) continue;
|
||||
if (!entry.routes) {
|
||||
continue;
|
||||
}
|
||||
for (const route of entry.routes) {
|
||||
if (route.match(path)) return route;
|
||||
if (route.match(path)) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -52,7 +52,9 @@ export async function ensureConfigReady(params: {
|
||||
: [];
|
||||
|
||||
const invalid = snapshot.exists && !snapshot.valid;
|
||||
if (!invalid) return;
|
||||
if (!invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rich = isRich();
|
||||
const muted = (value: string) => colorize(rich, theme.muted, value);
|
||||
|
||||
@@ -73,7 +73,9 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
}
|
||||
|
||||
program.addHelpText("beforeAll", () => {
|
||||
if (hasEmittedCliBanner()) return "";
|
||||
if (hasEmittedCliBanner()) {
|
||||
return "";
|
||||
}
|
||||
const rich = isRich();
|
||||
const line = formatCliBannerLine(ctx.programVersion, { richTty: rich });
|
||||
return `\n${line}\n`;
|
||||
@@ -84,7 +86,9 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
).join("\n");
|
||||
|
||||
program.addHelpText("afterAll", ({ command }) => {
|
||||
if (command !== program) return "";
|
||||
if (command !== program) {
|
||||
return "";
|
||||
}
|
||||
const docs = formatDocsLink("/cli", "docs.openclaw.ai/cli");
|
||||
return `\n${theme.heading("Examples:")}\n${fmtExamples}\n\n${theme.muted("Docs:")} ${docs}\n`;
|
||||
});
|
||||
|
||||
@@ -3,22 +3,30 @@ export function collectOption(value: string, previous: string[] = []): string[]
|
||||
}
|
||||
|
||||
export function parsePositiveIntOrUndefined(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) return undefined;
|
||||
if (!Number.isFinite(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Math.trunc(value);
|
||||
return parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) return undefined;
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveActionArgs(actionCommand?: import("commander").Command): string[] {
|
||||
if (!actionCommand) return [];
|
||||
if (!actionCommand) {
|
||||
return [];
|
||||
}
|
||||
const args = (actionCommand as import("commander").Command & { args?: string[] }).args;
|
||||
return Array.isArray(args) ? args : [];
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ function setProcessTitleForCommand(actionCommand: Command) {
|
||||
}
|
||||
const name = current.name();
|
||||
const cliName = resolveCliName();
|
||||
if (!name || name === cliName) return;
|
||||
if (!name || name === cliName) {
|
||||
return;
|
||||
}
|
||||
process.title = `${cliName}-${name}`;
|
||||
}
|
||||
|
||||
@@ -26,7 +28,9 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
||||
setProcessTitleForCommand(actionCommand);
|
||||
const argv = process.argv;
|
||||
if (hasHelpOrVersion(argv)) return;
|
||||
if (hasHelpOrVersion(argv)) {
|
||||
return;
|
||||
}
|
||||
const commandPath = getCommandPath(argv, 2);
|
||||
const hideBanner =
|
||||
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
|
||||
@@ -41,7 +45,9 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
if (!verbose) {
|
||||
process.env.NODE_NO_WARNINGS ??= "1";
|
||||
}
|
||||
if (commandPath[0] === "doctor" || commandPath[0] === "completion") return;
|
||||
if (commandPath[0] === "doctor" || commandPath[0] === "completion") {
|
||||
return;
|
||||
}
|
||||
await ensureConfigReady({ runtime: defaultRuntime, commandPath });
|
||||
// Load plugins for commands that need channel access
|
||||
if (PLUGIN_REQUIRED_COMMANDS.has(commandPath[0])) {
|
||||
|
||||
@@ -17,14 +17,20 @@ function resolveInstallDaemonFlag(
|
||||
command: unknown,
|
||||
opts: { installDaemon?: boolean },
|
||||
): boolean | undefined {
|
||||
if (!command || typeof command !== "object") return undefined;
|
||||
if (!command || typeof command !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const getOptionValueSource =
|
||||
"getOptionValueSource" in command ? command.getOptionValueSource : undefined;
|
||||
if (typeof getOptionValueSource !== "function") return undefined;
|
||||
if (typeof getOptionValueSource !== "function") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Commander doesn't support option conflicts natively; keep original behavior.
|
||||
// If --skip-daemon is explicitly passed, it wins.
|
||||
if (getOptionValueSource.call(command, "skipDaemon") === "cli") return false;
|
||||
if (getOptionValueSource.call(command, "skipDaemon") === "cli") {
|
||||
return false;
|
||||
}
|
||||
if (getOptionValueSource.call(command, "installDaemon") === "cli") {
|
||||
return Boolean(opts.installDaemon);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,12 @@ type SubCliEntry = {
|
||||
};
|
||||
|
||||
const shouldRegisterPrimaryOnly = (argv: string[]) => {
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS)) return false;
|
||||
if (hasHelpOrVersion(argv)) return false;
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS)) {
|
||||
return false;
|
||||
}
|
||||
if (hasHelpOrVersion(argv)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -251,9 +255,13 @@ function removeCommand(program: Command, command: Command) {
|
||||
|
||||
export async function registerSubCliByName(program: Command, name: string): Promise<boolean> {
|
||||
const entry = entries.find((candidate) => candidate.name === name);
|
||||
if (!entry) return false;
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const existing = program.commands.find((cmd) => cmd.name() === entry.name);
|
||||
if (existing) removeCommand(program, existing);
|
||||
if (existing) {
|
||||
removeCommand(program, existing);
|
||||
}
|
||||
await entry.register(program);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,13 +41,19 @@ const noopReporter: ProgressReporter = {
|
||||
};
|
||||
|
||||
export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
if (options.enabled === false) return noopReporter;
|
||||
if (activeProgress > 0) return noopReporter;
|
||||
if (options.enabled === false) {
|
||||
return noopReporter;
|
||||
}
|
||||
if (activeProgress > 0) {
|
||||
return noopReporter;
|
||||
}
|
||||
|
||||
const stream = options.stream ?? process.stderr;
|
||||
const isTty = stream.isTTY;
|
||||
const allowLog = !isTty && options.fallback === "log";
|
||||
if (!isTty && !allowLog) return noopReporter;
|
||||
if (!isTty && !allowLog) {
|
||||
return noopReporter;
|
||||
}
|
||||
|
||||
const delayMs = typeof options.delayMs === "number" ? options.delayMs : DEFAULT_DELAY_MS;
|
||||
const canOsc = isTty && supportsOscProgress(process.env, isTty);
|
||||
@@ -78,7 +84,9 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
const spin = allowSpinner ? spinner() : null;
|
||||
const renderLine = allowLine
|
||||
? () => {
|
||||
if (!started) return;
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
const suffix = indeterminate ? "" : ` ${percent}%`;
|
||||
clearActiveProgressLine();
|
||||
stream.write(`${theme.accent(label)}${suffix}`);
|
||||
@@ -90,11 +98,15 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
let lastAt = 0;
|
||||
const throttleMs = 250;
|
||||
return () => {
|
||||
if (!started) return;
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
const suffix = indeterminate ? "" : ` ${percent}%`;
|
||||
const nextLine = `${label}${suffix}`;
|
||||
const now = Date.now();
|
||||
if (nextLine === lastLine && now - lastAt < throttleMs) return;
|
||||
if (nextLine === lastLine && now - lastAt < throttleMs) {
|
||||
return;
|
||||
}
|
||||
lastLine = nextLine;
|
||||
lastAt = now;
|
||||
stream.write(`${nextLine}\n`);
|
||||
@@ -104,10 +116,15 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const applyState = () => {
|
||||
if (!started) return;
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
if (controller) {
|
||||
if (indeterminate) controller.setIndeterminate(label);
|
||||
else controller.setPercent(label, percent);
|
||||
if (indeterminate) {
|
||||
controller.setIndeterminate(label);
|
||||
} else {
|
||||
controller.setPercent(label, percent);
|
||||
}
|
||||
}
|
||||
if (spin) {
|
||||
spin.message(theme.accent(label));
|
||||
@@ -121,7 +138,9 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (started) return;
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
if (spin) {
|
||||
spin.start(theme.accent(label));
|
||||
@@ -147,7 +166,9 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
};
|
||||
|
||||
const tick = (delta = 1) => {
|
||||
if (!total) return;
|
||||
if (!total) {
|
||||
return;
|
||||
}
|
||||
completed = Math.min(total, completed + delta);
|
||||
const nextPercent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
setPercent(nextPercent);
|
||||
@@ -162,8 +183,12 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
activeProgress = Math.max(0, activeProgress - 1);
|
||||
return;
|
||||
}
|
||||
if (controller) controller.clear();
|
||||
if (spin) spin.stop();
|
||||
if (controller) {
|
||||
controller.clear();
|
||||
}
|
||||
if (spin) {
|
||||
spin.stop();
|
||||
}
|
||||
clearActiveProgressLine();
|
||||
if (isTty) {
|
||||
unregisterActiveProgressLine(stream);
|
||||
@@ -192,8 +217,12 @@ export async function withProgressTotals<T>(
|
||||
): Promise<T> {
|
||||
return await withProgress(options, async (progress) => {
|
||||
const update = ({ completed, total, label }: ProgressTotalsUpdate) => {
|
||||
if (label) progress.setLabel(label);
|
||||
if (!Number.isFinite(total) || total <= 0) return;
|
||||
if (label) {
|
||||
progress.setLabel(label);
|
||||
}
|
||||
if (!Number.isFinite(total) || total <= 0) {
|
||||
return;
|
||||
}
|
||||
progress.setPercent((completed / total) * 100);
|
||||
};
|
||||
return await work(update, progress);
|
||||
|
||||
@@ -5,12 +5,18 @@ import { isVerbose, isYes } from "../globals.js";
|
||||
|
||||
export async function promptYesNo(question: string, defaultYes = false): Promise<boolean> {
|
||||
// Simple Y/N prompt honoring global --yes and verbosity flags.
|
||||
if (isVerbose() && isYes()) return true; // redundant guard when both flags set
|
||||
if (isYes()) return true;
|
||||
if (isVerbose() && isYes()) {
|
||||
return true;
|
||||
} // redundant guard when both flags set
|
||||
if (isYes()) {
|
||||
return true;
|
||||
}
|
||||
const rl = readline.createInterface({ input, output });
|
||||
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
||||
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
||||
rl.close();
|
||||
if (!answer) return defaultYes;
|
||||
if (!answer) {
|
||||
return defaultYes;
|
||||
}
|
||||
return answer.startsWith("y");
|
||||
}
|
||||
|
||||
@@ -20,13 +20,21 @@ async function prepareRoutedCommand(params: {
|
||||
}
|
||||
|
||||
export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_ROUTE_FIRST)) return false;
|
||||
if (hasHelpOrVersion(argv)) return false;
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_ROUTE_FIRST)) {
|
||||
return false;
|
||||
}
|
||||
if (hasHelpOrVersion(argv)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = getCommandPath(argv, 2);
|
||||
if (!path[0]) return false;
|
||||
if (!path[0]) {
|
||||
return false;
|
||||
}
|
||||
const route = findRoutedCommand(path);
|
||||
if (!route) return false;
|
||||
if (!route) {
|
||||
return false;
|
||||
}
|
||||
await prepareRoutedCommand({ argv, commandPath: path, loadPlugins: route.loadPlugins });
|
||||
return route.run(argv);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ import { tryRouteCli } from "./route.js";
|
||||
|
||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||
const index = argv.indexOf("--update");
|
||||
if (index === -1) return argv;
|
||||
if (index === -1) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
const next = [...argv];
|
||||
next.splice(index, 1, "update");
|
||||
@@ -32,7 +34,9 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
// Enforce the minimum supported runtime before doing any work.
|
||||
assertSupportedRuntime();
|
||||
|
||||
if (await tryRouteCli(normalizedArgv)) return;
|
||||
if (await tryRouteCli(normalizedArgv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
||||
enableConsoleCapture();
|
||||
@@ -69,7 +73,9 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
}
|
||||
|
||||
function stripWindowsNodeExec(argv: string[]): string[] {
|
||||
if (process.platform !== "win32") return argv;
|
||||
if (process.platform !== "win32") {
|
||||
return argv;
|
||||
}
|
||||
const stripControlChars = (value: string): string => {
|
||||
let out = "";
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
@@ -90,9 +96,13 @@ function stripWindowsNodeExec(argv: string[]): string[] {
|
||||
const execPathLower = execPath.toLowerCase();
|
||||
const execBase = path.basename(execPath).toLowerCase();
|
||||
const isExecPath = (value: string | undefined): boolean => {
|
||||
if (!value) return false;
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeCandidate(value);
|
||||
if (!normalized) return false;
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const lower = normalized.toLowerCase();
|
||||
return (
|
||||
lower === execPathLower ||
|
||||
@@ -104,7 +114,9 @@ function stripWindowsNodeExec(argv: string[]): string[] {
|
||||
);
|
||||
};
|
||||
const filtered = argv.filter((arg, index) => index === 0 || !isExecPath(arg));
|
||||
if (filtered.length < 3) return filtered;
|
||||
if (filtered.length < 3) {
|
||||
return filtered;
|
||||
}
|
||||
const cleaned = [...filtered];
|
||||
if (isExecPath(cleaned[1])) {
|
||||
cleaned.splice(1, 1);
|
||||
|
||||
@@ -89,21 +89,27 @@ export function registerSecurityCli(program: Command) {
|
||||
for (const action of fixResult.actions) {
|
||||
if (action.kind === "chmod") {
|
||||
const mode = action.mode.toString(8).padStart(3, "0");
|
||||
if (action.ok) lines.push(muted(` chmod ${mode} ${shortenHomePath(action.path)}`));
|
||||
else if (action.skipped)
|
||||
if (action.ok) {
|
||||
lines.push(muted(` chmod ${mode} ${shortenHomePath(action.path)}`));
|
||||
} else if (action.skipped) {
|
||||
lines.push(
|
||||
muted(` skip chmod ${mode} ${shortenHomePath(action.path)} (${action.skipped})`),
|
||||
);
|
||||
else if (action.error)
|
||||
} else if (action.error) {
|
||||
lines.push(
|
||||
muted(` chmod ${mode} ${shortenHomePath(action.path)} failed: ${action.error}`),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const command = shortenHomeInString(action.command);
|
||||
if (action.ok) lines.push(muted(` ${command}`));
|
||||
else if (action.skipped) lines.push(muted(` skip ${command} (${action.skipped})`));
|
||||
else if (action.error) lines.push(muted(` ${command} failed: ${action.error}`));
|
||||
if (action.ok) {
|
||||
lines.push(muted(` ${command}`));
|
||||
} else if (action.skipped) {
|
||||
lines.push(muted(` skip ${command} (${action.skipped})`));
|
||||
} else if (action.error) {
|
||||
lines.push(muted(` ${command} failed: ${action.error}`));
|
||||
}
|
||||
}
|
||||
if (fixResult.errors.length > 0) {
|
||||
for (const err of fixResult.errors) {
|
||||
@@ -118,7 +124,9 @@ export function registerSecurityCli(program: Command) {
|
||||
|
||||
const render = (sev: "critical" | "warn" | "info") => {
|
||||
const list = bySeverity(sev);
|
||||
if (list.length === 0) return;
|
||||
if (list.length === 0) {
|
||||
return;
|
||||
}
|
||||
const label =
|
||||
sev === "critical"
|
||||
? rich
|
||||
@@ -136,7 +144,9 @@ export function registerSecurityCli(program: Command) {
|
||||
for (const f of list) {
|
||||
lines.push(`${theme.muted(f.checkId)} ${f.title}`);
|
||||
lines.push(` ${f.detail}`);
|
||||
if (f.remediation?.trim()) lines.push(` ${muted(`Fix: ${f.remediation.trim()}`)}`);
|
||||
if (f.remediation?.trim()) {
|
||||
lines.push(` ${muted(`Fix: ${f.remediation.trim()}`)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -218,7 +218,9 @@ describe("skills-cli", () => {
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(moduleDir, "..", "..");
|
||||
const candidate = path.join(root, "skills");
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -251,7 +253,9 @@ describe("skills-cli", () => {
|
||||
|
||||
it("formats info for a real bundled skill (peekaboo)", () => {
|
||||
const bundledDir = resolveBundledSkillsDir();
|
||||
if (!bundledDir) return;
|
||||
if (!bundledDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const report = buildWorkspaceSkillStatus("/tmp", {
|
||||
managedSkillsDir: "/nonexistent",
|
||||
|
||||
@@ -28,14 +28,22 @@ export type SkillsCheckOptions = {
|
||||
};
|
||||
|
||||
function appendClawHubHint(output: string, json?: boolean): string {
|
||||
if (json) return output;
|
||||
if (json) {
|
||||
return output;
|
||||
}
|
||||
return `${output}\n\nTip: use \`npx clawhub\` to search, install, and sync skills.`;
|
||||
}
|
||||
|
||||
function formatSkillStatus(skill: SkillStatusEntry): string {
|
||||
if (skill.eligible) return theme.success("✓ ready");
|
||||
if (skill.disabled) return theme.warn("⏸ disabled");
|
||||
if (skill.blockedByAllowlist) return theme.warn("🚫 blocked");
|
||||
if (skill.eligible) {
|
||||
return theme.success("✓ ready");
|
||||
}
|
||||
if (skill.disabled) {
|
||||
return theme.warn("⏸ disabled");
|
||||
}
|
||||
if (skill.blockedByAllowlist) {
|
||||
return theme.warn("🚫 blocked");
|
||||
}
|
||||
return theme.error("✗ missing");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,12 @@ type SystemEventOpts = GatewayRpcOpts & { text?: string; mode?: string; json?: b
|
||||
|
||||
const normalizeWakeMode = (raw: unknown) => {
|
||||
const mode = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!mode) return "next-heartbeat" as const;
|
||||
if (mode === "now" || mode === "next-heartbeat") return mode;
|
||||
if (!mode) {
|
||||
return "next-heartbeat" as const;
|
||||
}
|
||||
if (mode === "now" || mode === "next-heartbeat") {
|
||||
return mode;
|
||||
}
|
||||
throw new Error("--mode must be now or next-heartbeat");
|
||||
};
|
||||
|
||||
@@ -36,11 +40,16 @@ export function registerSystemCli(program: Command) {
|
||||
).action(async (opts: SystemEventOpts) => {
|
||||
try {
|
||||
const text = typeof opts.text === "string" ? opts.text.trim() : "";
|
||||
if (!text) throw new Error("--text is required");
|
||||
if (!text) {
|
||||
throw new Error("--text is required");
|
||||
}
|
||||
const mode = normalizeWakeMode(opts.mode);
|
||||
const result = await callGatewayFromCli("wake", opts, { mode, text }, { expectFinal: false });
|
||||
if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
else defaultRuntime.log("ok");
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
defaultRuntime.log("ok");
|
||||
}
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -127,7 +127,9 @@ const onSpecificDates =
|
||||
(date) => {
|
||||
const parts = utcParts(date);
|
||||
return dates.some(([year, month, day]) => {
|
||||
if (parts.year !== year) return false;
|
||||
if (parts.year !== year) {
|
||||
return false;
|
||||
}
|
||||
const start = Date.UTC(year, month, day);
|
||||
const current = Date.UTC(parts.year, parts.month, parts.day);
|
||||
return current >= start && current < start + durationDays * DAY_MS;
|
||||
@@ -146,7 +148,9 @@ const inYearWindow =
|
||||
(date) => {
|
||||
const parts = utcParts(date);
|
||||
const window = windows.find((entry) => entry.year === parts.year);
|
||||
if (!window) return false;
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
const start = Date.UTC(window.year, window.month, window.day);
|
||||
const current = Date.UTC(parts.year, parts.month, parts.day);
|
||||
return current >= start && current < start + window.duration * DAY_MS;
|
||||
@@ -154,7 +158,9 @@ const inYearWindow =
|
||||
|
||||
const isFourthThursdayOfNovember: HolidayRule = (date) => {
|
||||
const parts = utcParts(date);
|
||||
if (parts.month !== 10) return false; // November
|
||||
if (parts.month !== 10) {
|
||||
return false;
|
||||
} // November
|
||||
const firstDay = new Date(Date.UTC(parts.year, 10, 1)).getUTCDay();
|
||||
const offsetToThursday = (4 - firstDay + 7) % 7; // 4 = Thursday
|
||||
const fourthThursday = 1 + offsetToThursday + 21; // 1st + offset + 3 weeks
|
||||
@@ -224,7 +230,9 @@ const HOLIDAY_RULES = new Map<string, HolidayRule>([
|
||||
|
||||
function isTaglineActive(tagline: string, date: Date): boolean {
|
||||
const rule = HOLIDAY_RULES.get(tagline);
|
||||
if (!rule) return true;
|
||||
if (!rule) {
|
||||
return true;
|
||||
}
|
||||
return rule(date);
|
||||
}
|
||||
|
||||
@@ -235,7 +243,9 @@ export interface TaglineOptions {
|
||||
}
|
||||
|
||||
export function activeTaglines(options: TaglineOptions = {}): string[] {
|
||||
if (TAGLINES.length === 0) return [DEFAULT_TAGLINE];
|
||||
if (TAGLINES.length === 0) {
|
||||
return [DEFAULT_TAGLINE];
|
||||
}
|
||||
const today = options.now ? options.now() : new Date();
|
||||
const filtered = TAGLINES.filter((tagline) => isTaglineActive(tagline, today));
|
||||
return filtered.length > 0 ? filtered : TAGLINES;
|
||||
|
||||
@@ -118,10 +118,16 @@ const OPENCLAW_REPO_URL = "https://github.com/openclaw/openclaw.git";
|
||||
const DEFAULT_GIT_DIR = path.join(os.homedir(), ".openclaw");
|
||||
|
||||
function normalizeTag(value?: string | null): string | null {
|
||||
if (!value) return null;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.startsWith("openclaw@")) return trimmed.slice("openclaw@".length);
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.startsWith("openclaw@")) {
|
||||
return trimmed.slice("openclaw@".length);
|
||||
}
|
||||
if (trimmed.startsWith(`${DEFAULT_PACKAGE_NAME}@`)) {
|
||||
return trimmed.slice(`${DEFAULT_PACKAGE_NAME}@`.length);
|
||||
}
|
||||
@@ -134,7 +140,9 @@ function pickUpdateQuip(): string {
|
||||
|
||||
function normalizeVersionTag(tag: string): string | null {
|
||||
const trimmed = tag.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const cleaned = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
||||
return parseSemver(cleaned) ? cleaned : null;
|
||||
}
|
||||
@@ -151,7 +159,9 @@ async function readPackageVersion(root: string): Promise<string | null> {
|
||||
|
||||
async function resolveTargetVersion(tag: string, timeoutMs?: number): Promise<string | null> {
|
||||
const direct = normalizeVersionTag(tag);
|
||||
if (direct) return direct;
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const res = await fetchNpmTagVersion({ tag, timeoutMs });
|
||||
return res.version ?? null;
|
||||
}
|
||||
@@ -201,7 +211,9 @@ async function isEmptyDir(targetPath: string): Promise<boolean> {
|
||||
|
||||
function resolveGitInstallDir(): string {
|
||||
const override = process.env.OPENCLAW_GIT_DIR?.trim();
|
||||
if (override) return path.resolve(override);
|
||||
if (override) {
|
||||
return path.resolve(override);
|
||||
}
|
||||
return resolveDefaultGitDir();
|
||||
}
|
||||
|
||||
@@ -211,7 +223,9 @@ function resolveDefaultGitDir(): string {
|
||||
|
||||
function resolveNodeRunner(): string {
|
||||
const base = path.basename(process.execPath).toLowerCase();
|
||||
if (base === "node" || base === "node.exe") return process.execPath;
|
||||
if (base === "node" || base === "node.exe") {
|
||||
return process.execPath;
|
||||
}
|
||||
return "node";
|
||||
}
|
||||
|
||||
@@ -309,7 +323,9 @@ async function resolveGlobalManager(params: {
|
||||
params.root,
|
||||
params.timeoutMs,
|
||||
);
|
||||
if (detected) return detected;
|
||||
if (detected) {
|
||||
return detected;
|
||||
}
|
||||
}
|
||||
const byPresence = await detectGlobalInstallManagerByPresence(runCommand, params.timeoutMs);
|
||||
return byPresence ?? "npm";
|
||||
@@ -459,7 +475,9 @@ function createUpdateProgress(enabled: boolean): ProgressController {
|
||||
currentSpinner.start(theme.accent(getStepLabel(step)));
|
||||
},
|
||||
onStepComplete: (step) => {
|
||||
if (!currentSpinner) return;
|
||||
if (!currentSpinner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = getStepLabel(step);
|
||||
const duration = theme.muted(`(${formatDuration(step.durationMs)})`);
|
||||
@@ -491,14 +509,20 @@ function createUpdateProgress(enabled: boolean): ProgressController {
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
const seconds = (ms / 1000).toFixed(1);
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
function formatStepStatus(exitCode: number | null): string {
|
||||
if (exitCode === 0) return theme.success("\u2713");
|
||||
if (exitCode === null) return theme.warn("?");
|
||||
if (exitCode === 0) {
|
||||
return theme.success("\u2713");
|
||||
}
|
||||
if (exitCode === null) {
|
||||
return theme.warn("?");
|
||||
}
|
||||
return theme.error("\u2717");
|
||||
}
|
||||
|
||||
@@ -875,7 +899,9 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
|
||||
if (!opts.json) {
|
||||
const summarizeList = (list: string[]) => {
|
||||
if (list.length <= 6) return list.join(", ");
|
||||
if (list.length <= 6) {
|
||||
return list.join(", ");
|
||||
}
|
||||
return `${list.slice(0, 6).join(", ")} +${list.length - 6} more`;
|
||||
};
|
||||
|
||||
@@ -907,13 +933,19 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
defaultRuntime.log(theme.muted("No plugin updates needed."));
|
||||
} else {
|
||||
const parts = [`${updated} updated`, `${unchanged} unchanged`];
|
||||
if (failed > 0) parts.push(`${failed} failed`);
|
||||
if (skipped > 0) parts.push(`${skipped} skipped`);
|
||||
if (failed > 0) {
|
||||
parts.push(`${failed} failed`);
|
||||
}
|
||||
if (skipped > 0) {
|
||||
parts.push(`${skipped} skipped`);
|
||||
}
|
||||
defaultRuntime.log(theme.muted(`npm plugins: ${parts.join(", ")}.`));
|
||||
}
|
||||
|
||||
for (const outcome of npmResult.outcomes) {
|
||||
if (outcome.status !== "error") continue;
|
||||
if (outcome.status !== "error") {
|
||||
continue;
|
||||
}
|
||||
defaultRuntime.log(theme.error(outcome.message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,9 @@ export function registerWebhooksCli(program: Command) {
|
||||
function parseGmailSetupOptions(raw: Record<string, unknown>): GmailSetupOptions {
|
||||
const accountRaw = raw.account;
|
||||
const account = typeof accountRaw === "string" ? accountRaw.trim() : "";
|
||||
if (!account) throw new Error("--account is required");
|
||||
if (!account) {
|
||||
throw new Error("--account is required");
|
||||
}
|
||||
return {
|
||||
account,
|
||||
project: stringOption(raw.project),
|
||||
@@ -154,19 +156,27 @@ function parseGmailRunOptions(raw: Record<string, unknown>): GmailRunOptions {
|
||||
}
|
||||
|
||||
function stringOption(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function numberOption(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
const n = typeof value === "number" ? value : Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) return undefined;
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(n);
|
||||
}
|
||||
|
||||
function booleanOption(value: unknown): boolean | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user