Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -83,6 +85,17 @@ 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) |

### 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

Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub enum Commands {
Init {
/// Target directory (defaults to current directory)
path: Option<PathBuf>,

/// Overwrite an existing .barzel.toml
#[arg(long)]
force: bool,
},

/// Run the full verification suite (or specific layers)
Expand Down
64 changes: 46 additions & 18 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InitOutcome> {
let target_path = target.unwrap_or_else(|| Path::new("."));
let info = detect_project(target_path)?;

Expand All @@ -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!(
Expand All @@ -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)]
Expand All @@ -65,45 +80,58 @@ 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());
}

#[test]
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"));
Expand All @@ -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"));
}
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
77 changes: 74 additions & 3 deletions src/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub(crate) struct StdioRequest {
pub(crate) package_path: Option<String>,
#[serde(default)]
pub(crate) language: Option<String>,
/// Overwrite existing .barzel.toml when running the `init` command.
#[serde(default)]
pub(crate) force: bool,
}

#[derive(serde::Serialize)]
Expand Down Expand Up @@ -306,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 {
Expand All @@ -329,15 +341,17 @@ 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) => {
let (config_status, message) = init_status_and_message(&outcome);
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()),
"frameworks": project.as_ref().map(|p| serde_json::json!({
"is_nextjs": p.frameworks.is_nextjs,
Expand Down Expand Up @@ -1170,6 +1184,63 @@ 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");
}

#[test]
fn init_config_status_and_message_mapping() {
use crate::init::InitOutcome;
// 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"),
];
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}'");
}
}

// ── tool registry / check payload ─────────────────────────────────────────

#[test]
Expand Down
Loading