forked from 1b-agent/fieldcraft-upstream
CLI: fieldcraft-cli crashes [list|show|replay|dir|clear] (#107)
Lists, shows, replays and deletes the crash reports the desktop app wrote. A replay opens the report's model, runs its commands with writes confined to a scratch folder, and says whether the same internal error happens again. Adapted from SolidCraft's solidcraft-cli crashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
080ff048c6
commit
cb7e3a2711
3 files changed
+164
No files matched your search
@@ -0,0 +1,80 @@
|
||||
//! `fieldcraft-cli crashes`: the crash reports the desktop app wrote on this computer
|
||||
//! (docs/crash-reports.md): list them, show one, replay one, delete them.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use fieldcraft_engine::crash;
|
||||
use serde_json::json;
|
||||
|
||||
pub const USAGE: &str = "usage: fieldcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]";
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let dir = crash::default_dir().ok_or("no crash folder on this system (set FIELDCRAFT_CRASH_DIR)")?;
|
||||
let id = || args.get(1).map(String::as_str).ok_or_else(|| USAGE.to_string());
|
||||
match args.first().map(String::as_str) {
|
||||
None | Some("list") => {
|
||||
let reports = crash::list(&dir);
|
||||
if args.iter().any(|a| a == "--json") {
|
||||
return print_json(&json!({ "dir": dir.to_string_lossy(), "reports": reports }));
|
||||
}
|
||||
println!("crash reports in {}", dir.display());
|
||||
if reports.is_empty() {
|
||||
println!(" (none)");
|
||||
}
|
||||
for r in reports {
|
||||
let times = if r.count > 1 { format!(" ×{}", r.count) } else { String::new() };
|
||||
let cmd = r.command.map(|c| format!(" in {c}")).unwrap_or_default();
|
||||
let replay = if r.replayable { " [replayable]" } else { "" };
|
||||
println!(" {} {}{times}{cmd} v{}{replay}\n {}", r.id, r.kind, r.version, r.message);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some("show") => print_json(&crash::read(&dir, id()?)?),
|
||||
Some("replay") => replay(&dir, id()?),
|
||||
Some("dir") => {
|
||||
println!("{}", dir.display());
|
||||
Ok(())
|
||||
}
|
||||
Some("clear") => {
|
||||
println!("deleted {} report(s)", crash::clear(&dir, args.get(1).map(String::as_str)));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(v: &serde_json::Value) -> Result<(), String> {
|
||||
println!("{}", serde_json::to_string_pretty(v).map_err(|e| e.to_string())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a report's commands on its model and say whether the crash happens again. Files the
|
||||
/// commands write go to a scratch folder, never over the user's files.
|
||||
fn replay(dir: &Path, id: &str) -> Result<(), String> {
|
||||
let report = crash::read(dir, id)?;
|
||||
let scratch = std::env::temp_dir().join(format!("fieldcraft-replay-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&scratch).map_err(|e| format!("{}: {e}", scratch.display()))?;
|
||||
if report["kind"] == "unclean_exit" {
|
||||
println!("this crash ended the app without a report: if this process ends abruptly, it reproduced");
|
||||
}
|
||||
println!("replaying on {}", report["model_path"].as_str().unwrap_or("a new model"));
|
||||
let r = crash::replay(&report, &scratch);
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
let r = r?;
|
||||
for (i, (cmd, outcome)) in r.steps.iter().enumerate() {
|
||||
match outcome {
|
||||
Ok(()) => println!(" {:>3}. {cmd}: ok", i + 1),
|
||||
Err(e) => println!(" {:>3}. {cmd}: {e}", i + 1),
|
||||
}
|
||||
}
|
||||
let said = |k: &str| report[k].as_str().unwrap_or_default().to_string();
|
||||
if r.reproduced {
|
||||
println!("reproduced: the same internal error ({}) at {}", said("message"), said("location"));
|
||||
} else if let Some((l, m)) = r.panics.first() {
|
||||
println!("an internal error happened, but not the reported one: {m} at {l}");
|
||||
println!("(the report says: {} at {})", said("message"), said("location"));
|
||||
} else {
|
||||
println!("did not reproduce: no internal error (fixed, or it depends on something the replay does not have)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,10 +2,13 @@
|
||||
//! fieldcraft-cli run <script.json>
|
||||
//! fieldcraft-cli features
|
||||
//! fieldcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR] [--automation-write-root DIR]
|
||||
//! fieldcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
|
||||
//! fieldcraft-cli --version
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod crashes;
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
|
||||
@@ -28,6 +31,11 @@ usage:
|
||||
--automation-read-root DIR files may be read only inside DIR
|
||||
--automation-write-root DIR files may be written only inside DIR (with either root,
|
||||
the access a root left out is not granted)
|
||||
fieldcraft-cli crashes [list|show <id>|replay <id>|dir|clear [<id>]]
|
||||
the crash reports the desktop app wrote on this
|
||||
computer (docs/crash-reports.md): list them (--json),
|
||||
show one, replay one (does the crash happen again?),
|
||||
print their folder, delete them
|
||||
fieldcraft-cli --version print the version
|
||||
";
|
||||
|
||||
@@ -52,6 +60,7 @@ fn main() -> ExitCode {
|
||||
Ok(())
|
||||
}
|
||||
Some("mcp") => mcp(args.get(1..).unwrap_or(&[])),
|
||||
Some("crashes") => crashes::run(args.get(1..).unwrap_or(&[])),
|
||||
Some("run") => args.get(1).ok_or_else(|| format!("run needs a script\n\n{USAGE}")).and_then(|p| run(p)),
|
||||
Some("-V" | "--version") => {
|
||||
println!("fieldcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
Reference in new issue
Block a user