From ed8533c5b155f317138d4d3b30bb64b86442e55d Mon Sep 17 00:00:00 2001 From: teyrebaz33 Date: Mon, 6 Apr 2026 20:08:39 +0300 Subject: [PATCH 1/4] feat(localnet): add --json flag to logs command --- src/cli.rs | 5 ++++- src/commands/localnet.rs | 34 +++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f389e04..95a7ea0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -158,6 +158,9 @@ struct LocalnetStatusArgs { struct LocalnetLogsArgs { #[arg(long, default_value_t = 200)] tail: usize, + /// Output logs as JSON (for CI/tooling) + #[arg(long)] + json: bool, } #[derive(Debug, clap::Args)] @@ -259,7 +262,7 @@ pub(crate) fn run(args: Vec) -> DynResult<()> { }, LocalnetSubcommand::Stop => LocalnetAction::Stop, LocalnetSubcommand::Status(args) => LocalnetAction::Status { json: args.json }, - LocalnetSubcommand::Logs(args) => LocalnetAction::Logs { tail: args.tail }, + LocalnetSubcommand::Logs(args) => LocalnetAction::Logs { tail: args.tail, json: args.json }, }; cmd_localnet(action) } diff --git a/src/commands/localnet.rs b/src/commands/localnet.rs index 3e1b45c..01c1522 100644 --- a/src/commands/localnet.rs +++ b/src/commands/localnet.rs @@ -21,7 +21,7 @@ pub(crate) enum LocalnetAction { Start { timeout_sec: u64 }, Stop, Status { json: bool }, - Logs { tail: usize }, + Logs { tail: usize, json: bool }, } pub(crate) fn cmd_localnet(action: LocalnetAction) -> DynResult<()> { @@ -77,7 +77,7 @@ fn cmd_localnet_in_project(project: &Project, action: LocalnetAction) -> DynResu LocalnetAction::Status { json } => { cmd_localnet_status(&state_path, &log_path, json, &localnet_addr, localnet_port) } - LocalnetAction::Logs { tail } => cmd_localnet_logs(&log_path, tail), + LocalnetAction::Logs { tail, json } => cmd_localnet_logs(&log_path, tail, json), } } @@ -323,24 +323,36 @@ fn ownership_label(ownership: LocalnetOwnership) -> &'static str { } } -fn cmd_localnet_logs(log_path: &Path, tail: usize) -> DynResult<()> { +fn cmd_localnet_logs(log_path: &Path, tail: usize, json: bool) -> DynResult<()> { if !log_path.exists() { - println!("log file does not exist yet: {}", log_path.display()); + if json { + println!("{}", serde_json::json!({ "tail": tail, "lines": [] })); + } else { + println!("log file does not exist yet: {}", log_path.display()); + } return Ok(()); } let content = fs::read_to_string(log_path) .with_context(|| format!("failed to read log file {}", log_path.display()))?; - if content.trim().is_empty() { - println!("log file is empty: {}", log_path.display()); - return Ok(()); - } - let lines: Vec<&str> = content.lines().collect(); let start = lines.len().saturating_sub(tail); - for line in &lines[start..] { - println!("{line}"); + let tail_lines: Vec<&str> = lines[start..].to_vec(); + + if json { + println!("{}", serde_json::to_string_pretty(&serde_json::json!({ + "tail": tail, + "lines": tail_lines, + }))?); + } else { + if tail_lines.is_empty() { + println!("log file is empty: {}", log_path.display()); + return Ok(()); + } + for line in &tail_lines { + println!("{line}"); + } } Ok(()) From 30031c9da57052b6cf4f7ecf02d13fb1eb137943 Mon Sep 17 00:00:00 2001 From: teyrebaz33 Date: Mon, 6 Apr 2026 20:14:54 +0300 Subject: [PATCH 2/4] test(localnet): add tests for localnet logs --json flag --- tests/cli.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 4a4c765..6263d3b 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1704,3 +1704,64 @@ fn respond_last_block(stream: &mut TcpStream) { let _ = stream.write_all(response.as_bytes()); let _ = stream.flush(); } + + +#[test] +fn localnet_logs_json_flag_shown_in_help() { + Command::new(assert_cmd::cargo::cargo_bin!("logos-scaffold")) + .args(["localnet", "logs", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("--json")); +} + +#[test] +fn localnet_logs_json_outputs_valid_json_when_no_log_file() { + let temp = tempdir().expect("tempdir"); + let lssa_path = temp.path().join("lssa"); + fs::create_dir_all(&lssa_path).expect("create lssa path"); + write_scaffold_toml(temp.path(), &lssa_path, "wallet-not-installed-for-tests"); + + let assert = Command::new(assert_cmd::cargo::cargo_bin!("logos-scaffold")) + .current_dir(temp.path()) + .args(["localnet", "logs", "--json"]) + .assert() + .success(); + + let stdout = String::from_utf8(assert.get_output().stdout.clone()).expect("utf8 stdout"); + let value: serde_json::Value = serde_json::from_str(&stdout).expect("valid json"); + + assert!(value.get("tail").is_some()); + assert!(value.get("lines").is_some()); + assert_eq!(value["lines"].as_array().unwrap().len(), 0); +} + +#[test] +fn localnet_logs_json_outputs_lines_array() { + let temp = tempdir().expect("tempdir"); + let lssa_path = temp.path().join("lssa"); + fs::create_dir_all(&lssa_path).expect("create lssa path"); + write_scaffold_toml(temp.path(), &lssa_path, "wallet-not-installed-for-tests"); + + fs::create_dir_all(temp.path().join(".scaffold/logs")).expect("create logs dir"); + fs::write( + temp.path().join(".scaffold/logs/sequencer.log"), + "line-one\nline-two\nline-three\n", + ) + .expect("write sequencer log"); + + let assert = Command::new(assert_cmd::cargo::cargo_bin!("logos-scaffold")) + .current_dir(temp.path()) + .args(["localnet", "logs", "--json", "--tail", "2"]) + .assert() + .success(); + + let stdout = String::from_utf8(assert.get_output().stdout.clone()).expect("utf8 stdout"); + let value: serde_json::Value = serde_json::from_str(&stdout).expect("valid json"); + + let lines = value["lines"].as_array().expect("lines array"); + assert_eq!(lines.len(), 2); + assert_eq!(lines[0].as_str().unwrap(), "line-two"); + assert_eq!(lines[1].as_str().unwrap(), "line-three"); + assert_eq!(value["tail"].as_u64().unwrap(), 2); +} From 69420eec82b7f2b53b24ebe1cd9245c6206f23e7 Mon Sep 17 00:00:00 2001 From: teyrebaz33 Date: Wed, 8 Apr 2026 13:13:26 +0300 Subject: [PATCH 3/4] refactor(localnet): extract print_log_lines helper per review feedback --- src/commands/localnet.rs | 42 +++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/commands/localnet.rs b/src/commands/localnet.rs index 01c1522..d12906f 100644 --- a/src/commands/localnet.rs +++ b/src/commands/localnet.rs @@ -323,41 +323,35 @@ fn ownership_label(ownership: LocalnetOwnership) -> &'static str { } } -fn cmd_localnet_logs(log_path: &Path, tail: usize, json: bool) -> DynResult<()> { - if !log_path.exists() { - if json { - println!("{}", serde_json::json!({ "tail": tail, "lines": [] })); - } else { - println!("log file does not exist yet: {}", log_path.display()); - } - return Ok(()); - } - - let content = fs::read_to_string(log_path) - .with_context(|| format!("failed to read log file {}", log_path.display()))?; - - let lines: Vec<&str> = content.lines().collect(); - let start = lines.len().saturating_sub(tail); - let tail_lines: Vec<&str> = lines[start..].to_vec(); - +fn print_log_lines(tail: usize, lines: &[&str], log_path: &Path, json: bool) -> DynResult<()> { if json { println!("{}", serde_json::to_string_pretty(&serde_json::json!({ "tail": tail, - "lines": tail_lines, + "lines": lines, }))?); + } else if lines.is_empty() { + println!("log file is empty: {}", log_path.display()); } else { - if tail_lines.is_empty() { - println!("log file is empty: {}", log_path.display()); - return Ok(()); - } - for line in &tail_lines { + for line in lines { println!("{line}"); } } - Ok(()) } +fn cmd_localnet_logs(log_path: &Path, tail: usize, json: bool) -> DynResult<()> { + if !log_path.exists() { + return print_log_lines(tail, &[], log_path, json); + } + + let content = fs::read_to_string(log_path) + .with_context(|| format!("failed to read log file {}", log_path.display()))?; + + let lines: Vec<&str> = content.lines().collect(); + let start = lines.len().saturating_sub(tail); + print_log_lines(tail, &lines[start..], log_path, json) +} + fn build_status_report( state_path: &Path, log_path: &Path, From e11e6871146eec4c5f9c4f637e0e77746839c3b0 Mon Sep 17 00:00:00 2001 From: teyrebaz33 Date: Sat, 11 Apr 2026 15:51:46 +0300 Subject: [PATCH 4/4] fix(localnet): restore missing-file message and blank-line filter per review --- src/commands/localnet.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/localnet.rs b/src/commands/localnet.rs index d12906f..a5f0177 100644 --- a/src/commands/localnet.rs +++ b/src/commands/localnet.rs @@ -341,15 +341,20 @@ fn print_log_lines(tail: usize, lines: &[&str], log_path: &Path, json: bool) -> fn cmd_localnet_logs(log_path: &Path, tail: usize, json: bool) -> DynResult<()> { if !log_path.exists() { - return print_log_lines(tail, &[], log_path, json); + if json { + return print_log_lines(tail, &[], log_path, true); + } + println!("log file does not exist yet: {}", log_path.display()); + return Ok(()); } let content = fs::read_to_string(log_path) .with_context(|| format!("failed to read log file {}", log_path.display()))?; let lines: Vec<&str> = content.lines().collect(); - let start = lines.len().saturating_sub(tail); - print_log_lines(tail, &lines[start..], log_path, json) + let non_empty: Vec<&str> = lines.iter().filter(|l| !l.trim().is_empty()).copied().collect(); + let start = non_empty.len().saturating_sub(tail); + print_log_lines(tail, &non_empty[start..], log_path, json) } fn build_status_report(