@@ -0,0 +1,78 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user