kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
159 lines (148 loc) • 5.82 kB
text/typescript
/**
* # cli/args — argv parsing + global-flag extraction (Kestrel CLI v1 §2)
*
* `parseArgs` reads leading positionals, then `--flag value` pairs, against a per-verb known set:
* `booleanFlags` are consumed value-lessly, `valueFlags` each take the following token as a value.
* A `--flag` in **neither** set is an UNKNOWN flag and a loud {@link CliError} (exit 2) **regardless
* of position** — mid-argv or trailing — so an agent-authored command line with a typo (e.g.
* `--r-usdd` for `--r-usd`) can never be silently swallowed and masquerade as an intentional run
* (AGENTS.md fail-closed non-negotiable; kestrel-gbv). Anything else malformed is likewise exit 2.
* `extractGlobals` peels the render flags (`--json`/`--format`/`--agent`/`--color`/`--no-color`/
* `-h`/`--help`/`-V`/`--version`) off the raw argv **before** the router runs, so every command sees
* a clean argv (with only its own flags) and the resolver sees the global flags.
*/
import type { GlobalFlags } from "./context.ts";
import { CliError, EXIT } from "./errors.ts";
function usageErr(message: string): CliError {
return new CliError({ code: "USAGE", exit: EXIT.USAGE, message });
}
/** A parsed argv: leading positionals, then `--flag value` pairs, plus consumed boolean flags. */
export interface ParsedArgs {
readonly positionals: readonly string[];
readonly flags: Map<string, string>;
readonly bools: ReadonlySet<string>;
}
/**
* Positionals-then-flags, against a per-verb known set. `booleanFlags` are value-less; `valueFlags`
* each consume the following token. A `--flag` in neither set is UNKNOWN → loud USAGE error, checked
* **before** value consumption so it fails identically mid-argv or trailing (§6, fail-closed). A
* recognized value flag with no following token is the distinct "missing value" USAGE error.
*/
export function parseArgs(
argv: readonly string[],
booleanFlags: ReadonlySet<string> = new Set(),
valueFlags: ReadonlySet<string> = new Set(),
): ParsedArgs {
const positionals: string[] = [];
const flags = new Map<string, string>();
const bools = new Set<string>();
let i = 0;
while (i < argv.length && !argv[i]!.startsWith("--")) {
positionals.push(argv[i]!);
i += 1;
}
for (; i < argv.length; i += 1) {
const key = argv[i]!;
if (!key.startsWith("--")) {
throw usageErr(`bad argument ${JSON.stringify(key)} — expected a --flag here`);
}
const name = key.slice(2);
if (booleanFlags.has(name)) {
bools.add(name);
continue;
}
// Reject unknown flags uniformly, BEFORE any value is consumed — so a typo fails closed the same
// way whether or not a token follows it (kestrel-gbv), never swallowing the next token silently.
if (!valueFlags.has(name)) throw usageErr(`unknown flag --${name}`);
const val = argv[i + 1];
if (val === undefined) throw usageErr(`missing value for --${name}`);
flags.set(name, val);
i += 1;
}
return { positionals, flags, bools };
}
/** Required flag or a loud USAGE error. */
export function required(flags: Map<string, string>, name: string): string {
const v = flags.get(name);
if (v === undefined) throw usageErr(`missing required flag --${name}`);
return v;
}
/** The result of stripping the global render flags off a raw argv. */
export interface GlobalScan {
readonly globals: GlobalFlags;
/** The remaining argv with global flags removed (command + its own args). */
readonly rest: string[];
/** True if `-h`/`--help`/`help` requested (fast-path before the router). */
readonly wantHelp: boolean;
/** True if `-V`/`--version`/`version` requested (fast-path before the router). */
readonly wantVersion: boolean;
}
/**
* Peel the global render + meta flags off a raw argv. Recognizes `--json`, `--format <v>`,
* `--agent`, `--color <v>`, `--no-color`, `-h`/`--help`, `-V`/`--version`. Everything else passes
* through in `rest` in order. An unknown `--flag` is **not** rejected here — it is left in `rest`
* for the command's own parser to accept or reject (so `--fromat` errors at the command, not
* silently swallowed). Fast-path meta words (`help`, `version`) as the FIRST token are detected.
*/
export function extractGlobals(argv: readonly string[]): GlobalScan {
const rest: string[] = [];
const globals: {
json?: boolean;
format?: string;
agent?: boolean;
color?: string;
noColor?: boolean;
api?: string;
} = {};
let wantHelp = false;
let wantVersion = false;
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i]!;
switch (a) {
case "--json":
globals.json = true;
break;
case "--agent":
globals.agent = true;
break;
case "--no-color":
globals.noColor = true;
break;
case "-h":
case "--help":
wantHelp = true;
break;
case "-V":
case "--version":
wantVersion = true;
break;
case "--format": {
const v = argv[i + 1];
if (v === undefined) throw usageErr("missing value for --format");
globals.format = v;
i += 1;
break;
}
case "--color": {
const v = argv[i + 1];
if (v === undefined) throw usageErr("missing value for --color");
globals.color = v;
i += 1;
break;
}
case "--api": {
const v = argv[i + 1];
if (v === undefined) throw usageErr("missing value for --api");
globals.api = v;
i += 1;
break;
}
default:
rest.push(a);
}
}
// Bare `help`/`version` as the leading command word are meta fast-paths too.
if (rest[0] === "help") wantHelp = true;
if (rest[0] === "version") wantVersion = true;
return { globals, rest, wantHelp, wantVersion };
}