Spaces:
Configuration error
Configuration error
| import { listChannelPlugins } from "../../channels/plugins/index.js"; | |
| import { parseAbsoluteTimeMs } from "../../cron/parse.js"; | |
| import type { CronJob, CronSchedule } from "../../cron/types.js"; | |
| import { defaultRuntime } from "../../runtime.js"; | |
| import { colorize, isRich, theme } from "../../terminal/theme.js"; | |
| import type { GatewayRpcOpts } from "../gateway-rpc.js"; | |
| import { callGatewayFromCli } from "../gateway-rpc.js"; | |
| export const getCronChannelOptions = () => | |
| ["last", ...listChannelPlugins().map((plugin) => plugin.id)].join("|"); | |
| export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) { | |
| try { | |
| const res = (await callGatewayFromCli("cron.status", opts, {})) as { | |
| enabled?: boolean; | |
| storePath?: string; | |
| }; | |
| if (res?.enabled === true) return; | |
| const store = typeof res?.storePath === "string" ? res.storePath : ""; | |
| defaultRuntime.error( | |
| [ | |
| "warning: cron scheduler is disabled in the Gateway; jobs are saved but will not run automatically.", | |
| "Re-enable with `cron.enabled: true` (or remove `cron.enabled: false`) and restart the Gateway.", | |
| store ? `store: ${store}` : "", | |
| ] | |
| .filter(Boolean) | |
| .join("\n"), | |
| ); | |
| } catch { | |
| // Ignore status failures (older gateway, offline, etc.) | |
| } | |
| } | |
| export function parseDurationMs(input: string): number | null { | |
| const raw = input.trim(); | |
| if (!raw) return null; | |
| const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i); | |
| if (!match) return null; | |
| const n = Number.parseFloat(match[1] ?? ""); | |
| if (!Number.isFinite(n) || n <= 0) return null; | |
| const unit = (match[2] ?? "").toLowerCase(); | |
| const factor = | |
| unit === "ms" | |
| ? 1 | |
| : unit === "s" | |
| ? 1000 | |
| : unit === "m" | |
| ? 60_000 | |
| : unit === "h" | |
| ? 3_600_000 | |
| : 86_400_000; | |
| return Math.floor(n * factor); | |
| } | |
| export function parseAtMs(input: string): number | null { | |
| const raw = input.trim(); | |
| if (!raw) return null; | |
| const absolute = parseAbsoluteTimeMs(raw); | |
| if (absolute) return absolute; | |
| const dur = parseDurationMs(raw); | |
| if (dur) return Date.now() + dur; | |
| return null; | |
| } | |
| const CRON_ID_PAD = 36; | |
| const CRON_NAME_PAD = 24; | |
| const CRON_SCHEDULE_PAD = 32; | |
| const CRON_NEXT_PAD = 10; | |
| const CRON_LAST_PAD = 10; | |
| const CRON_STATUS_PAD = 9; | |
| const CRON_TARGET_PAD = 9; | |
| 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); | |
| return `${value.slice(0, width - 3)}...`; | |
| }; | |
| const formatIsoMinute = (ms: number) => { | |
| const d = new Date(ms); | |
| 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`; | |
| 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`; | |
| return `${Math.round(ms / 86_400_000)}d`; | |
| }; | |
| const formatRelative = (ms: number | null | undefined, nowMs: number) => { | |
| 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)}`; | |
| 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"; | |
| return job.state.lastStatus ?? "idle"; | |
| }; | |
| export function printCronList(jobs: CronJob[], runtime = defaultRuntime) { | |
| if (jobs.length === 0) { | |
| runtime.log("No cron jobs."); | |
| return; | |
| } | |
| const rich = isRich(); | |
| const header = [ | |
| pad("ID", CRON_ID_PAD), | |
| pad("Name", CRON_NAME_PAD), | |
| pad("Schedule", CRON_SCHEDULE_PAD), | |
| pad("Next", CRON_NEXT_PAD), | |
| pad("Last", CRON_LAST_PAD), | |
| pad("Status", CRON_STATUS_PAD), | |
| pad("Target", CRON_TARGET_PAD), | |
| pad("Agent", CRON_AGENT_PAD), | |
| ].join(" "); | |
| runtime.log(rich ? theme.heading(header) : header); | |
| const now = Date.now(); | |
| for (const job of jobs) { | |
| const idLabel = pad(job.id, CRON_ID_PAD); | |
| const nameLabel = pad(truncate(job.name, CRON_NAME_PAD), CRON_NAME_PAD); | |
| const scheduleLabel = pad( | |
| truncate(formatSchedule(job.schedule), CRON_SCHEDULE_PAD), | |
| CRON_SCHEDULE_PAD, | |
| ); | |
| const nextLabel = pad( | |
| job.enabled ? formatRelative(job.state.nextRunAtMs, now) : "-", | |
| CRON_NEXT_PAD, | |
| ); | |
| const lastLabel = pad(formatRelative(job.state.lastRunAtMs, now), CRON_LAST_PAD); | |
| const statusRaw = formatStatus(job); | |
| const statusLabel = pad(statusRaw, CRON_STATUS_PAD); | |
| const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD); | |
| 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); | |
| return colorize(rich, theme.muted, statusLabel); | |
| })(); | |
| const coloredTarget = | |
| job.sessionTarget === "isolated" | |
| ? colorize(rich, theme.accentBright, targetLabel) | |
| : colorize(rich, theme.accent, targetLabel); | |
| const coloredAgent = job.agentId | |
| ? colorize(rich, theme.info, agentLabel) | |
| : colorize(rich, theme.muted, agentLabel); | |
| const line = [ | |
| colorize(rich, theme.accent, idLabel), | |
| colorize(rich, theme.info, nameLabel), | |
| colorize(rich, theme.info, scheduleLabel), | |
| colorize(rich, theme.muted, nextLabel), | |
| colorize(rich, theme.muted, lastLabel), | |
| coloredStatus, | |
| coloredTarget, | |
| coloredAgent, | |
| ].join(" "); | |
| runtime.log(line.trimEnd()); | |
| } | |
| } | |