import { parseDuration } from "./duration.js"; import type { LoopPolicy, LoopStartOptions, LoopUpdatePatch } from "./domain.js"; export type LoopCommand = | { command: "start"; options: LoopStartOptions } | { command: "update"; patch: LoopUpdatePatch } | { command: "status" | "pause" | "resume" | "stop" }; const VALUE_FLAGS = new Set(["every", "after", "for", "max-iterations", "min-delay", "max-delay"]); function positiveInteger(value: string, flag: string): number { if (!/^\d+$/.test(value) || Number(value) < 1 || !Number.isSafeInteger(Number(value))) throw new Error(`--${flag} exige inteiro positivo.`); return Number(value); } function tokensWithOffsets(input: string): Array<{ value: string; start: number; end: number }> { return [...input.matchAll(/\S+/g)].map((match) => ({ value: match[0], start: match.index!, end: match.index! + match[0].length })); } function parseFlags(input: string, allowObjective: boolean): { values: Map; objective: string } { const tokens = tokensWithOffsets(input); const values = new Map(); let objectiveStart = input.length; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (!token.value.startsWith("--")) { objectiveStart = token.start; break; } const name = token.value.slice(2); if (!VALUE_FLAGS.has(name)) throw new Error(`Opção desconhecida: --${name}`); if (values.has(name)) throw new Error(`Opção repetida: --${name}`); const next = tokens[++i]; if (!next || next.value.startsWith("--")) throw new Error(`--${name} exige valor.`); values.set(name, next.value); } const objective = input.slice(objectiveStart).trim(); if (!allowObjective && objective) throw new Error("Este subcomando não aceita objetivo."); return { values, objective }; } function duration(values: Map, name: string): number | undefined { const value = values.get(name); return value === undefined ? undefined : parseDuration(value); } export function parseLoopCommand(raw: string): LoopCommand { const input = raw.trim(); const firstSpace = input.search(/\s/); const command = (firstSpace < 0 ? input : input.slice(0, firstSpace)) || "status"; const rest = firstSpace < 0 ? "" : input.slice(firstSpace).trimStart(); if (["status", "pause", "resume", "stop"].includes(command)) { if (rest) throw new Error(`/${command} não aceita argumentos.`); return { command: command as "status" | "pause" | "resume" | "stop" }; } if (command !== "start" && command !== "update") throw new Error(`Subcomando desconhecido: ${command}`); const { values, objective } = parseFlags(rest, command === "start"); const everyRaw = values.get("every"); const minDelayMs = duration(values, "min-delay"); const maxDelayMs = duration(values, "max-delay"); if ((minDelayMs ?? 60_000) > (maxDelayMs ?? 86_400_000)) throw new Error("--min-delay não pode exceder --max-delay."); const every: LoopPolicy | undefined = everyRaw === undefined ? undefined : everyRaw === "auto" ? { kind: "auto" } : { kind: "fixed", delayMs: parseDuration(everyRaw) }; const rangeMin = minDelayMs ?? 60_000; const rangeMax = maxDelayMs ?? 86_400_000; if (command === "start" && every?.kind === "fixed" && (every.delayMs < rangeMin || every.delayMs > rangeMax)) throw new Error("--every está fora da faixa min/max."); if (command === "update" && every?.kind === "fixed" && ((minDelayMs !== undefined && every.delayMs < minDelayMs) || (maxDelayMs !== undefined && every.delayMs > maxDelayMs))) throw new Error("--every está fora da faixa min/max informada."); const maxIterations = values.has("max-iterations") ? positiveInteger(values.get("max-iterations")!, "max-iterations") : undefined; if (command === "start") { if (!every) throw new Error("start exige --every ."); if (!objective) throw new Error("start exige um objetivo."); return { command, options: { objective, every, afterMs: duration(values, "after") ?? 0, forMs: duration(values, "for") ?? 86_400_000, maxIterations: maxIterations ?? 50, minDelayMs: rangeMin, maxDelayMs: rangeMax } }; } if (values.has("after")) throw new Error("update não aceita --after."); const patch: LoopUpdatePatch = {}; if (every) patch.every = every; if (values.has("for")) patch.forMs = duration(values, "for"); if (maxIterations !== undefined) patch.maxIterations = maxIterations; if (minDelayMs !== undefined) patch.minDelayMs = minDelayMs; if (maxDelayMs !== undefined) patch.maxDelayMs = maxDelayMs; if (Object.keys(patch).length === 0) throw new Error("update exige ao menos uma alteração."); return { command, patch }; }