mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 05:31:30 +00:00
refactor: dedupe cli cron trimmed readers
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
} from "../plugins/memory-embedding-providers.js";
|
||||
import { writeRuntimeJson, defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeStringifiedOptionalString } from "../shared/string-coerce.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { canonicalizeSpeechProviderId, listSpeechProviders } from "../tts/provider-registry.js";
|
||||
@@ -1237,7 +1238,7 @@ export function registerCapabilityCli(program: Command) {
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
const target = String(opts.model).trim();
|
||||
const target = normalizeStringifiedOptionalString(opts.model) ?? "";
|
||||
const catalog = await loadModelCatalog({ config: loadConfig() });
|
||||
const entry =
|
||||
catalog.find((candidate) => `${candidate.provider}/${candidate.id}` === target) ??
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
discoverConfigSecretTargets,
|
||||
resolveConfigSecretTargetByPath,
|
||||
} from "../secrets/target-registry.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
@@ -370,7 +371,7 @@ function pruneInactiveGatewayAuthCredentials(params: {
|
||||
return [];
|
||||
}
|
||||
const auth = authRaw as Record<string, unknown>;
|
||||
const mode = typeof auth.mode === "string" ? auth.mode.trim() : "";
|
||||
const mode = normalizeOptionalString(auth.mode) ?? "";
|
||||
|
||||
const removedPaths: string[] = [];
|
||||
const remove = (key: "token" | "password") => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
|
||||
export type ConfigSetOptions = {
|
||||
strictJson?: boolean;
|
||||
@@ -38,8 +42,7 @@ export type ConfigSetBatchEntry = {
|
||||
|
||||
export function hasBatchMode(opts: ConfigSetOptions): boolean {
|
||||
return Boolean(
|
||||
(opts.batchJson && opts.batchJson.trim().length > 0) ||
|
||||
(opts.batchFile && opts.batchFile.trim().length > 0),
|
||||
normalizeOptionalString(opts.batchJson) || normalizeOptionalString(opts.batchFile),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,7 +90,7 @@ function parseBatchEntries(raw: string, sourceLabel: string): ConfigSetBatchEntr
|
||||
throw new Error(`${sourceLabel}[${index}] must be an object.`);
|
||||
}
|
||||
const typed = entry as Record<string, unknown>;
|
||||
const path = typeof typed.path === "string" ? typed.path.trim() : "";
|
||||
const path = normalizeOptionalString(typed.path) ?? "";
|
||||
if (!path) {
|
||||
throw new Error(`${sourceLabel}[${index}].path is required.`);
|
||||
}
|
||||
@@ -111,8 +114,10 @@ function parseBatchEntries(raw: string, sourceLabel: string): ConfigSetBatchEntr
|
||||
}
|
||||
|
||||
export function parseBatchSource(opts: ConfigSetOptions): ConfigSetBatchEntry[] | null {
|
||||
const hasInline = Boolean(opts.batchJson && opts.batchJson.trim().length > 0);
|
||||
const hasFile = Boolean(opts.batchFile && opts.batchFile.trim().length > 0);
|
||||
const batchJson = normalizeOptionalString(opts.batchJson);
|
||||
const batchFile = normalizeOptionalString(opts.batchFile);
|
||||
const hasInline = Boolean(batchJson);
|
||||
const hasFile = Boolean(batchFile);
|
||||
if (!hasInline && !hasFile) {
|
||||
return null;
|
||||
}
|
||||
@@ -120,9 +125,9 @@ export function parseBatchSource(opts: ConfigSetOptions): ConfigSetBatchEntry[]
|
||||
throw new Error("Use either --batch-json or --batch-file, not both.");
|
||||
}
|
||||
if (hasInline) {
|
||||
return parseBatchEntries(opts.batchJson as string, "--batch-json");
|
||||
return parseBatchEntries(batchJson as string, "--batch-json");
|
||||
}
|
||||
const pathname = (opts.batchFile as string).trim();
|
||||
const pathname = normalizeStringifiedOptionalString(opts.batchFile) ?? "";
|
||||
if (!pathname) {
|
||||
throw new Error("--batch-file must not be empty.");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from "../infra/device-pairing.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
@@ -208,7 +212,7 @@ function formatTokenSummary(tokens: DeviceTokenSummary[] | undefined) {
|
||||
}
|
||||
|
||||
function formatPendingRoles(request: PendingDevice): string {
|
||||
const role = typeof request.role === "string" ? request.role.trim() : "";
|
||||
const role = normalizeOptionalString(request.role) ?? "";
|
||||
if (role) {
|
||||
return role;
|
||||
}
|
||||
@@ -234,8 +238,8 @@ function formatPendingScopes(request: PendingDevice): string {
|
||||
function resolveRequiredDeviceRole(
|
||||
opts: DevicesRpcOpts,
|
||||
): { deviceId: string; role: string } | null {
|
||||
const deviceId = String(opts.device ?? "").trim();
|
||||
const role = String(opts.role ?? "").trim();
|
||||
const deviceId = normalizeStringifiedOptionalString(opts.device) ?? "";
|
||||
const role = normalizeStringifiedOptionalString(opts.role) ?? "";
|
||||
if (deviceId && role) {
|
||||
return { deviceId, role };
|
||||
}
|
||||
@@ -355,7 +359,7 @@ export function registerDevicesCli(program: Command) {
|
||||
const rejectedRequestIds: string[] = [];
|
||||
const paired = Array.isArray(list.paired) ? list.paired : [];
|
||||
for (const device of paired) {
|
||||
const deviceId = typeof device.deviceId === "string" ? device.deviceId.trim() : "";
|
||||
const deviceId = normalizeOptionalString(device.deviceId) ?? "";
|
||||
if (!deviceId) {
|
||||
continue;
|
||||
}
|
||||
@@ -365,7 +369,7 @@ export function registerDevicesCli(program: Command) {
|
||||
if (opts.pending) {
|
||||
const pending = Array.isArray(list.pending) ? list.pending : [];
|
||||
for (const req of pending) {
|
||||
const requestId = typeof req.requestId === "string" ? req.requestId.trim() : "";
|
||||
const requestId = normalizeOptionalString(req.requestId) ?? "";
|
||||
if (!requestId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { danger } from "../globals.js";
|
||||
import { resolveMessageChannelSelection } from "../infra/outbound/channel-selection.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
@@ -22,7 +26,7 @@ function parseLimit(value: unknown): number | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const raw = value.trim();
|
||||
const raw = normalizeOptionalString(value) ?? "";
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
@@ -36,7 +40,7 @@ function parseLimit(value: unknown): number | null {
|
||||
function buildRows(entries: Array<{ id: string; name?: string | undefined }>) {
|
||||
return entries.map((entry) => ({
|
||||
ID: entry.id,
|
||||
Name: entry.name?.trim() ?? "",
|
||||
Name: normalizeOptionalString(entry.name) ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -274,7 +278,7 @@ export function registerDirectoryCli(program: Command) {
|
||||
if (!fn) {
|
||||
throw new Error(`Channel ${channelId} does not support group members listing`);
|
||||
}
|
||||
const groupId = String(opts.groupId ?? "").trim();
|
||||
const groupId = normalizeStringifiedOptionalString(opts.groupId) ?? "";
|
||||
if (!groupId) {
|
||||
throw new Error("Missing --group-id");
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { serveOpenClawChannelMcp } from "../mcp/channel-server.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { normalizeStringifiedOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
function fail(message: string): never {
|
||||
defaultRuntime.error(message);
|
||||
@@ -85,7 +86,7 @@ export function registerMcpCli(program: Command) {
|
||||
warnSecretCliFlag("--password");
|
||||
}
|
||||
const claudeChannelMode = normalizeLowercaseStringOrEmpty(
|
||||
String(opts.claudeChannelMode ?? "auto").trim(),
|
||||
normalizeStringifiedOptionalString(opts.claudeChannelMode) ?? "auto",
|
||||
);
|
||||
if (
|
||||
claudeChannelMode !== "auto" &&
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeStringifiedOptionalString } from "../../shared/string-coerce.js";
|
||||
|
||||
export { parseNodeList, parsePairingList } from "../../shared/node-list-parse.js";
|
||||
|
||||
export function formatPermissions(raw: unknown) {
|
||||
@@ -5,7 +7,7 @@ export function formatPermissions(raw: unknown) {
|
||||
return null;
|
||||
}
|
||||
const entries = Object.entries(raw as Record<string, unknown>)
|
||||
.map(([key, value]) => [String(key).trim(), value === true] as const)
|
||||
.map(([key, value]) => [normalizeStringifiedOptionalString(key) ?? "", value === true] as const)
|
||||
.filter(([key]) => key.length > 0)
|
||||
.toSorted((a, b) => a[0].localeCompare(b[0]));
|
||||
if (entries.length === 0) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../../terminal/table.js";
|
||||
import { shortenHomePath } from "../../utils.js";
|
||||
import {
|
||||
@@ -23,7 +26,7 @@ import {
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
|
||||
const parseFacing = (value: string): CameraFacing => {
|
||||
const v = normalizeLowercaseStringOrEmpty(String(value ?? "").trim());
|
||||
const v = normalizeLowercaseStringOrEmpty(normalizeOptionalString(value) ?? "");
|
||||
if (v === "front" || v === "back") {
|
||||
return v;
|
||||
}
|
||||
@@ -112,9 +115,11 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
await runNodesCommand("camera snap", async () => {
|
||||
const node = await resolveNode(opts, String(opts.node ?? ""));
|
||||
const node = await resolveNode(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const nodeId = node.nodeId;
|
||||
const facingOpt = normalizeLowercaseStringOrEmpty(String(opts.facing ?? "both").trim());
|
||||
const facingOpt = normalizeLowercaseStringOrEmpty(
|
||||
normalizeOptionalString(opts.facing) ?? "both",
|
||||
);
|
||||
const facings: CameraFacing[] =
|
||||
facingOpt === "both"
|
||||
? ["front", "back"]
|
||||
@@ -129,7 +134,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
const maxWidth = opts.maxWidth ? Number.parseInt(String(opts.maxWidth), 10) : undefined;
|
||||
const quality = opts.quality ? Number.parseFloat(String(opts.quality)) : undefined;
|
||||
const delayMs = opts.delayMs ? Number.parseInt(String(opts.delayMs), 10) : undefined;
|
||||
const deviceId = opts.deviceId ? String(opts.deviceId).trim() : undefined;
|
||||
const deviceId = normalizeOptionalString(opts.deviceId);
|
||||
if (deviceId && facings.length > 1) {
|
||||
throw new Error("facing=both is not allowed when --device-id is set");
|
||||
}
|
||||
@@ -206,7 +211,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 90000)", "90000")
|
||||
.action(async (opts: NodesRpcOpts & { audio?: boolean }) => {
|
||||
await runNodesCommand("camera clip", async () => {
|
||||
const node = await resolveNode(opts, String(opts.node ?? ""));
|
||||
const node = await resolveNode(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const nodeId = node.nodeId;
|
||||
const facing = parseFacing(String(opts.facing ?? "front"));
|
||||
const durationMs = parseDurationMs(String(opts.duration ?? "3000"));
|
||||
@@ -214,7 +219,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
const timeoutMs = opts.invokeTimeout
|
||||
? Number.parseInt(String(opts.invokeTimeout), 10)
|
||||
: undefined;
|
||||
const deviceId = opts.deviceId ? String(opts.deviceId).trim() : undefined;
|
||||
const deviceId = normalizeOptionalString(opts.deviceId);
|
||||
|
||||
const invokeParams = buildNodeInvokeParams({
|
||||
nodeId,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { shortenHomePath } from "../../utils.js";
|
||||
import { writeBase64ToFile } from "../nodes-camera.js";
|
||||
import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js";
|
||||
@@ -12,7 +15,7 @@ import { buildNodeInvokeParams, callGatewayCli, nodesCallOpts, resolveNodeId } f
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
|
||||
async function invokeCanvas(opts: NodesRpcOpts, command: string, params?: Record<string, unknown>) {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const timeoutMs = parseTimeoutMs(opts.invokeTimeout);
|
||||
return await callGatewayCli(
|
||||
"node.invoke",
|
||||
@@ -42,7 +45,9 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
await runNodesCommand("canvas snapshot", async () => {
|
||||
const formatOpt = normalizeLowercaseStringOrEmpty(String(opts.format ?? "jpg").trim());
|
||||
const formatOpt = normalizeLowercaseStringOrEmpty(
|
||||
normalizeOptionalString(opts.format) ?? "jpg",
|
||||
);
|
||||
const formatForParams =
|
||||
formatOpt === "jpg" ? "jpeg" : formatOpt === "jpeg" ? "jpeg" : "png";
|
||||
if (formatForParams !== "png" && formatForParams !== "jpeg") {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Command } from "commander";
|
||||
import { randomIdempotencyKey } from "../../gateway/call.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
|
||||
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
@@ -20,8 +23,8 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
.option("--idempotency-key <key>", "Idempotency key (optional)")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
await runNodesCommand("invoke", async () => {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const command = String(opts.command ?? "").trim();
|
||||
const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const command = normalizeOptionalString(opts.command) ?? "";
|
||||
if (!nodeId || !command) {
|
||||
const { error } = getNodesTheme();
|
||||
defaultRuntime.error(error("--node and --command required"));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import { randomIdempotencyKey } from "../../gateway/call.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
|
||||
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
@@ -19,9 +20,9 @@ export function registerNodesNotifyCommand(nodes: Command) {
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
await runNodesCommand("notify", async () => {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const title = String(opts.title ?? "").trim();
|
||||
const body = String(opts.body ?? "").trim();
|
||||
const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const title = normalizeOptionalString(opts.title) ?? "";
|
||||
const body = normalizeOptionalString(opts.body) ?? "";
|
||||
if (!title && !body) {
|
||||
throw new Error("missing --title or --body");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { getTerminalTableWidth } from "../../terminal/table.js";
|
||||
import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
|
||||
import { parsePairingList } from "./format.js";
|
||||
@@ -78,8 +79,8 @@ export function registerNodesPairingCommands(nodes: Command) {
|
||||
.requiredOption("--name <displayName>", "New display name")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
await runNodesCommand("rename", async () => {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const name = String(opts.name ?? "").trim();
|
||||
const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const name = normalizeOptionalString(opts.name) ?? "";
|
||||
if (!nodeId || !name) {
|
||||
defaultRuntime.error("--node and --name required");
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -37,9 +37,9 @@ export function registerNodesPushCommand(nodes: Command) {
|
||||
.option("--environment <sandbox|production>", "Override APNs environment")
|
||||
.action(async (opts: NodesPushOpts) => {
|
||||
await runNodesCommand("push", async () => {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const title = String(opts.title ?? "").trim() || "OpenClaw";
|
||||
const body = String(opts.body ?? "").trim() || `Push test for node ${nodeId}`;
|
||||
const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
|
||||
const title = normalizeOptionalString(opts.title) || "OpenClaw";
|
||||
const body = normalizeOptionalString(opts.body) || `Push test for node ${nodeId}`;
|
||||
const environment = normalizeEnvironment(opts.environment);
|
||||
if (opts.environment && !environment) {
|
||||
throw new Error("invalid --environment (use sandbox|production)");
|
||||
|
||||
@@ -92,8 +92,7 @@ function parseSinceMs(raw: unknown, label: string): number | undefined {
|
||||
if (raw === undefined || raw === null) {
|
||||
return undefined;
|
||||
}
|
||||
const value =
|
||||
typeof raw === "string" ? raw.trim() : typeof raw === "number" ? String(raw).trim() : null;
|
||||
const value = normalizeOptionalString(raw) ?? (typeof raw === "number" ? String(raw) : null);
|
||||
if (value === null) {
|
||||
defaultRuntime.error(`${label}: invalid duration value`);
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
type PairingChannel,
|
||||
} from "../pairing/pairing-store.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
@@ -17,14 +20,7 @@ import { formatCliCommand } from "./command-format.js";
|
||||
|
||||
/** Parse channel, allowing extension channels not in core registry. */
|
||||
function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel {
|
||||
const value = normalizeLowercaseStringOrEmpty(
|
||||
(typeof raw === "string"
|
||||
? raw
|
||||
: typeof raw === "number" || typeof raw === "boolean"
|
||||
? String(raw)
|
||||
: ""
|
||||
).trim(),
|
||||
);
|
||||
const value = normalizeLowercaseStringOrEmpty(normalizeStringifiedOptionalString(raw) ?? "");
|
||||
if (!value) {
|
||||
throw new Error("Channel required");
|
||||
}
|
||||
@@ -75,7 +71,7 @@ export function registerPairingCli(program: Command) {
|
||||
);
|
||||
}
|
||||
const channel = parseChannel(channelRaw, channels);
|
||||
const accountId = String(opts.account ?? "").trim();
|
||||
const accountId = normalizeStringifiedOptionalString(opts.account) ?? "";
|
||||
const requests = accountId
|
||||
? await listChannelPairingRequests(channel, process.env, accountId)
|
||||
: await listChannelPairingRequests(channel);
|
||||
@@ -144,7 +140,7 @@ export function registerPairingCli(program: Command) {
|
||||
);
|
||||
}
|
||||
const channel = parseChannel(channelRaw, channels);
|
||||
const accountId = String(opts.account ?? "").trim();
|
||||
const accountId = normalizeStringifiedOptionalString(opts.account) ?? "";
|
||||
const approved = accountId
|
||||
? await approveChannelPairingCode({
|
||||
channel,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
|
||||
export type BytesParseOptions = {
|
||||
defaultUnit?: "b" | "kb" | "mb" | "gb" | "tb";
|
||||
@@ -17,7 +20,7 @@ const UNIT_MULTIPLIERS: Record<string, number> = {
|
||||
};
|
||||
|
||||
export function parseByteSize(raw: string, opts?: BytesParseOptions): number {
|
||||
const trimmed = normalizeLowercaseStringOrEmpty(String(raw ?? "").trim());
|
||||
const trimmed = normalizeLowercaseStringOrEmpty(normalizeOptionalString(raw) ?? "");
|
||||
if (!trimmed) {
|
||||
throw new Error("invalid byte size (empty)");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
|
||||
export type DurationMsParseOptions = {
|
||||
defaultUnit?: "ms" | "s" | "m" | "h" | "d";
|
||||
@@ -13,7 +16,7 @@ const DURATION_MULTIPLIERS: Record<string, number> = {
|
||||
};
|
||||
|
||||
export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): number {
|
||||
const trimmed = normalizeLowercaseStringOrEmpty(String(raw ?? "").trim());
|
||||
const trimmed = normalizeLowercaseStringOrEmpty(normalizeOptionalString(raw) ?? "");
|
||||
if (!trimmed) {
|
||||
throw new Error("invalid duration (empty)");
|
||||
}
|
||||
|
||||
@@ -121,15 +121,15 @@ export function registerQrCli(program: Command) {
|
||||
throw new Error("Use either --token or --password, not both.");
|
||||
}
|
||||
|
||||
const token = typeof opts.token === "string" ? opts.token.trim() : "";
|
||||
const password = typeof opts.password === "string" ? opts.password.trim() : "";
|
||||
const token = trimToUndefined(opts.token) ?? "";
|
||||
const password = trimToUndefined(opts.password) ?? "";
|
||||
const wantsRemote = opts.remote === true;
|
||||
|
||||
const loadedRaw = loadConfig();
|
||||
if (wantsRemote && !opts.url && !opts.publicUrl) {
|
||||
const tailscaleMode = loadedRaw.gateway?.tailscale?.mode ?? "off";
|
||||
const remoteUrl = loadedRaw.gateway?.remote?.url;
|
||||
const hasRemoteUrl = typeof remoteUrl === "string" && remoteUrl.trim().length > 0;
|
||||
const hasRemoteUrl = Boolean(trimToUndefined(remoteUrl));
|
||||
const hasTailscaleServe = tailscaleMode === "serve" || tailscaleMode === "funnel";
|
||||
if (!hasRemoteUrl && !hasTailscaleServe) {
|
||||
throw new Error(
|
||||
@@ -170,12 +170,8 @@ export function registerQrCli(program: Command) {
|
||||
cfg.gateway.auth.token = undefined;
|
||||
}
|
||||
if (wantsRemote && !token && !password) {
|
||||
const remoteToken =
|
||||
typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway.remote.token.trim() : "";
|
||||
const remotePassword =
|
||||
typeof cfg.gateway?.remote?.password === "string"
|
||||
? cfg.gateway.remote.password.trim()
|
||||
: "";
|
||||
const remoteToken = trimToUndefined(cfg.gateway?.remote?.token) ?? "";
|
||||
const remotePassword = trimToUndefined(cfg.gateway?.remote?.password) ?? "";
|
||||
if (remoteToken) {
|
||||
cfg.gateway.auth.mode = "token";
|
||||
cfg.gateway.auth.token = remoteToken;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import { danger } from "../globals.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import type { GatewayRpcOpts } from "./gateway-rpc.js";
|
||||
@@ -10,7 +11,7 @@ type SystemEventOpts = GatewayRpcOpts & { text?: string; mode?: string; json?: b
|
||||
type SystemGatewayOpts = GatewayRpcOpts & { json?: boolean };
|
||||
|
||||
const normalizeWakeMode = (raw: unknown) => {
|
||||
const mode = typeof raw === "string" ? raw.trim() : "";
|
||||
const mode = normalizeOptionalString(raw) ?? "";
|
||||
if (!mode) {
|
||||
return "next-heartbeat" as const;
|
||||
}
|
||||
@@ -59,7 +60,7 @@ export function registerSystemCli(program: Command) {
|
||||
await runSystemGatewayCommand(
|
||||
opts,
|
||||
async () => {
|
||||
const text = typeof opts.text === "string" ? opts.text.trim() : "";
|
||||
const text = normalizeOptionalString(opts.text) ?? "";
|
||||
if (!text) {
|
||||
throw new Error("--text is required");
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function registerWebhooksCli(program: Command) {
|
||||
|
||||
function parseGmailSetupOptions(raw: Record<string, unknown>): GmailSetupOptions {
|
||||
const accountRaw = raw.account;
|
||||
const account = typeof accountRaw === "string" ? accountRaw.trim() : "";
|
||||
const account = normalizeOptionalString(accountRaw) ?? "";
|
||||
if (!account) {
|
||||
throw new Error("--account is required");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
export function normalizeCronJobIdentityFields(raw: Record<string, unknown>): {
|
||||
mutated: boolean;
|
||||
legacyJobIdIssue: boolean;
|
||||
} {
|
||||
const rawId = typeof raw.id === "string" ? raw.id.trim() : "";
|
||||
const legacyJobId = typeof raw.jobId === "string" ? raw.jobId.trim() : "";
|
||||
const rawId = normalizeOptionalString(raw.id) ?? "";
|
||||
const legacyJobId = normalizeOptionalString(raw.jobId) ?? "";
|
||||
const hadJobIdKey = "jobId" in raw;
|
||||
const normalizedId = rawId || legacyJobId;
|
||||
const idChanged = Boolean(normalizedId && raw.id !== normalizedId);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { CronConfig } from "../config/types.cron.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import type { CronDeliveryStatus, CronRunStatus, CronRunTelemetry } from "./types.js";
|
||||
|
||||
@@ -93,7 +94,10 @@ export function resolveCronRunLogPruneOptions(cfg?: CronConfig["runLog"]): {
|
||||
let maxBytes = DEFAULT_CRON_RUN_LOG_MAX_BYTES;
|
||||
if (cfg?.maxBytes !== undefined) {
|
||||
try {
|
||||
maxBytes = parseByteSize(String(cfg.maxBytes).trim(), { defaultUnit: "b" });
|
||||
const configuredMaxBytes = normalizeStringifiedOptionalString(cfg.maxBytes);
|
||||
if (configuredMaxBytes) {
|
||||
maxBytes = parseByteSize(configuredMaxBytes, { defaultUnit: "b" });
|
||||
}
|
||||
} catch {
|
||||
maxBytes = DEFAULT_CRON_RUN_LOG_MAX_BYTES;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Cron } from "croner";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { parseAbsoluteTimeMs } from "./parse.js";
|
||||
import type { CronSchedule } from "./types.js";
|
||||
|
||||
@@ -6,7 +7,7 @@ const CRON_EVAL_CACHE_MAX = 512;
|
||||
const cronEvalCache = new Map<string, Cron>();
|
||||
|
||||
function resolveCronTimezone(tz?: string) {
|
||||
const trimmed = typeof tz === "string" ? tz.trim() : "";
|
||||
const trimmed = normalizeOptionalString(tz) ?? "";
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -764,19 +764,19 @@ function mergeCronDelivery(
|
||||
};
|
||||
if (patchFd) {
|
||||
if ("channel" in patchFd) {
|
||||
const channel = typeof patchFd.channel === "string" ? patchFd.channel.trim() : "";
|
||||
const channel = normalizeOptionalString(patchFd.channel) ?? "";
|
||||
nextFd.channel = channel ? channel : undefined;
|
||||
}
|
||||
if ("to" in patchFd) {
|
||||
const to = typeof patchFd.to === "string" ? patchFd.to.trim() : "";
|
||||
const to = normalizeOptionalString(patchFd.to) ?? "";
|
||||
nextFd.to = to ? to : undefined;
|
||||
}
|
||||
if ("accountId" in patchFd) {
|
||||
const accountId = typeof patchFd.accountId === "string" ? patchFd.accountId.trim() : "";
|
||||
const accountId = normalizeOptionalString(patchFd.accountId) ?? "";
|
||||
nextFd.accountId = accountId ? accountId : undefined;
|
||||
}
|
||||
if ("mode" in patchFd) {
|
||||
const mode = typeof patchFd.mode === "string" ? patchFd.mode.trim() : "";
|
||||
const mode = normalizeOptionalString(patchFd.mode) ?? "";
|
||||
nextFd.mode = mode === "announce" || mode === "webhook" ? mode : undefined;
|
||||
}
|
||||
}
|
||||
@@ -818,11 +818,11 @@ function mergeCronFailureAlert(
|
||||
next.cooldownMs = cooldownMs >= 0 ? Math.floor(cooldownMs) : undefined;
|
||||
}
|
||||
if ("mode" in patch) {
|
||||
const mode = typeof patch.mode === "string" ? patch.mode.trim() : "";
|
||||
const mode = normalizeOptionalString(patch.mode) ?? "";
|
||||
next.mode = mode === "announce" || mode === "webhook" ? mode : undefined;
|
||||
}
|
||||
if ("accountId" in patch) {
|
||||
const accountId = typeof patch.accountId === "string" ? patch.accountId.trim() : "";
|
||||
const accountId = normalizeOptionalString(patch.accountId) ?? "";
|
||||
next.accountId = accountId ? accountId : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { parseAbsoluteTimeMs } from "./parse.js";
|
||||
import type { CronSchedule } from "./types.js";
|
||||
|
||||
@@ -29,7 +30,7 @@ export function validateScheduleTimestamp(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const atRaw = typeof schedule.at === "string" ? schedule.at.trim() : "";
|
||||
const atRaw = normalizeOptionalString(schedule.at) ?? "";
|
||||
const atMs = atRaw ? parseAbsoluteTimeMs(atRaw) : null;
|
||||
|
||||
if (atMs === null || !Number.isFinite(atMs)) {
|
||||
|
||||
@@ -35,6 +35,7 @@ export {
|
||||
normalizeNullableString,
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
normalizeStringifiedOptionalString,
|
||||
readStringValue,
|
||||
} from "../shared/string-coerce.js";
|
||||
export {
|
||||
|
||||
@@ -14,6 +14,16 @@ export function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return normalizeNullableString(value) ?? undefined;
|
||||
}
|
||||
|
||||
export function normalizeStringifiedOptionalString(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return normalizeOptionalString(value);
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return normalizeOptionalString(String(value));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeOptionalLowercaseString(value: unknown): string | undefined {
|
||||
return normalizeOptionalString(value)?.toLowerCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user