From bbcd33c9df7a0c11737b89f4f138a0ba12740985 Mon Sep 17 00:00:00 2001 From: tupe12334 Date: Thu, 11 Jun 2026 21:22:07 +0300 Subject: [PATCH] feat: add --version flag and steplock init subcommand `steplock --version` (and -V) prints the binary version from Cargo.toml. `steplock init` creates .steplock/checklists/ and .steplock/.gitignore in the current directory; both are documented in Installation.md but were previously unimplemented. Co-Authored-By: Claude Sonnet 4.6 --- core/src/bin/main.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/core/src/bin/main.rs b/core/src/bin/main.rs index 0616acd..42c1b79 100644 --- a/core/src/bin/main.rs +++ b/core/src/bin/main.rs @@ -1,4 +1,5 @@ use std::env; +use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; use std::process; @@ -6,6 +7,23 @@ use std::process; use steplock_core::{run, HookEvent, HookResponse}; fn main() { + let args: Vec = env::args().collect(); + + match args.get(1).map(String::as_str) { + Some("--version") | Some("-V") => { + println!("steplock {}", env!("CARGO_PKG_VERSION")); + return; + } + Some("init") => { + if let Err(e) = run_init(&env::current_dir().unwrap()) { + eprintln!("steplock: init failed: {e}"); + process::exit(1); + } + return; + } + _ => {} + } + let repo_root = find_repo_root_from(&env::current_dir().unwrap()) .unwrap_or_else(|| env::current_dir().unwrap()); @@ -23,6 +41,24 @@ fn main() { } } +/// Create `.steplock/checklists/` and a `.steplock/.gitignore` in `dir`. +fn run_init(dir: &Path) -> std::io::Result<()> { + let checklists_dir = dir.join(".steplock").join("checklists"); + if checklists_dir.exists() { + println!("steplock: .steplock/checklists/ already exists"); + return Ok(()); + } + fs::create_dir_all(&checklists_dir)?; + fs::write( + dir.join(".steplock").join(".gitignore"), + "sessions/\naudit.log\n", + )?; + println!("steplock: initialized .steplock/checklists/"); + println!("Next: create a checklist in .steplock/checklists//"); + println!(" with config.toml and flow.mmd."); + Ok(()) +} + /// Parse the hook event from `reader`, run the gate, and return the polyhook response. /// Returns `Err(message)` when input is unreadable or the gate engine fails. fn run_app(mut reader: impl Read, repo_root: &Path) -> Result { @@ -202,4 +238,22 @@ reset = "session" let result = find_repo_root_from(tmp.path()); assert!(result.is_none()); } + + #[test] + fn init_creates_checklists_dir_and_gitignore() { + let tmp = TempDir::new().unwrap(); + run_init(tmp.path()).unwrap(); + assert!(tmp.path().join(".steplock/checklists").is_dir()); + let gitignore = fs::read_to_string(tmp.path().join(".steplock/.gitignore")).unwrap(); + assert!(gitignore.contains("sessions/")); + assert!(gitignore.contains("audit.log")); + } + + #[test] + fn init_is_idempotent_when_checklists_exists() { + let tmp = TempDir::new().unwrap(); + fs::create_dir_all(tmp.path().join(".steplock/checklists")).unwrap(); + // Second call should not error + run_init(tmp.path()).unwrap(); + } }