// MoonTrace CLI entry point
///|
fn print_help() -> Unit {
println("MoonTrace - Agent execution trace observability tool")
println("")
println("Usage: moon run src/cli [options]")
println("")
println("Commands:")
println(" list [options] List saved traces")
println(" --errors Show only traces containing errors")
println(" --name Filter by root span name")
println(" errors List only traces containing errors")
println(" ids [--errors] [--name ] Print trace IDs for scripts")
println(
" repair Rebuild index and report unreadable trace files",
)
println(" compare Compare two traces by span name")
println(
" regress [--check-output] Check spans and optionally the final answer",
)
println(" replay Replay recorded span outputs")
println(" show Show detailed trace view in terminal")
println(
" export [out] Export trace to HTML file (default: .html)",
)
println(
" export-all [dir] Export all traces to HTML files in directory",
)
println(" delete Delete a trace")
println(" help Show this help")
println("")
println("Environment:")
println(" MOONTRACE_DIR Override trace storage directory")
println(" AGENT_QUERY Override the Bailian demo question")
}
///|
fn get_store() -> @storage.JsonFileStore {
match @env.get_env_var("MOONTRACE_DIR") {
None => @storage.JsonFileStore::default()
Some(dir) => @storage.JsonFileStore::new(dir)
}
}
///|
fn cmd_list(filter : @storage.TraceFilter, ids_only? : Bool = false) -> Unit {
let store = get_store()
let summaries = @storage.query_summaries(store.list(), filter)
if ids_only {
for summary in summaries {
println(summary.trace_id)
}
} else {
println(@tui.render_trace_list(summaries))
}
match store.last_error() {
Some(message) => println("Warning: " + message)
None => ()
}
}
///|
fn cmd_repair() -> Unit {
let report = get_store().repair()
println(
"Recovered \{report.trace_count} traces; index rebuilt: \{report.index_rebuilt}",
)
for issue in report.issues {
println("Warning: " + issue)
}
if !report.index_rebuilt || !report.issues.is_empty() {
@sys.exit(1)
}
}
///|
fn cmd_list_args(args : Array[String], ids_only? : Bool = false) -> Unit {
let mut errors = false
let mut name : String? = None
let mut i = 2
while i < args.length() {
match args[i] {
"--errors" => errors = true
"--name" =>
if i + 1 < args.length() {
name = Some(args[i + 1])
i = i + 1
} else {
println("Error: '--name' requires a value")
return
}
value => {
println("Error: unknown list option: " + value)
return
}
}
i = i + 1
}
let filter = match (errors, name) {
(true, Some(value)) => @storage.TraceFilter::with_name_and_errors(value)
(true, None) => @storage.TraceFilter::error_only()
(false, Some(value)) => @storage.TraceFilter::with_name(value)
(false, None) => @storage.TraceFilter::new()
}
cmd_list(filter, ids_only~)
}
///|
fn cmd_show(trace_id : String) -> Unit {
let store = get_store()
match store.load(trace_id) {
None =>
println(
"Error: " + store.last_error().unwrap_or("trace not found: " + trace_id),
)
Some(trace) => println(@tui.render_trace_detail(trace))
}
}
///|
fn trace_duration(trace : @trace.Trace) -> Int64 {
trace.duration_ms().unwrap_or(0)
}
///|
fn trace_error_count(trace : @trace.Trace) -> Int {
let mut count = 0
for span in trace.spans.values() {
if status_label(span.status) == "error" {
count = count + 1
}
}
count
}
///|
fn status_label(status : @trace.SpanStatus) -> String {
match status {
@trace.Ok => "ok"
@trace.Error => "error"
@trace.Running => "running"
@trace.Skipped => "skipped"
}
}
///|
fn find_span_by_name(trace : @trace.Trace, name : String) -> @trace.Span? {
for span in trace.spans.values() {
if span.name == name {
return Some(span)
}
}
None
}
///|
fn cmd_compare(first_id : String, second_id : String) -> Unit {
let store = get_store()
let first = store.load(first_id)
let second = store.load(second_id)
match (first, second) {
(Some(a), Some(b)) => {
println("=== MoonTrace Trace Comparison ===")
println("A: " + a.id + " B: " + b.id)
println(
"Elapsed duration: " +
"\{trace_duration(a)}ms -> \{trace_duration(b)}ms",
)
println("Span count: " + "\{a.spans.length()} -> \{b.spans.length()}")
println(
"Error count: " + "\{trace_error_count(a)} -> \{trace_error_count(b)}",
)
println("")
println("Span changes:")
let seen : Map[String, Bool] = Map([])
for span in a.spans.values() {
seen[span.name] = true
match find_span_by_name(b, span.name) {
None => println("- " + span.name + ": removed")
Some(other) => {
let first_duration = span.duration_ms.unwrap_or(0)
let second_duration = other.duration_ms.unwrap_or(0)
let delta = second_duration - first_duration
let status_change = if status_label(span.status) ==
status_label(other.status) {
status_label(span.status)
} else {
status_label(span.status) + " -> " + status_label(other.status)
}
println(
"- " +
span.name +
": " +
"\{first_duration}ms -> \{second_duration}ms (delta \{delta}ms, " +
status_change +
")",
)
}
}
}
for span in b.spans.values() {
if !seen.contains(span.name) {
println("- " + span.name + ": added")
}
}
}
(None, _) => println("Error: trace not found: " + first_id)
(_, None) => println("Error: trace not found: " + second_id)
}
}
///|
fn print_replay_result(name : String, result : @replay.ReplayResult) -> Unit {
match result {
@replay.Output(value) =>
println(
"- " + name + ": output replayed (" + "\{value.length()}" + " chars)",
)
@replay.Error(message) =>
println("- " + name + ": recorded error: " + message)
@replay.Missing => println("- " + name + ": missing from recording")
}
}
///|
fn cmd_replay(trace_id : String) -> Unit {
let store = get_store()
match store.load(trace_id) {
None =>
println(
"Error: " + store.last_error().unwrap_or("trace not found: " + trace_id),
)
Some(trace) => {
let session = @replay.ReplaySession::from_trace(trace)
println("=== MoonTrace Replay: " + trace_id + " ===")
println(
"Recorded spans are returned locally; external tools are not called.",
)
for span in trace.spans.values() {
print_replay_result(span.name, session.replay(span.name))
}
println("Remaining recorded calls: " + "\{session.remaining()}")
}
}
}
///|
fn print_regression_issue(issue : @replay.RegressionIssue) -> Unit {
match issue {
@replay.MissingSpan(name) => println("- missing span: " + name)
@replay.AddedSpan(name) => println("- added span: " + name)
@replay.StatusChanged(name, before, after) =>
println(
"- status changed: " + name + " (" + before + " -> " + after + ")",
)
@replay.RootOutputChanged(name) => println("- root output changed: " + name)
}
}
///|
fn cmd_regress(
baseline_id : String,
candidate_id : String,
check_output : Bool,
) -> Unit {
let store = get_store()
match (store.load(baseline_id), store.load(candidate_id)) {
(Some(baseline), Some(candidate)) => {
let report = @replay.compare_traces_with_options(
baseline, candidate, check_output,
)
println("=== MoonTrace Regression Check ===")
println("Baseline: " + baseline_id + " Candidate: " + candidate_id)
if report.passed() {
println(
if check_output {
"PASS: no span, status, or root output regressions detected"
} else {
"PASS: no span or status regressions detected"
},
)
} else {
println("FAIL: \{report.issue_count()} regression issue(s)")
for issue in report.issues {
print_regression_issue(issue)
}
@sys.exit(1)
}
}
(None, _) => {
println("Error: trace not found: " + baseline_id)
@sys.exit(2)
}
(_, None) => {
println("Error: trace not found: " + candidate_id)
@sys.exit(2)
}
}
}
///|
fn cmd_export(trace_id : String, out_path : String?) -> Unit {
let store = get_store()
match store.load(trace_id) {
None => {
println(
"Error: " + store.last_error().unwrap_or("trace not found: " + trace_id),
)
@sys.exit(2)
}
Some(trace) => {
let html = @exporter.export_trace_html(trace)
let path = match out_path {
None => trace_id + ".html"
Some(p) => p
}
try {
@fs.write_string_to_file(path, html)
println("Exported trace " + trace_id + " to " + path)
} catch {
_ => println("Error: failed to write file " + path)
}
}
}
}
///|
fn cmd_export_all(dir : String?) -> Unit {
let store = get_store()
let summaries = store.list()
let out_dir = match dir {
None => "moontrace_export"
Some(d) => d
}
// Create output directory
if !@fs.path_exists(out_dir) {
@fs.create_dir(out_dir) catch {
_ => println("Warning: failed to export a trace")
}
}
let mut count = 0
for s in summaries {
match store.load(s.trace_id) {
None => ()
Some(trace) => {
let html = @exporter.export_trace_html(trace)
let path = out_dir + "/" + s.trace_id + ".html"
try {
@fs.write_string_to_file(path, html)
count = count + 1
} catch {
_ => ()
}
}
}
}
// Also export index
let index_html = @exporter.export_index_html(summaries)
@fs.write_string_to_file(out_dir + "/index.html", index_html) catch {
_ => println("Warning: failed to export index")
}
println("Exported " + "\{count}" + " traces to " + out_dir + "/")
}
///|
fn cmd_delete(trace_id : String) -> Unit {
let store = get_store()
if store.delete(trace_id) {
println("Deleted trace: " + trace_id)
} else {
println("Error: trace not found: " + trace_id)
}
}
///|
fn main {
let args = @env.args()
// args[0] = program path, args[1] = command
if args.length() < 2 {
cmd_list(@storage.TraceFilter::new())
return
}
let cmd = args[1]
match cmd {
"list" | "ls" => cmd_list_args(args)
"errors" => cmd_list(@storage.TraceFilter::error_only())
"show" =>
if args.length() < 3 {
println("Error: 'show' requires a trace ID")
} else {
cmd_show(args[2])
}
"ids" => cmd_list_args(args, ids_only=true)
"repair" => cmd_repair()
"compare" =>
if args.length() < 4 {
println("Error: 'compare' requires two trace IDs")
} else {
cmd_compare(args[2], args[3])
}
"replay" =>
if args.length() < 3 {
println("Error: 'replay' requires a trace ID")
} else {
cmd_replay(args[2])
}
"regress" =>
if args.length() < 4 {
println("Error: 'regress' requires a baseline and candidate trace ID")
@sys.exit(2)
} else if args.length() == 4 {
cmd_regress(args[2], args[3], false)
} else if args.length() == 5 && args[4] == "--check-output" {
cmd_regress(args[2], args[3], true)
} else {
println("Error: expected 'regress [--check-output]'")
@sys.exit(2)
}
"export" =>
if args.length() < 3 {
println("Error: 'export' requires a trace ID")
} else {
let out = if args.length() >= 4 { Some(args[3]) } else { None }
cmd_export(args[2], out)
}
"export-all" => {
let dir = if args.length() >= 3 { Some(args[2]) } else { None }
cmd_export_all(dir)
}
"delete" | "rm" =>
if args.length() < 3 {
println("Error: 'delete' requires a trace ID")
} else {
cmd_delete(args[2])
}
"help" | "--help" | "-h" => print_help()
_ => {
println("Unknown command: " + cmd)
println("Use 'moon run src/cli help' for usage")
}
}
}