feat(extensions): establish canonical goal source (#54, #55)

This commit is contained in:
2026-09-06 02:32:32 -05:00
parent 44f257cb06
commit d4696d09eb
43 changed files with 6845 additions and 0 deletions
+78
View File
@@ -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;
}