mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 03:20:49 +00:00
refactor: dedupe cli config cron and install flows
This commit is contained in:
@@ -2,7 +2,12 @@ import type { Command } from "commander";
|
||||
import { danger } from "../../globals.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import type { BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
import { callBrowserAct, requireRef, resolveBrowserActionContext } from "./shared.js";
|
||||
import {
|
||||
callBrowserAct,
|
||||
logBrowserActionResult,
|
||||
requireRef,
|
||||
resolveBrowserActionContext,
|
||||
} from "./shared.js";
|
||||
|
||||
export function registerBrowserElementCommands(
|
||||
browser: Command,
|
||||
@@ -41,12 +46,8 @@ export function registerBrowserElementCommands(
|
||||
modifiers,
|
||||
},
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
const suffix = result.url ? ` on ${result.url}` : "";
|
||||
defaultRuntime.log(`clicked ref ${refValue}${suffix}`);
|
||||
logBrowserActionResult(parent, result, `clicked ref ${refValue}${suffix}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -80,11 +81,7 @@ export function registerBrowserElementCommands(
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`typed into ref ${refValue}`);
|
||||
logBrowserActionResult(parent, result, `typed into ref ${refValue}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -104,11 +101,7 @@ export function registerBrowserElementCommands(
|
||||
profile,
|
||||
body: { kind: "press", key, targetId: opts.targetId?.trim() || undefined },
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`pressed ${key}`);
|
||||
logBrowserActionResult(parent, result, `pressed ${key}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -128,11 +121,7 @@ export function registerBrowserElementCommands(
|
||||
profile,
|
||||
body: { kind: "hover", ref, targetId: opts.targetId?.trim() || undefined },
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`hovered ref ${ref}`);
|
||||
logBrowserActionResult(parent, result, `hovered ref ${ref}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -165,11 +154,7 @@ export function registerBrowserElementCommands(
|
||||
},
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`scrolled into view: ${refValue}`);
|
||||
logBrowserActionResult(parent, result, `scrolled into view: ${refValue}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -195,11 +180,7 @@ export function registerBrowserElementCommands(
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`dragged ${startRef} → ${endRef}`);
|
||||
logBrowserActionResult(parent, result, `dragged ${startRef} → ${endRef}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -225,11 +206,7 @@ export function registerBrowserElementCommands(
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`selected ${values.join(", ")}`);
|
||||
logBrowserActionResult(parent, result, `selected ${values.join(", ")}`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -2,7 +2,12 @@ import type { Command } from "commander";
|
||||
import { danger } from "../../globals.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import type { BrowserParentOpts } from "../browser-cli-shared.js";
|
||||
import { callBrowserAct, readFields, resolveBrowserActionContext } from "./shared.js";
|
||||
import {
|
||||
callBrowserAct,
|
||||
logBrowserActionResult,
|
||||
readFields,
|
||||
resolveBrowserActionContext,
|
||||
} from "./shared.js";
|
||||
|
||||
export function registerBrowserFormWaitEvalCommands(
|
||||
browser: Command,
|
||||
@@ -30,11 +35,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(`filled ${fields.length} field(s)`);
|
||||
logBrowserActionResult(parent, result, `filled ${fields.length} field(s)`);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -83,11 +84,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
},
|
||||
timeoutMs,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log("wait complete");
|
||||
logBrowserActionResult(parent, result, "wait complete");
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -40,6 +40,18 @@ export async function callBrowserAct<T = unknown>(params: {
|
||||
);
|
||||
}
|
||||
|
||||
export function logBrowserActionResult(
|
||||
parent: BrowserParentOpts,
|
||||
result: unknown,
|
||||
successMessage: string,
|
||||
) {
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(successMessage);
|
||||
}
|
||||
|
||||
export function requireRef(ref: string | undefined) {
|
||||
const refValue = typeof ref === "string" ? ref.trim() : "";
|
||||
if (!refValue) {
|
||||
|
||||
@@ -35,13 +35,7 @@ vi.mock("./cli-utils.js", () => ({
|
||||
_runtime: unknown,
|
||||
action: () => Promise<void>,
|
||||
onError: (err: unknown) => void,
|
||||
) => {
|
||||
try {
|
||||
await action();
|
||||
} catch (err) {
|
||||
onError(err);
|
||||
}
|
||||
},
|
||||
) => await action().catch(onError),
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
parseAt,
|
||||
parseCronStaggerMs,
|
||||
parseDurationMs,
|
||||
printCronList,
|
||||
warnIfCronSchedulerDisabled,
|
||||
@@ -129,19 +130,7 @@ export function registerCronAddCommand(cron: Command) {
|
||||
}
|
||||
return { kind: "every" as const, everyMs };
|
||||
}
|
||||
const staggerMs = (() => {
|
||||
if (useExact) {
|
||||
return 0;
|
||||
}
|
||||
if (!staggerRaw) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseDurationMs(staggerRaw);
|
||||
if (!parsed) {
|
||||
throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
|
||||
}
|
||||
return parsed;
|
||||
})();
|
||||
const staggerMs = parseCronStaggerMs({ staggerRaw, useExact });
|
||||
return {
|
||||
kind: "cron" as const,
|
||||
expr: cronExpr,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
parseAt,
|
||||
parseCronStaggerMs,
|
||||
parseDurationMs,
|
||||
warnIfCronSchedulerDisabled,
|
||||
} from "./shared.js";
|
||||
@@ -98,19 +99,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
if (staggerRaw && useExact) {
|
||||
throw new Error("Choose either --stagger or --exact, not both");
|
||||
}
|
||||
const requestedStaggerMs = (() => {
|
||||
if (useExact) {
|
||||
return 0;
|
||||
}
|
||||
if (!staggerRaw) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseDurationMs(staggerRaw);
|
||||
if (!parsed) {
|
||||
throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
|
||||
}
|
||||
return parsed;
|
||||
})();
|
||||
const requestedStaggerMs = parseCronStaggerMs({ staggerRaw, useExact });
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (typeof opts.name === "string") {
|
||||
|
||||
@@ -62,6 +62,23 @@ export function parseDurationMs(input: string): number | null {
|
||||
return Math.floor(n * factor);
|
||||
}
|
||||
|
||||
export function parseCronStaggerMs(params: {
|
||||
staggerRaw: string;
|
||||
useExact: boolean;
|
||||
}): number | undefined {
|
||||
if (params.useExact) {
|
||||
return 0;
|
||||
}
|
||||
if (!params.staggerRaw) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseDurationMs(params.staggerRaw);
|
||||
if (!parsed) {
|
||||
throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseAt(input: string): string | null {
|
||||
const raw = input.trim();
|
||||
if (!raw) {
|
||||
|
||||
@@ -15,6 +15,32 @@ vi.mock("../../infra/ports.js", () => ({
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
async function inspectUnknownListenerFallback(params: {
|
||||
runtime: { status: "running"; pid: number } | { status: "stopped" };
|
||||
includeUnknownListenersAsStale: boolean;
|
||||
}) {
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
classifyPortListener.mockReturnValue("unknown");
|
||||
|
||||
const service = {
|
||||
readRuntime: vi.fn(async () => params.runtime),
|
||||
} as unknown as GatewayService;
|
||||
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [{ pid: 10920, command: "unknown" }],
|
||||
hints: [],
|
||||
});
|
||||
|
||||
const { inspectGatewayRestart } = await import("./restart-health.js");
|
||||
return inspectGatewayRestart({
|
||||
service,
|
||||
port: 18789,
|
||||
includeUnknownListenersAsStale: params.includeUnknownListenersAsStale,
|
||||
});
|
||||
}
|
||||
|
||||
describe("inspectGatewayRestart", () => {
|
||||
beforeEach(() => {
|
||||
inspectPortUsage.mockReset();
|
||||
@@ -71,24 +97,8 @@ describe("inspectGatewayRestart", () => {
|
||||
});
|
||||
|
||||
it("treats unknown listeners as stale on Windows when enabled", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
classifyPortListener.mockReturnValue("unknown");
|
||||
|
||||
const service = {
|
||||
readRuntime: vi.fn(async () => ({ status: "stopped" })),
|
||||
} as unknown as GatewayService;
|
||||
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [{ pid: 10920, command: "unknown" }],
|
||||
hints: [],
|
||||
});
|
||||
|
||||
const { inspectGatewayRestart } = await import("./restart-health.js");
|
||||
const snapshot = await inspectGatewayRestart({
|
||||
service,
|
||||
port: 18789,
|
||||
const snapshot = await inspectUnknownListenerFallback({
|
||||
runtime: { status: "stopped" },
|
||||
includeUnknownListenersAsStale: true,
|
||||
});
|
||||
|
||||
@@ -96,24 +106,8 @@ describe("inspectGatewayRestart", () => {
|
||||
});
|
||||
|
||||
it("does not treat unknown listeners as stale when fallback is disabled", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
classifyPortListener.mockReturnValue("unknown");
|
||||
|
||||
const service = {
|
||||
readRuntime: vi.fn(async () => ({ status: "stopped" })),
|
||||
} as unknown as GatewayService;
|
||||
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [{ pid: 10920, command: "unknown" }],
|
||||
hints: [],
|
||||
});
|
||||
|
||||
const { inspectGatewayRestart } = await import("./restart-health.js");
|
||||
const snapshot = await inspectGatewayRestart({
|
||||
service,
|
||||
port: 18789,
|
||||
const snapshot = await inspectUnknownListenerFallback({
|
||||
runtime: { status: "stopped" },
|
||||
includeUnknownListenersAsStale: false,
|
||||
});
|
||||
|
||||
@@ -121,24 +115,8 @@ describe("inspectGatewayRestart", () => {
|
||||
});
|
||||
|
||||
it("does not apply unknown-listener fallback while runtime is running", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
classifyPortListener.mockReturnValue("unknown");
|
||||
|
||||
const service = {
|
||||
readRuntime: vi.fn(async () => ({ status: "running", pid: 10920 })),
|
||||
} as unknown as GatewayService;
|
||||
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [{ pid: 10920, command: "unknown" }],
|
||||
hints: [],
|
||||
});
|
||||
|
||||
const { inspectGatewayRestart } = await import("./restart-health.js");
|
||||
const snapshot = await inspectGatewayRestart({
|
||||
service,
|
||||
port: 18789,
|
||||
const snapshot = await inspectUnknownListenerFallback({
|
||||
runtime: { status: "running", pid: 10920 },
|
||||
includeUnknownListenersAsStale: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
|
||||
import { findExtraGatewayServices } from "../../daemon/inspect.js";
|
||||
import type { ServiceConfigAudit } from "../../daemon/service-audit.js";
|
||||
import { auditGatewayServiceConfig } from "../../daemon/service-audit.js";
|
||||
import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { resolveGatewayBindHost } from "../../gateway/net.js";
|
||||
import {
|
||||
@@ -54,19 +55,7 @@ export type DaemonStatus = {
|
||||
environment?: Record<string, string>;
|
||||
sourcePath?: string;
|
||||
} | null;
|
||||
runtime?: {
|
||||
status?: string;
|
||||
state?: string;
|
||||
subState?: string;
|
||||
pid?: number;
|
||||
lastExitStatus?: number;
|
||||
lastExitReason?: string;
|
||||
lastRunResult?: string;
|
||||
lastRunTime?: string;
|
||||
detail?: string;
|
||||
cachedLabel?: boolean;
|
||||
missingUnit?: boolean;
|
||||
};
|
||||
runtime?: GatewayServiceRuntime;
|
||||
configAudit?: ServiceConfigAudit;
|
||||
};
|
||||
config?: {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveUserPath, shortenHomePath } from "../utils.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { looksLikeLocalInstallSpec } from "./install-spec.js";
|
||||
import {
|
||||
buildNpmInstallRecordFields,
|
||||
resolvePinnedNpmInstallRecordForCli,
|
||||
@@ -660,15 +661,7 @@ export function registerHooksCli(program: Command): void {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const looksLikePath =
|
||||
raw.startsWith(".") ||
|
||||
raw.startsWith("~") ||
|
||||
path.isAbsolute(raw) ||
|
||||
raw.endsWith(".zip") ||
|
||||
raw.endsWith(".tgz") ||
|
||||
raw.endsWith(".tar.gz") ||
|
||||
raw.endsWith(".tar");
|
||||
if (looksLikePath) {
|
||||
if (looksLikeLocalInstallSpec(raw, [".zip", ".tgz", ".tar.gz", ".tar"])) {
|
||||
defaultRuntime.error(`Path not found: ${resolved}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
10
src/cli/install-spec.ts
Normal file
10
src/cli/install-spec.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import path from "node:path";
|
||||
|
||||
export function looksLikeLocalInstallSpec(spec: string, knownSuffixes: readonly string[]): boolean {
|
||||
return (
|
||||
spec.startsWith(".") ||
|
||||
spec.startsWith("~") ||
|
||||
path.isAbsolute(spec) ||
|
||||
knownSuffixes.some((suffix) => spec.endsWith(suffix))
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
export type NpmResolutionMetadata = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
resolvedSpec?: string;
|
||||
integrity?: string;
|
||||
shasum?: string;
|
||||
resolvedAt?: string;
|
||||
};
|
||||
import {
|
||||
buildNpmResolutionFields,
|
||||
type NpmSpecResolution as NpmResolutionMetadata,
|
||||
} from "../infra/install-source-utils.js";
|
||||
|
||||
export function resolvePinnedNpmSpec(params: {
|
||||
rawSpec: string;
|
||||
@@ -36,14 +32,7 @@ export function mapNpmResolutionMetadata(resolution?: NpmResolutionMetadata): {
|
||||
shasum?: string;
|
||||
resolvedAt?: string;
|
||||
} {
|
||||
return {
|
||||
resolvedName: resolution?.name,
|
||||
resolvedVersion: resolution?.version,
|
||||
resolvedSpec: resolution?.resolvedSpec,
|
||||
integrity: resolution?.integrity,
|
||||
shasum: resolution?.shasum,
|
||||
resolvedAt: resolution?.resolvedAt,
|
||||
};
|
||||
return buildNpmResolutionFields(resolution);
|
||||
}
|
||||
|
||||
export function buildNpmInstallRecordFields(params: {
|
||||
@@ -68,7 +57,7 @@ export function buildNpmInstallRecordFields(params: {
|
||||
spec: params.spec,
|
||||
installPath: params.installPath,
|
||||
version: params.version,
|
||||
...mapNpmResolutionMetadata(params.resolution),
|
||||
...buildNpmResolutionFields(params.resolution),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { formatDocsLink } from "../terminal/links.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveUserPath, shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
import { looksLikeLocalInstallSpec } from "./install-spec.js";
|
||||
import { resolvePinnedNpmInstallRecordForCli } from "./npm-resolution.js";
|
||||
import { setPluginEnabledInConfig } from "./plugins-config.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
@@ -603,19 +604,18 @@ export function registerPluginsCli(program: Command) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const looksLikePath =
|
||||
raw.startsWith(".") ||
|
||||
raw.startsWith("~") ||
|
||||
path.isAbsolute(raw) ||
|
||||
raw.endsWith(".ts") ||
|
||||
raw.endsWith(".js") ||
|
||||
raw.endsWith(".mjs") ||
|
||||
raw.endsWith(".cjs") ||
|
||||
raw.endsWith(".tgz") ||
|
||||
raw.endsWith(".tar.gz") ||
|
||||
raw.endsWith(".tar") ||
|
||||
raw.endsWith(".zip");
|
||||
if (looksLikePath) {
|
||||
if (
|
||||
looksLikeLocalInstallSpec(raw, [
|
||||
".ts",
|
||||
".js",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".tgz",
|
||||
".tar.gz",
|
||||
".tar",
|
||||
".zip",
|
||||
])
|
||||
) {
|
||||
defaultRuntime.error(`Path not found: ${resolved}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,18 @@ describe("cli program (nodes media)", () => {
|
||||
await program.parseAsync(argv, { from: "user" });
|
||||
}
|
||||
|
||||
async function expectCameraSnapParseFailure(args: string[], expectedError: RegExp) {
|
||||
mockNodeGateway();
|
||||
|
||||
const parseProgram = new Command();
|
||||
parseProgram.exitOverride();
|
||||
registerNodesCli(parseProgram);
|
||||
runtime.error.mockClear();
|
||||
|
||||
await expect(parseProgram.parseAsync(args, { from: "user" })).rejects.toThrow(/exit/i);
|
||||
expect(runtime.error.mock.calls.some(([msg]) => expectedError.test(String(msg)))).toBe(true);
|
||||
}
|
||||
|
||||
async function runAndExpectUrlPayloadMediaFile(params: {
|
||||
command: "camera.snap" | "camera.clip";
|
||||
payload: Record<string, unknown>;
|
||||
@@ -266,54 +278,27 @@ describe("cli program (nodes media)", () => {
|
||||
});
|
||||
|
||||
it("fails nodes camera snap on invalid facing", async () => {
|
||||
mockNodeGateway();
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerNodesCli(program);
|
||||
runtime.error.mockClear();
|
||||
|
||||
await expect(
|
||||
program.parseAsync(["nodes", "camera", "snap", "--node", "ios-node", "--facing", "nope"], {
|
||||
from: "user",
|
||||
}),
|
||||
).rejects.toThrow(/exit/i);
|
||||
|
||||
expect(runtime.error.mock.calls.some(([msg]) => /invalid facing/i.test(String(msg)))).toBe(
|
||||
true,
|
||||
await expectCameraSnapParseFailure(
|
||||
["nodes", "camera", "snap", "--node", "ios-node", "--facing", "nope"],
|
||||
/invalid facing/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails nodes camera snap when --facing both and --device-id are combined", async () => {
|
||||
mockNodeGateway();
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerNodesCli(program);
|
||||
runtime.error.mockClear();
|
||||
|
||||
await expect(
|
||||
program.parseAsync(
|
||||
[
|
||||
"nodes",
|
||||
"camera",
|
||||
"snap",
|
||||
"--node",
|
||||
"ios-node",
|
||||
"--facing",
|
||||
"both",
|
||||
"--device-id",
|
||||
"cam-123",
|
||||
],
|
||||
{ from: "user" },
|
||||
),
|
||||
).rejects.toThrow(/exit/i);
|
||||
|
||||
expect(
|
||||
runtime.error.mock.calls.some(([msg]) =>
|
||||
/facing=both is not allowed when --device-id is set/i.test(String(msg)),
|
||||
),
|
||||
).toBe(true);
|
||||
await expectCameraSnapParseFailure(
|
||||
[
|
||||
"nodes",
|
||||
"camera",
|
||||
"snap",
|
||||
"--node",
|
||||
"ios-node",
|
||||
"--facing",
|
||||
"both",
|
||||
"--device-id",
|
||||
"cam-123",
|
||||
],
|
||||
/facing=both is not allowed when --device-id is set/i,
|
||||
);
|
||||
});
|
||||
|
||||
describe("URL-based payloads", () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { resolveStateDir } from "../../config/paths.js";
|
||||
import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js";
|
||||
import { readPackageName, readPackageVersion } from "../../infra/package-json.js";
|
||||
import { normalizePackageTagInput } from "../../infra/package-tag.js";
|
||||
import { trimLogTail } from "../../infra/restart-sentinel.js";
|
||||
import { parseSemver } from "../../infra/runtime-guard.js";
|
||||
import { fetchNpmTagVersion } from "../../infra/update-check.js";
|
||||
@@ -58,20 +59,7 @@ export const DEFAULT_PACKAGE_NAME = "openclaw";
|
||||
const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]);
|
||||
|
||||
export function normalizeTag(value?: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
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);
|
||||
}
|
||||
return trimmed;
|
||||
return normalizePackageTagInput(value, ["openclaw", DEFAULT_PACKAGE_NAME]);
|
||||
}
|
||||
|
||||
export function normalizeVersionTag(tag: string): string | null {
|
||||
|
||||
Reference in New Issue
Block a user