Files
penguin-harness/packages/cli/src/goal-command.ts
T

38 lines
1.7 KiB
TypeScript

/**
* Goal-command parsing (pure logic, shared by chat's `/goal` and run's `--goal`, unit-tested).
*
* Chat syntax: `/goal[:<budget>] <objective>` — the optional budget rides on the command
* token (`/goal:500k Raise coverage to 80%`); omitting it means no budget. Run passes the
* budget value (or `true` for a bare `--goal`), so only `parseTokenBudget` applies there.
*/
import { UNLIMITED_BUDGET } from "@prismshadow/penguin-core";
/**
* Parses a budget token: a positive number with an optional `k` / `m` suffix
* (`500k` = 500_000, `1.5m` = 1_500_000, `123456` literal). Returns null when invalid.
*/
export function parseTokenBudget(text: string): number | null {
const m = /^(\d+(?:\.\d+)?)([km])?$/i.exec(text.trim());
if (!m) return null;
const scale = m[2]?.toLowerCase() === "m" ? 1_000_000 : m[2]?.toLowerCase() === "k" ? 1_000 : 1;
const value = Math.round(Number(m[1]) * scale);
return value > 0 ? value : null;
}
export type GoalCommandResult =
| { ok: true; budget: number; objective: string }
| { ok: false; reason: "usage" }
| { ok: false; reason: "budget"; value: string };
/** Parses a full `/goal…` chat line (the caller has already matched the `/goal` prefix). */
export function parseGoalCommand(line: string): GoalCommandResult {
const m = /^\/goal(?::(\S+))?(?:\s+([\s\S]+))?$/.exec(line.trim());
if (!m) return { ok: false, reason: "usage" };
const rest = m[2]?.trim() ?? "";
if (!rest) return { ok: false, reason: "usage" };
if (m[1] === undefined) return { ok: true, budget: UNLIMITED_BUDGET, objective: rest };
const budget = parseTokenBudget(m[1]);
if (budget === null) return { ok: false, reason: "budget", value: m[1] };
return { ok: true, budget, objective: rest };
}