From c18047ee1b859ced2208943505612e4beedfc05d Mon Sep 17 00:00:00 2001 From: thefourCraft Date: Mon, 4 May 2026 09:59:57 +0300 Subject: [PATCH 1/3] feat: implement barzel init --force to overwrite existing .barzel.toml - Add --force flag to 'barzel init' CLI command - Add force: bool field to StdioRequest for {"command":"init"} - run_init now returns InitOutcome (Created/Skipped/Overwritten) - Existing config is skipped by default; overwritten only when force=true - stdio init response includes config_status field with the outcome - README updated with --force example and stdio force field --- README.md | 5 +++- src/cli.rs | 4 ++++ src/init.rs | 64 +++++++++++++++++++++++++++++++++++++--------------- src/main.rs | 4 ++-- src/stdio.rs | 55 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 109 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 61aa8c5..7150b06 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Language matrix: Rust, TypeScript, Python, Go. `semgrep` runs on all languages. ## Human CLI ```bash -barzel init . # write .barzel.toml with defaults +barzel init . # write .barzel.toml with defaults (skips if already exists) +barzel init . --force # overwrite existing .barzel.toml barzel check # show tool availability and required installs barzel run # run all layers, human output barzel run --layer logic,hostile # run specific layers @@ -57,6 +58,7 @@ Send a single JSON object to stdin; barzel writes newline-delimited JSON to stdo ```bash echo '{"command":"init","project_path":"."}' | barzel --stdio +echo '{"command":"init","project_path":".","force":true}' | barzel --stdio echo '{"command":"check","project_path":"."}' | barzel --stdio echo '{"command":"run"}' | barzel --stdio echo '{"command":"run","layers":["logic"],"fail_fast":true}' | barzel --stdio @@ -83,6 +85,7 @@ echo '{"command":"history","limit":10,"package_path":"crates/api"}' | barzel --s | `limit` | int | 20 | max history entries (cap 200, 0 returns empty) | | `package_path` | string | — | filter history by workspace member path | | `language` | string | — | filter history by language | +| `force` | bool | false | overwrite existing `.barzel.toml` (for `init` command) | ### Response envelope diff --git a/src/cli.rs b/src/cli.rs index b45ea2c..d960ece 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -20,6 +20,10 @@ pub enum Commands { Init { /// Target directory (defaults to current directory) path: Option, + + /// Overwrite an existing .barzel.toml + #[arg(long)] + force: bool, }, /// Run the full verification suite (or specific layers) diff --git a/src/init.rs b/src/init.rs index a852fe8..9483890 100644 --- a/src/init.rs +++ b/src/init.rs @@ -4,7 +4,15 @@ use crate::error::Result; use owo_colors::OwoColorize; use std::path::Path; -pub fn run_init(target: Option<&Path>, stdio: bool) -> Result<()> { +/// Result of running init, for structured callers (e.g. stdio response). +#[derive(Debug, PartialEq)] +pub enum InitOutcome { + Created, + Skipped, + Overwritten, +} + +pub fn run_init(target: Option<&Path>, stdio: bool, force: bool) -> Result { let target_path = target.unwrap_or_else(|| Path::new(".")); let info = detect_project(target_path)?; @@ -20,25 +28,32 @@ pub fn run_init(target: Option<&Path>, stdio: bool) -> Result<()> { let config = BarzelConfig::from_project_info(&info); let config_path = target_path.join(".barzel.toml"); - if config_path.exists() { + if config_path.exists() && !force { if !stdio { println!( "{} .barzel.toml already exists — skipping (use --force to overwrite)", "⚠".bright_yellow() ); } - return Ok(()); + return Ok(InitOutcome::Skipped); } + let outcome = if config_path.exists() { InitOutcome::Overwritten } else { InitOutcome::Created }; + config.save(&config_path)?; let barzel_dir = target_path.join(".barzel"); std::fs::create_dir_all(&barzel_dir)?; if !stdio { + let action = match outcome { + InitOutcome::Overwritten => "Overwrote", + _ => "Created", + }; println!( - "{} Created {}", + "{} {} {}", "✓".bright_green(), + action, ".barzel.toml".bright_cyan() ); println!( @@ -52,7 +67,7 @@ pub fn run_init(target: Option<&Path>, stdio: bool) -> Result<()> { println!(" {} barzel run --layer logic", "→".bright_blue()); } - Ok(()) + Ok(outcome) } #[cfg(test)] @@ -65,37 +80,50 @@ mod tests { fn init_creates_barzel_toml() { let dir = tempdir().unwrap(); fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"test\"").unwrap(); - run_init(Some(dir.path()), true).unwrap(); + let outcome = run_init(Some(dir.path()), true, false).unwrap(); assert!(dir.path().join(".barzel.toml").exists()); + assert_eq!(outcome, InitOutcome::Created); } #[test] fn init_creates_barzel_directory() { let dir = tempdir().unwrap(); fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"test\"").unwrap(); - run_init(Some(dir.path()), true).unwrap(); + run_init(Some(dir.path()), true, false).unwrap(); assert!(dir.path().join(".barzel").exists()); } #[test] - fn init_skips_if_toml_already_exists() { + fn init_skips_if_toml_already_exists_without_force() { let dir = tempdir().unwrap(); fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"test\"").unwrap(); - // First init - run_init(Some(dir.path()), true).unwrap(); + run_init(Some(dir.path()), true, false).unwrap(); // Overwrite .barzel.toml with sentinel content fs::write(dir.path().join(".barzel.toml"), b"# sentinel").unwrap(); - // Second init should skip (not overwrite) - run_init(Some(dir.path()), true).unwrap(); + // Second init without --force should skip + let outcome = run_init(Some(dir.path()), true, false).unwrap(); + let content = fs::read_to_string(dir.path().join(".barzel.toml")).unwrap(); + assert!(content.contains("sentinel"), "sentinel must be preserved when skipping"); + assert_eq!(outcome, InitOutcome::Skipped); + } + + #[test] + fn init_force_overwrites_existing_toml() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"test\"").unwrap(); + run_init(Some(dir.path()), true, false).unwrap(); + // Write sentinel + fs::write(dir.path().join(".barzel.toml"), b"# sentinel").unwrap(); + // Force overwrite + let outcome = run_init(Some(dir.path()), true, true).unwrap(); let content = fs::read_to_string(dir.path().join(".barzel.toml")).unwrap(); - assert!(content.contains("sentinel")); + assert!(!content.contains("sentinel"), "sentinel must be gone after force overwrite"); + assert_eq!(outcome, InitOutcome::Overwritten); } #[test] fn init_uses_current_dir_when_no_path() { - // This just checks it doesn't panic/error when path is None - // (it will use `.` which exists) - let result = run_init(None, true); + let result = run_init(None, true, false); assert!(result.is_ok()); } @@ -103,7 +131,7 @@ mod tests { fn init_works_for_typescript_project() { let dir = tempdir().unwrap(); fs::write(dir.path().join("package.json"), br#"{"name":"my-app"}"#).unwrap(); - run_init(Some(dir.path()), true).unwrap(); + run_init(Some(dir.path()), true, false).unwrap(); assert!(dir.path().join(".barzel.toml").exists()); let content = fs::read_to_string(dir.path().join(".barzel.toml")).unwrap(); assert!(content.contains("typescript") || content.contains("my-app")); @@ -113,7 +141,7 @@ mod tests { fn init_toml_contains_project_name() { let dir = tempdir().unwrap(); fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"my-crate\"").unwrap(); - run_init(Some(dir.path()), true).unwrap(); + run_init(Some(dir.path()), true, false).unwrap(); let content = fs::read_to_string(dir.path().join(".barzel.toml")).unwrap(); assert!(content.contains("my-crate")); } diff --git a/src/main.rs b/src/main.rs index db48c69..9721851 100644 --- a/src/main.rs +++ b/src/main.rs @@ -425,8 +425,8 @@ fn main() -> ExitCode { }; match command { - Commands::Init { path } => match init::run_init(path.as_deref(), false) { - Ok(()) => ExitCode::SUCCESS, + Commands::Init { path, force } => match init::run_init(path.as_deref(), false, force) { + Ok(_) => ExitCode::SUCCESS, Err(e) => { eprintln!("{} {}", "Error:".bright_red(), e); ExitCode::from(1) diff --git a/src/stdio.rs b/src/stdio.rs index 2bed52e..14e0d67 100644 --- a/src/stdio.rs +++ b/src/stdio.rs @@ -38,6 +38,9 @@ pub(crate) struct StdioRequest { pub(crate) package_path: Option, #[serde(default)] pub(crate) language: Option, + /// Overwrite existing .barzel.toml when running the `init` command. + #[serde(default)] + pub(crate) force: bool, } #[derive(serde::Serialize)] @@ -329,8 +332,14 @@ pub(crate) fn handle_stdio() -> ExitCode { "init" => { let path = req.project_path.as_deref().map(Path::new); let target = path.unwrap_or_else(|| Path::new(".")); - match crate::init::run_init(Some(target), true) { - Ok(()) => { + match crate::init::run_init(Some(target), true, req.force) { + Ok(outcome) => { + use crate::init::InitOutcome; + let config_status = match outcome { + InitOutcome::Created => "created", + InitOutcome::Skipped => "skipped", + InitOutcome::Overwritten => "overwritten", + }; let project = crate::detect::detect_project(target).ok(); let resp = create_response( "success", @@ -338,6 +347,7 @@ pub(crate) fn handle_stdio() -> ExitCode { Some(serde_json::json!({ "message": "project initialized", "config_file": ".barzel.toml", + "config_status": config_status, "language": project.as_ref().map(|p| p.language.to_string()), "frameworks": project.as_ref().map(|p| serde_json::json!({ "is_nextjs": p.frameworks.is_nextjs, @@ -1170,6 +1180,47 @@ mod tests { assert_eq!(entries[0]["language"].as_str(), Some("rust")); } + // ── stdio init / force flag ─────────────────────────────────────────────── + + #[test] + fn stdio_request_parses_force_true() { + let json = r#"{"command":"init","force":true}"#; + let req: StdioRequest = serde_json::from_str(json).unwrap(); + assert!(req.force, "force:true must be parsed from request"); + } + + #[test] + fn stdio_request_force_defaults_to_false() { + let json = r#"{"command":"init"}"#; + let req: StdioRequest = serde_json::from_str(json).unwrap(); + assert!(!req.force, "force must default to false when absent"); + } + + #[test] + fn init_build_run_data_config_status_skipped() { + use crate::init::{InitOutcome, run_init}; + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"x\"").unwrap(); + // First init creates the file + run_init(Some(dir.path()), true, false).unwrap(); + // Second init without force returns Skipped + let outcome = run_init(Some(dir.path()), true, false).unwrap(); + assert_eq!(outcome, InitOutcome::Skipped); + } + + #[test] + fn init_force_returns_overwritten_outcome() { + use crate::init::{InitOutcome, run_init}; + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"x\"").unwrap(); + run_init(Some(dir.path()), true, false).unwrap(); + std::fs::write(dir.path().join(".barzel.toml"), b"# sentinel").unwrap(); + let outcome = run_init(Some(dir.path()), true, true).unwrap(); + assert_eq!(outcome, InitOutcome::Overwritten); + let content = std::fs::read_to_string(dir.path().join(".barzel.toml")).unwrap(); + assert!(!content.contains("sentinel"), "force must overwrite sentinel content"); + } + // ── tool registry / check payload ───────────────────────────────────────── #[test] From 28c9614bb8444f029e5b26463898470b016c0bd2 Mon Sep 17 00:00:00 2001 From: thefourCraft Date: Mon, 4 May 2026 10:01:55 +0300 Subject: [PATCH 2/3] fix: conditional init message and document config_status in README - init response message is now outcome-conditional: created -> 'project initialized' skipped -> 'config already exists' overwritten -> 'config overwritten' - README documents config_status table so agents know the three values - Test verifies message/status mapping is stable --- README.md | 10 ++++++++++ src/stdio.rs | 29 ++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7150b06..1ac90be 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,16 @@ echo '{"command":"history","limit":10,"package_path":"crates/api"}' | barzel --s | `language` | string | — | filter history by language | | `force` | bool | false | overwrite existing `.barzel.toml` (for `init` command) | +### Init response `data` fields + +The `init` success response always includes `config_status`, which agents must check to know whether the config was actually written: + +| `config_status` | `message` | Meaning | +|-----------------|-----------|---------| +| `"created"` | `"project initialized"` | `.barzel.toml` was written for the first time | +| `"skipped"` | `"config already exists"` | `.barzel.toml` already existed; no changes made | +| `"overwritten"` | `"config overwritten"` | `.barzel.toml` was replaced because `force: true` | + ### Response envelope Every JSON line — progress events and the final response — shares this envelope: diff --git a/src/stdio.rs b/src/stdio.rs index 14e0d67..50c1df0 100644 --- a/src/stdio.rs +++ b/src/stdio.rs @@ -335,17 +335,17 @@ pub(crate) fn handle_stdio() -> ExitCode { match crate::init::run_init(Some(target), true, req.force) { Ok(outcome) => { use crate::init::InitOutcome; - let config_status = match outcome { - InitOutcome::Created => "created", - InitOutcome::Skipped => "skipped", - InitOutcome::Overwritten => "overwritten", + let (config_status, message) = match outcome { + InitOutcome::Created => ("created", "project initialized"), + InitOutcome::Skipped => ("skipped", "config already exists"), + InitOutcome::Overwritten => ("overwritten", "config overwritten"), }; let project = crate::detect::detect_project(target).ok(); let resp = create_response( "success", request_id, Some(serde_json::json!({ - "message": "project initialized", + "message": message, "config_file": ".barzel.toml", "config_status": config_status, "language": project.as_ref().map(|p| p.language.to_string()), @@ -1221,6 +1221,25 @@ mod tests { assert!(!content.contains("sentinel"), "force must overwrite sentinel content"); } + #[test] + fn init_config_status_and_message_mapping() { + use crate::init::InitOutcome; + // Verify the (config_status, message) pairs agents rely on + for (outcome, expected_status, expected_msg) in [ + (InitOutcome::Created, "created", "project initialized"), + (InitOutcome::Skipped, "skipped", "config already exists"), + (InitOutcome::Overwritten, "overwritten", "config overwritten"), + ] { + let (status, msg) = match outcome { + InitOutcome::Created => ("created", "project initialized"), + InitOutcome::Skipped => ("skipped", "config already exists"), + InitOutcome::Overwritten => ("overwritten", "config overwritten"), + }; + assert_eq!(status, expected_status, "config_status must match for {:?}", expected_status); + assert_eq!(msg, expected_msg, "message must match for {:?}", expected_msg); + } + } + // ── tool registry / check payload ───────────────────────────────────────── #[test] From a55e65703ac58ee46fef05dae2b91f615fd06ca4 Mon Sep 17 00:00:00 2001 From: thefourCraft Date: Mon, 4 May 2026 10:03:03 +0300 Subject: [PATCH 3/3] refactor: extract init_status_and_message helper; test calls production code Test now calls the same init_status_and_message() used by handle_stdio instead of duplicating the match, so it guards the actual stdio contract. --- src/stdio.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/stdio.rs b/src/stdio.rs index 50c1df0..ef8431b 100644 --- a/src/stdio.rs +++ b/src/stdio.rs @@ -309,6 +309,15 @@ pub(crate) fn build_stdio_report_payload( Ok(serde_json::json!({ "report": report_json })) } +fn init_status_and_message(outcome: &crate::init::InitOutcome) -> (&'static str, &'static str) { + use crate::init::InitOutcome; + match outcome { + InitOutcome::Created => ("created", "project initialized"), + InitOutcome::Skipped => ("skipped", "config already exists"), + InitOutcome::Overwritten => ("overwritten", "config overwritten"), + } +} + // ── Command dispatcher ──────────────────────────────────────────────────────── pub(crate) fn handle_stdio() -> ExitCode { @@ -334,12 +343,7 @@ pub(crate) fn handle_stdio() -> ExitCode { let target = path.unwrap_or_else(|| Path::new(".")); match crate::init::run_init(Some(target), true, req.force) { Ok(outcome) => { - use crate::init::InitOutcome; - let (config_status, message) = match outcome { - InitOutcome::Created => ("created", "project initialized"), - InitOutcome::Skipped => ("skipped", "config already exists"), - InitOutcome::Overwritten => ("overwritten", "config overwritten"), - }; + let (config_status, message) = init_status_and_message(&outcome); let project = crate::detect::detect_project(target).ok(); let resp = create_response( "success", @@ -1224,19 +1228,16 @@ mod tests { #[test] fn init_config_status_and_message_mapping() { use crate::init::InitOutcome; - // Verify the (config_status, message) pairs agents rely on - for (outcome, expected_status, expected_msg) in [ + // Call the production helper used by handle_stdio so the test guards the actual contract. + let cases = [ (InitOutcome::Created, "created", "project initialized"), (InitOutcome::Skipped, "skipped", "config already exists"), - (InitOutcome::Overwritten, "overwritten", "config overwritten"), - ] { - let (status, msg) = match outcome { - InitOutcome::Created => ("created", "project initialized"), - InitOutcome::Skipped => ("skipped", "config already exists"), - InitOutcome::Overwritten => ("overwritten", "config overwritten"), - }; - assert_eq!(status, expected_status, "config_status must match for {:?}", expected_status); - assert_eq!(msg, expected_msg, "message must match for {:?}", expected_msg); + (InitOutcome::Overwritten, "overwritten", "config overwritten"), + ]; + for (outcome, expected_status, expected_msg) in cases { + let (status, msg) = init_status_and_message(&outcome); + assert_eq!(status, expected_status, "config_status must be '{expected_status}'"); + assert_eq!(msg, expected_msg, "message must be '{expected_msg}'"); } }