79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
// Pure /goal command parsing. No I/O, no pi imports.
|
|
//
|
|
// Verbs (Q1, locked with Jason 2026-08-28):
|
|
// bare -> status
|
|
// stop -> pause the loop, goal retained
|
|
// clear -> remove the goal entirely
|
|
// resume -> continue a paused goal
|
|
// <text> -> set (or replace) the active goal
|
|
// --max N / --max=N run-limit option on set (Q4); remainder is the goal text
|
|
//
|
|
// Only the exact words stop/clear/resume are verbs; anything else is goal text.
|
|
|
|
export type GoalCommand =
|
|
| { kind: "status" }
|
|
| { kind: "stop" }
|
|
| { kind: "clear" }
|
|
| { kind: "resume" }
|
|
| { kind: "set"; text: string; max?: number; waitTimeoutSeconds?: number }
|
|
| { kind: "error"; message: string };
|
|
|
|
export function parseGoalCommand(raw: string): GoalCommand {
|
|
const input = (raw ?? "").trim();
|
|
if (input === "") return { kind: "status" };
|
|
|
|
const lower = input.toLowerCase();
|
|
if (lower === "stop") return { kind: "stop" };
|
|
if (lower === "clear") return { kind: "clear" };
|
|
if (lower === "resume") return { kind: "resume" };
|
|
|
|
let max: number | undefined;
|
|
let text = input;
|
|
|
|
let waitTimeoutSeconds: number | undefined;
|
|
const waitOption = text.match(/(?:^|\s)--wait-timeout(?:=|\s+)([^\s]+)(?=\s|$)/);
|
|
if (waitOption) {
|
|
const n = Number(waitOption[1]);
|
|
if (!Number.isInteger(n) || n < 10 || n > 86400 || String(n) !== waitOption[1]) {
|
|
return { kind: "error", message: "--wait-timeout must be an integer from 10 to 86400 seconds" };
|
|
}
|
|
waitTimeoutSeconds = n;
|
|
text = text.replace(waitOption[0], " ").trim();
|
|
}
|
|
if (/(?:^|\s)--wait-timeout(?:=|\s|$)/.test(text)) {
|
|
return { kind: "error", message: "--wait-timeout requires one value from 10 to 86400 seconds" };
|
|
}
|
|
|
|
const inline = text.match(/(?:^|\s)--max=([^\s]+)(?:\s|$)/);
|
|
if (inline) {
|
|
const parsed = parseMax(inline[1]);
|
|
if (typeof parsed === "string") return { kind: "error", message: parsed };
|
|
max = parsed;
|
|
text = text.replace(inline[0], " ").trim();
|
|
} else {
|
|
const spaced = text.match(/(?:^|\s)--max\s+([^\s]+)(?:\s|$)/);
|
|
if (spaced) {
|
|
const parsed = parseMax(spaced[1]);
|
|
if (typeof parsed === "string") return { kind: "error", message: parsed };
|
|
max = parsed;
|
|
text = text.replace(spaced[0], " ").trim();
|
|
} else if (/(?:^|\s)--max(?:\s|$)/.test(text)) {
|
|
return { kind: "error", message: '--max requires a positive integer (e.g. "--max 40")' };
|
|
}
|
|
}
|
|
|
|
text = text.trim();
|
|
if (text === "") {
|
|
return { kind: "error", message: "goal text required (verbs: stop, clear, resume)" };
|
|
}
|
|
return { kind: "set", text, max, ...(waitTimeoutSeconds === undefined ? {} : { waitTimeoutSeconds }) };
|
|
}
|
|
|
|
function parseMax(raw: string): number | string {
|
|
const n = Number.parseInt(raw, 10);
|
|
if (!Number.isInteger(n) || n < 1 || String(n) !== raw) {
|
|
return `--max must be a positive integer, got "${raw}"`;
|
|
}
|
|
return n;
|
|
}
|