mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-17 08:02:12 -06:00
6205cf3bb3
* fix(cron): make --json help match behavior * fix(cron): preserve scratch write JSON output * test(cli): classify cron JSON result commands
506 lines
24 KiB
TypeScript
506 lines
24 KiB
TypeScript
import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion";
|
|
// Cron edit command registration and patch construction for existing jobs.
|
|
import {
|
|
normalizeOptionalLowercaseString,
|
|
normalizeOptionalString,
|
|
} from "@openclaw/normalization-core/string-coerce";
|
|
import type { Command } from "commander";
|
|
import { THINKING_LEVELS_HELP } from "../../auto-reply/thinking.shared.js";
|
|
import type { CronJob } from "../../cron/types.js";
|
|
import { danger } from "../../globals.js";
|
|
import { formatErrorMessage } from "../../infra/errors.js";
|
|
import { sanitizeAgentId } from "../../routing/session-key.js";
|
|
import { defaultRuntime } from "../../runtime.js";
|
|
import {
|
|
addGatewayClientOptions,
|
|
callGatewayFromCli,
|
|
type GatewayRpcOpts,
|
|
} from "../gateway-rpc.js";
|
|
import { parseDurationMs } from "../parse-duration.js";
|
|
import { isUnknownCronGetMethodError, listCronJobsFromGateway } from "./list-jobs.js";
|
|
import { createCronOutputCommand } from "./output-mode.js";
|
|
import { resolveCronEditPayloadDeliveryPatch } from "./register.cron-edit-options.js";
|
|
import {
|
|
applyExistingCronSchedulePatch,
|
|
applyExistingStreamSchedulePatch,
|
|
resolveCronEditScheduleRequest,
|
|
validateStreamScheduleMetadata,
|
|
} from "./schedule-options.js";
|
|
import { getCronChannelOptions, warnIfCronSchedulerDisabled } from "./shared.js";
|
|
import { normalizeCronSessionTargetOption } from "./thread-id-shared.js";
|
|
import { readCronTriggerScript } from "./trigger-options.js";
|
|
|
|
type CronJobForEdit = CronJob & { configRevision?: string };
|
|
|
|
async function readCronJobForEdit(opts: GatewayRpcOpts, id: string): Promise<CronJobForEdit> {
|
|
try {
|
|
return (await callGatewayFromCli("cron.get", opts, { id })) as CronJobForEdit;
|
|
} catch (error) {
|
|
if (!isUnknownCronGetMethodError(error)) {
|
|
throw error;
|
|
}
|
|
// Protocol-v4 gateways shipped before cron.get; keep remote edits working
|
|
// without paying the paginated lookup cost on current gateways.
|
|
const inventory = await listCronJobsFromGateway(
|
|
opts,
|
|
{ includeDisabled: true },
|
|
{ allowLegacyUnversionedPagination: true },
|
|
);
|
|
const existing = inventory.jobs.find((job) => job.id === id);
|
|
if (!existing) {
|
|
throw new Error(`unknown automation id: ${id}`, { cause: error });
|
|
}
|
|
return existing;
|
|
}
|
|
}
|
|
|
|
export function registerCronEditCommand(cron: Command) {
|
|
addGatewayClientOptions(
|
|
createCronOutputCommand(cron, "edit")
|
|
.description("Edit an automation (patch fields)")
|
|
.argument("<id>", "Job id")
|
|
.option("--name <name>", "Set name")
|
|
.option("--display-name <name>", "Set human-readable display name")
|
|
.option("--clear-display-name", "Restore the stable name in list and detail views", false)
|
|
.option("--description <text>", "Set description")
|
|
.option("--enable", "Enable job", false)
|
|
.option("--disable", "Disable job", false)
|
|
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
|
|
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
|
|
.option("--session <target>", "Session target (main|isolated)")
|
|
.option("--agent <id>", "Set agent id")
|
|
.option("--clear-agent", "Unset agent and use default", false)
|
|
.option("--session-key <key>", "Set session key for job routing")
|
|
.option("--clear-session-key", "Unset session key", false)
|
|
.option("--wake <mode>", "Wake mode (now|next-heartbeat)")
|
|
.option("--at <when>", "Set one-shot time (ISO, offset-less uses --tz) or duration like 20m")
|
|
.option("--every <duration>", "Set interval duration like 10m")
|
|
.option("--pacing-min <duration>", "Set minimum delay for a dynamic next check")
|
|
.option("--pacing-max <duration>", "Set maximum delay for a dynamic next check")
|
|
.option("--clear-pacing", "Remove dynamic-cadence bounds", false)
|
|
.option("--cron <expr>", "Set cron expression")
|
|
.option("--stream-command <json>", "Set stream source argv as a JSON array of strings")
|
|
.option("--stream-cwd <path>", "Set stream source working directory")
|
|
.option("--stream-mode <mode>", "Set stream selection mode (line|match)")
|
|
.option("--stream-match <regex>", "Set stream match regex source")
|
|
.option("--stream-batch-ms <n>", "Set stream quiet-window delay in milliseconds")
|
|
.option("--stream-max-batch-bytes <n>", "Set maximum UTF-8 bytes per stream batch")
|
|
.option(
|
|
"--tz <iana>",
|
|
"Timezone for cron expressions (IANA; cron default: Gateway host local timezone)",
|
|
)
|
|
.option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
|
|
.option("--exact", "Disable cron staggering (set stagger to 0)")
|
|
.option("--trigger-script <path|->", "Set condition script from file, or - for stdin")
|
|
.option("--trigger-once", "Disable after the first successful triggered run", false)
|
|
.option("--clear-trigger", "Remove the condition trigger", false)
|
|
.option("--system-event <text>", "Set systemEvent payload")
|
|
.option("--message <text>", "Set agentTurn payload message")
|
|
.option("--script <file|->", "Set headless script payload from file, or - for stdin")
|
|
.option("--script-timeout-seconds <n>", "Set script wall-clock timeout seconds")
|
|
.option("--script-tool-budget <n>", "Set maximum script tool calls")
|
|
.option("--command <shell>", "Set command payload run as sh -lc <shell> on the Gateway")
|
|
.option("--command-argv <json>", "Set command payload argv as JSON array of strings")
|
|
.option("--command-cwd <path>", "Set command payload working directory")
|
|
.option(
|
|
"--command-env <KEY=VALUE>",
|
|
"Set command payload environment overrides (repeatable)",
|
|
(value: string, previous: string[] | undefined) => [...(previous ?? []), value],
|
|
)
|
|
.option("--command-input <text>", "Set command payload stdin")
|
|
.option("--thinking <level>", `Thinking level for agent jobs (${THINKING_LEVELS_HELP})`)
|
|
.option(
|
|
"--clear-thinking",
|
|
"Remove the per-job thinking override (restore normal cron thinking precedence)",
|
|
false,
|
|
)
|
|
.option("--model <model>", "Model override for agent jobs")
|
|
.option("--fallbacks <list>", "Fallback model list for agent jobs")
|
|
.option("--clear-fallbacks", "Remove per-job fallback override", false)
|
|
.option(
|
|
"--clear-model",
|
|
"Remove the per-job model override (restore normal cron model precedence)",
|
|
false,
|
|
)
|
|
.option("--timeout-seconds <n>", "Timeout seconds for agent or command jobs")
|
|
.option("--no-output-timeout-seconds <n>", "No-output timeout seconds for command jobs")
|
|
.option("--output-max-bytes <n>", "Maximum captured stdout/stderr bytes for command jobs")
|
|
.option("--light-context", "Enable lightweight bootstrap context for agent jobs")
|
|
.option("--no-light-context", "Disable lightweight bootstrap context for agent jobs")
|
|
.option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)")
|
|
.option("--clear-tools", "Remove tool allow-list (use all tools)", false)
|
|
.option("--announce", "Fallback-deliver final text to a chat")
|
|
.option("--deliver", "Deprecated (use --announce). Fallback-delivers final text to a chat.")
|
|
.option("--no-deliver", "Disable runner fallback delivery")
|
|
.option("--webhook <url>", "POST the finished payload to a webhook URL")
|
|
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`)
|
|
.option(
|
|
"--to <dest>",
|
|
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
|
)
|
|
.option("--thread-id <id>", "Telegram forum topic thread id")
|
|
.option("--account <id>", "Channel account id for delivery (multi-account setups)")
|
|
.option("--clear-channel", "Unset the delivery channel", false)
|
|
.option("--clear-to", "Unset the delivery destination", false)
|
|
.option("--clear-thread-id", "Unset the Telegram forum topic thread id", false)
|
|
.option("--clear-account", "Unset the per-job delivery account override", false)
|
|
.option(
|
|
"--best-effort-deliver",
|
|
"Do not fail job if delivery fails (also implies --announce when used alone)",
|
|
)
|
|
.option("--no-best-effort-deliver", "Fail job when delivery fails")
|
|
.option("--failure-alert", "Enable failure alerts for this job")
|
|
.option("--no-failure-alert", "Disable failure alerts for this job")
|
|
.option("--failure-alert-after <n>", "Alert after N consecutive job errors")
|
|
.option(
|
|
"--failure-alert-channel <channel>",
|
|
`Failure alert channel (${getCronChannelOptions()})`,
|
|
)
|
|
.option("--failure-alert-to <dest>", "Failure alert destination")
|
|
.option("--failure-alert-cooldown <duration>", "Minimum time between alerts (e.g. 1h, 30m)")
|
|
.option("--failure-alert-include-skipped", "Count consecutive skipped runs toward alerts")
|
|
.option("--failure-alert-exclude-skipped", "Alert only on execution errors")
|
|
.option("--failure-alert-mode <mode>", "Failure alert delivery mode (announce or webhook)")
|
|
.option(
|
|
"--failure-alert-account-id <id>",
|
|
"Account ID for failure alert channel (multi-account setups)",
|
|
)
|
|
.action(async (id, opts) => {
|
|
try {
|
|
if (opts.clearTools && opts.tools !== undefined) {
|
|
throw new Error("Use --tools or --clear-tools, not both");
|
|
}
|
|
let existingJobPromise: Promise<CronJobForEdit> | undefined;
|
|
let expectedConfigRevision: string | undefined;
|
|
const readExistingCronJob = async (): Promise<CronJobForEdit> => {
|
|
const existing = await (existingJobPromise ??= readCronJobForEdit(opts, String(id)));
|
|
if (typeof existing.configRevision === "string") {
|
|
expectedConfigRevision = existing.configRevision;
|
|
}
|
|
return existing;
|
|
};
|
|
const sessionTarget =
|
|
typeof opts.session === "string"
|
|
? normalizeCronSessionTargetOption(opts.session)
|
|
: undefined;
|
|
if (typeof opts.session === "string" && !sessionTarget) {
|
|
throw new Error("--session must be main, isolated, current, or session:<id>");
|
|
}
|
|
if (sessionTarget === "main" && (opts.message || opts.command || opts.commandArgv)) {
|
|
throw new Error(
|
|
"Main jobs cannot use --message or --command; use --system-event or --session isolated.",
|
|
);
|
|
}
|
|
if (
|
|
(sessionTarget === "current" || sessionTarget?.startsWith("session:")) &&
|
|
typeof opts.script === "string"
|
|
) {
|
|
throw new Error("Script jobs require --session main or --session isolated.");
|
|
}
|
|
if (
|
|
(sessionTarget === "isolated" ||
|
|
sessionTarget === "current" ||
|
|
sessionTarget?.startsWith("session:")) &&
|
|
opts.systemEvent
|
|
) {
|
|
throw new Error(
|
|
"Isolated jobs cannot use --system-event; use --message, --command, or --session main.",
|
|
);
|
|
}
|
|
const hasExplicitChatDelivery =
|
|
typeof opts.channel === "string" ||
|
|
typeof opts.to === "string" ||
|
|
typeof opts.account === "string" ||
|
|
typeof opts.threadId === "string";
|
|
if (
|
|
sessionTarget === "main" &&
|
|
typeof opts.systemEvent === "string" &&
|
|
hasExplicitChatDelivery
|
|
) {
|
|
throw new Error(
|
|
"--channel, --to, --account, and --thread-id require a non-main agentTurn or command job with delivery.",
|
|
);
|
|
}
|
|
const hasWebhookDelivery = typeof opts.webhook === "string";
|
|
const deliveryModeFlagCount = [
|
|
Boolean(opts.announce),
|
|
typeof opts.deliver === "boolean",
|
|
hasWebhookDelivery,
|
|
].filter(Boolean).length;
|
|
if (deliveryModeFlagCount > 1) {
|
|
throw new Error("Choose at most one of --announce, --no-deliver, or --webhook.");
|
|
}
|
|
const triggerScriptPath = normalizeOptionalString(opts.triggerScript);
|
|
if (typeof opts.triggerScript === "string" && !triggerScriptPath) {
|
|
throw new Error("--trigger-script must not be blank");
|
|
}
|
|
if (opts.clearTrigger && (triggerScriptPath || opts.triggerOnce)) {
|
|
throw new Error("Use --clear-trigger or trigger options, not both");
|
|
}
|
|
// Local input errors must not depend on Gateway availability, even when
|
|
// another edit field needs the existing job.
|
|
const triggerScript = triggerScriptPath
|
|
? await readCronTriggerScript(triggerScriptPath)
|
|
: undefined;
|
|
const patch: Record<string, unknown> = {};
|
|
if (typeof opts.name === "string") {
|
|
patch.name = opts.name;
|
|
}
|
|
const displayName = normalizeOptionalString(opts.displayName);
|
|
if (typeof opts.displayName === "string" && !displayName) {
|
|
throw new Error("--display-name must not be blank");
|
|
}
|
|
if (displayName && opts.clearDisplayName) {
|
|
throw new Error("Use --display-name or --clear-display-name, not both");
|
|
}
|
|
if (displayName) {
|
|
patch.displayName = displayName;
|
|
}
|
|
if (opts.clearDisplayName) {
|
|
patch.displayName = null;
|
|
}
|
|
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.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 = sessionTarget;
|
|
}
|
|
if (typeof opts.wake === "string") {
|
|
const wakeMode = opts.wake.trim();
|
|
if (wakeMode !== "now" && wakeMode !== "next-heartbeat") {
|
|
throw new Error("--wake must be now or next-heartbeat");
|
|
}
|
|
patch.wakeMode = wakeMode;
|
|
}
|
|
const agentId = normalizeOptionalString(opts.agent);
|
|
if (typeof opts.agent === "string" && !agentId) {
|
|
throw new Error("--agent must not be blank");
|
|
}
|
|
if (agentId && opts.clearAgent) {
|
|
throw new Error("Use --agent or --clear-agent, not both");
|
|
}
|
|
if (agentId) {
|
|
patch.agentId = sanitizeAgentId(agentId);
|
|
}
|
|
if (opts.clearAgent) {
|
|
patch.agentId = null;
|
|
}
|
|
const sessionKey = normalizeOptionalString(opts.sessionKey);
|
|
if (typeof opts.sessionKey === "string" && !sessionKey) {
|
|
throw new Error("--session-key must not be blank");
|
|
}
|
|
if (sessionKey && opts.clearSessionKey) {
|
|
throw new Error("Use --session-key or --clear-session-key, not both");
|
|
}
|
|
if (sessionKey) {
|
|
patch.sessionKey = sessionKey;
|
|
}
|
|
if (opts.clearSessionKey) {
|
|
patch.sessionKey = null;
|
|
}
|
|
|
|
const pacingMin = normalizeOptionalString(opts.pacingMin);
|
|
const pacingMax = normalizeOptionalString(opts.pacingMax);
|
|
const hasPacingMin = typeof opts.pacingMin === "string";
|
|
const hasPacingMax = typeof opts.pacingMax === "string";
|
|
if (hasPacingMin && !pacingMin) {
|
|
throw new Error("--pacing-min must not be blank");
|
|
}
|
|
if (hasPacingMax && !pacingMax) {
|
|
throw new Error("--pacing-max must not be blank");
|
|
}
|
|
if (opts.clearPacing && (hasPacingMin || hasPacingMax)) {
|
|
throw new Error("Use --clear-pacing or pacing bounds, not both");
|
|
}
|
|
if (opts.clearPacing) {
|
|
patch.pacing = null;
|
|
} else if (hasPacingMin || hasPacingMax) {
|
|
const existing = await readExistingCronJob();
|
|
patch.pacing = {
|
|
...existing.pacing,
|
|
...(pacingMin ? { min: pacingMin } : {}),
|
|
...(pacingMax ? { max: pacingMax } : {}),
|
|
};
|
|
}
|
|
if (opts.clearTrigger) {
|
|
patch.trigger = null;
|
|
} else if (triggerScript !== undefined) {
|
|
const existing = await readExistingCronJob();
|
|
patch.trigger = {
|
|
...existing.trigger,
|
|
script: triggerScript,
|
|
...(opts.triggerOnce ? { once: true } : {}),
|
|
};
|
|
} else if (opts.triggerOnce) {
|
|
const existing = await readExistingCronJob();
|
|
if (!existing.trigger) {
|
|
throw new Error("--trigger-once requires an existing trigger or --trigger-script");
|
|
}
|
|
patch.trigger = { ...existing.trigger, once: true };
|
|
}
|
|
|
|
const scheduleRequest = resolveCronEditScheduleRequest({
|
|
at: opts.at,
|
|
cron: opts.cron,
|
|
every: opts.every,
|
|
streamCommand: opts.streamCommand,
|
|
streamCwd: opts.streamCwd,
|
|
streamMode: opts.streamMode,
|
|
streamMatch: opts.streamMatch,
|
|
streamBatchMs: opts.streamBatchMs,
|
|
streamMaxBatchBytes: opts.streamMaxBatchBytes,
|
|
exact: opts.exact,
|
|
stagger: opts.stagger,
|
|
tz: opts.tz,
|
|
});
|
|
if (scheduleRequest.kind === "direct") {
|
|
if (scheduleRequest.schedule.kind === "stream") {
|
|
const existing = await readExistingCronJob();
|
|
if (existing.schedule.kind === "stream") {
|
|
const metadataRequest = resolveCronEditScheduleRequest({
|
|
streamCwd: opts.streamCwd,
|
|
streamMode: opts.streamMode,
|
|
streamMatch: opts.streamMatch,
|
|
streamBatchMs: opts.streamBatchMs,
|
|
streamMaxBatchBytes: opts.streamMaxBatchBytes,
|
|
});
|
|
const merged =
|
|
metadataRequest.kind === "patch-existing-stream"
|
|
? applyExistingStreamSchedulePatch(existing.schedule, metadataRequest)
|
|
: existing.schedule;
|
|
patch.schedule = { ...merged, command: scheduleRequest.schedule.command };
|
|
} else {
|
|
validateStreamScheduleMetadata(scheduleRequest.schedule);
|
|
patch.schedule = scheduleRequest.schedule;
|
|
}
|
|
} else if (
|
|
scheduleRequest.schedule.kind === "cron" &&
|
|
scheduleRequest.schedule.tz === undefined
|
|
) {
|
|
const existing = await readExistingCronJob();
|
|
patch.schedule =
|
|
existing.schedule.kind === "cron" && existing.schedule.tz !== undefined
|
|
? { ...scheduleRequest.schedule, tz: existing.schedule.tz }
|
|
: scheduleRequest.schedule;
|
|
} else {
|
|
patch.schedule = scheduleRequest.schedule;
|
|
}
|
|
} else if (scheduleRequest.kind === "patch-existing-cron") {
|
|
const existing = await readExistingCronJob();
|
|
patch.schedule = applyExistingCronSchedulePatch(existing.schedule, scheduleRequest);
|
|
} else if (scheduleRequest.kind === "patch-existing-stream") {
|
|
const existing = await readExistingCronJob();
|
|
patch.schedule = applyExistingStreamSchedulePatch(existing.schedule, scheduleRequest);
|
|
}
|
|
|
|
Object.assign(
|
|
patch,
|
|
await resolveCronEditPayloadDeliveryPatch(opts, readExistingCronJob),
|
|
);
|
|
|
|
const hasFailureAlertAfter = typeof opts.failureAlertAfter === "string";
|
|
const hasFailureAlertChannel = typeof opts.failureAlertChannel === "string";
|
|
const hasFailureAlertTo = typeof opts.failureAlertTo === "string";
|
|
const hasFailureAlertCooldown = typeof opts.failureAlertCooldown === "string";
|
|
const hasFailureAlertIncludeSkipped =
|
|
typeof opts.failureAlertIncludeSkipped === "boolean";
|
|
const hasFailureAlertExcludeSkipped =
|
|
typeof opts.failureAlertExcludeSkipped === "boolean";
|
|
const hasFailureAlertMode = typeof opts.failureAlertMode === "string";
|
|
const hasFailureAlertAccountId = typeof opts.failureAlertAccountId === "string";
|
|
if (hasFailureAlertIncludeSkipped && hasFailureAlertExcludeSkipped) {
|
|
throw new Error(
|
|
"Use either --failure-alert-include-skipped or --failure-alert-exclude-skipped.",
|
|
);
|
|
}
|
|
const hasFailureAlertFields =
|
|
hasFailureAlertAfter ||
|
|
hasFailureAlertChannel ||
|
|
hasFailureAlertTo ||
|
|
hasFailureAlertCooldown ||
|
|
hasFailureAlertIncludeSkipped ||
|
|
hasFailureAlertExcludeSkipped ||
|
|
hasFailureAlertMode ||
|
|
hasFailureAlertAccountId;
|
|
const failureAlertFlag =
|
|
typeof opts.failureAlert === "boolean" ? opts.failureAlert : undefined;
|
|
if (failureAlertFlag === false && hasFailureAlertFields) {
|
|
throw new Error("Use --no-failure-alert alone (without failure-alert-* options).");
|
|
}
|
|
if (failureAlertFlag === false) {
|
|
patch.failureAlert = false;
|
|
} else if (failureAlertFlag === true || hasFailureAlertFields) {
|
|
const failureAlert: Record<string, unknown> = {};
|
|
if (hasFailureAlertAfter) {
|
|
const after = parseStrictPositiveInteger(opts.failureAlertAfter);
|
|
if (after === undefined) {
|
|
throw new Error("Invalid --failure-alert-after (must be a positive integer).");
|
|
}
|
|
failureAlert.after = after;
|
|
}
|
|
if (hasFailureAlertChannel) {
|
|
failureAlert.channel = normalizeOptionalLowercaseString(opts.failureAlertChannel);
|
|
}
|
|
if (hasFailureAlertTo) {
|
|
const to = normalizeOptionalString(opts.failureAlertTo) ?? "";
|
|
failureAlert.to = to ? to : undefined;
|
|
}
|
|
if (hasFailureAlertCooldown) {
|
|
let cooldownMs: number;
|
|
try {
|
|
cooldownMs = parseDurationMs(String(opts.failureAlertCooldown));
|
|
} catch {
|
|
throw new Error("Invalid --failure-alert-cooldown.");
|
|
}
|
|
failureAlert.cooldownMs = cooldownMs;
|
|
}
|
|
if (hasFailureAlertIncludeSkipped || hasFailureAlertExcludeSkipped) {
|
|
failureAlert.includeSkipped = hasFailureAlertIncludeSkipped;
|
|
}
|
|
if (hasFailureAlertMode) {
|
|
const mode = normalizeOptionalLowercaseString(opts.failureAlertMode);
|
|
if (mode !== "announce" && mode !== "webhook") {
|
|
throw new Error("Invalid --failure-alert-mode (must be 'announce' or 'webhook').");
|
|
}
|
|
failureAlert.mode = mode;
|
|
}
|
|
if (hasFailureAlertAccountId) {
|
|
const accountId = normalizeOptionalString(opts.failureAlertAccountId) ?? "";
|
|
failureAlert.accountId = accountId ? accountId : undefined;
|
|
}
|
|
patch.failureAlert = failureAlert;
|
|
}
|
|
|
|
const res = await callGatewayFromCli("cron.update", opts, {
|
|
id,
|
|
patch,
|
|
...(expectedConfigRevision !== undefined ? { expectedConfigRevision } : {}),
|
|
});
|
|
defaultRuntime.writeJson(res);
|
|
await warnIfCronSchedulerDisabled(opts);
|
|
} catch (err) {
|
|
defaultRuntime.error(danger(formatErrorMessage(err)));
|
|
defaultRuntime.exit(1);
|
|
}
|
|
}),
|
|
);
|
|
}
|