feat: Forth-style prefix command parser

Replace JSON command syntax with prefix notation: `word arg1 arg2`.
Multiple commands per line separated by `.`. Coordinate pairs use
`Category/Item`. Quoted strings for multi-word values. set-cell
uses value-first: `set-cell 100 Region/East Measure/Revenue`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-04-03 22:14:37 -07:00
parent 6647be30fa
commit 567ca341f7
3 changed files with 417 additions and 16 deletions

View File

@ -320,21 +320,23 @@ fn run_headless_commands(cmds: &[String], file: &Option<PathBuf>) -> Result<()>
let mut model = get_initial_model(file)?;
let mut exit_code = 0;
for raw_cmd in cmds {
let parsed: command::Command = match serde_json::from_str(raw_cmd) {
Ok(c) => c,
for line in cmds {
let parsed = match command::parse_line(line) {
Ok(cmds) => cmds,
Err(e) => {
let r = CommandResult::err(format!("JSON parse error: {e}"));
let r = CommandResult::err(format!("Parse error: {e}"));
println!("{}", serde_json::to_string(&r)?);
exit_code = 1;
continue;
}
};
let result = command::dispatch(&mut model, &parsed);
if !result.ok {
exit_code = 1;
for cmd in &parsed {
let result = command::dispatch(&mut model, cmd);
if !result.ok {
exit_code = 1;
}
println!("{}", serde_json::to_string(&result)?);
}
println!("{}", serde_json::to_string(&result)?);
}
if let Some(path) = file {
@ -346,14 +348,8 @@ fn run_headless_commands(cmds: &[String], file: &Option<PathBuf>) -> Result<()>
fn run_headless_script(script_path: &PathBuf, file: &Option<PathBuf>) -> Result<()> {
let content = std::fs::read_to_string(script_path)?;
let cmds: Vec<String> = content
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with("//") && !l.starts_with('#'))
.map(String::from)
.collect();
run_headless_commands(&cmds, file)
let lines: Vec<String> = content.lines().map(String::from).collect();
run_headless_commands(&lines, file)
}
// ── Helpers ──────────────────────────────────────────────────────────────────