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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Session cleanup uses correct scope key when `session_id` is empty

### Changed
- CLI argument parsing uses `clap`: adds `steplock help`, per-command `--help`, and clearer usage errors (still exit 1)
- Idempotent ack: re-acknowledging the current step is a no-op, not an error

## [0.1.0] - Initial release
Expand Down
123 changes: 122 additions & 1 deletion core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ enum_glob_use = "deny"
single_match_else = "deny"
manual_let_else = "deny"
items_after_statements = "deny"
needless_pass_by_value = "deny"

[dependencies]
cel-interpreter = "0.10"
clap = { version = "4.5", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
dirs = "6"
polyhook = "0.1.5"
Expand Down
134 changes: 70 additions & 64 deletions core/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,72 @@ use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process;

use clap::{Parser, Subcommand};
use polyhook::parse;
use steplock::{global_steplock_dir, run_with_global, HookEvent, HookResponse};

/// Extra help text shown after the generated command list.
const AFTER_HELP: &str = "\
With no command, steplock reads a hook event from stdin and responds (used by polyhook).

CHECKLIST FILES:
.steplock/checklists/<name>/config.toml Gate trigger and reset configuration
.steplock/checklists/<name>/flow.mmd Mermaid stateDiagram-v2 checklist flow

GLOBAL CHECKLISTS:
Checklists in <global>/checklists/<name>/ apply to every project. They run after
the project checklists. A project checklist with the same name replaces the global one.
<global> is $STEPLOCK_GLOBAL_DIR, else $XDG_CONFIG_HOME/steplock, else
~/.config/steplock. Set STEPLOCK_GLOBAL_DIR=\"\" to turn global checklists off.

For more information: https://github.com/polyhook/steplock";

/// Stateful quality gate for AI coding agents.
#[derive(Debug, Parser)]
#[command(name = "steplock", version, after_help = AFTER_HELP)]
struct Cli {
/// Command to run. Omit it to handle a hook event from stdin.
#[command(subcommand)]
command: Option<CliCommand>,
}

/// `steplock` subcommands.
#[derive(Debug, Subcommand)]
enum CliCommand {
/// Create .steplock/checklists/ with a sample checklist in the current directory
Init {
/// Create checklists/ in the global steplock directory instead
#[arg(long)]
global: bool,
},
/// Check all project and global checklist configs for errors
Validate,
/// Remove all session state (forces checklists to restart)
Clean {
/// Remove session state in the global steplock directory instead
#[arg(long)]
global: bool,
},
}

fn main() {
let args: Vec<String> = env::args().skip(1).collect();
match args.as_slice() {
[flag] if flag == "--version" || flag == "-V" => {
println!("steplock {}", env!("CARGO_PKG_VERSION"));
}
[flag] if flag == "--help" || flag == "-h" => {
print_help();
}
[cmd] if cmd == "init" => {
let cli = Cli::try_parse().unwrap_or_else(|e| {
// Help and version go to stdout and exit 0; usage errors exit 1 (not clap's 2,
// which steplock reserves for hook failures).
let code = i32::from(e.use_stderr());
let _: io::Result<()> = e.print();
process::exit(code);
});
match cli.command {
None => run_hook(),
Some(CliCommand::Init { global: false }) => {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if let Err(e) = run_init(&cwd) {
eprintln!("steplock: init failed: {e}");
process::exit(1);
}
exit_on_error("init", run_init(&cwd));
}
[cmd, flag] if cmd == "init" && flag == "--global" => {
if let Err(e) = init_steplock_dir(&require_global_dir(), false) {
eprintln!("steplock: init failed: {e}");
process::exit(1);
}
Some(CliCommand::Init { global: true }) => {
exit_on_error("init", init_steplock_dir(&require_global_dir(), false));
}
[cmd] if cmd == "validate" => {
Some(CliCommand::Validate) => {
let dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let root = find_repo_root_from(&dir).unwrap_or(dir);
match run_validate(&root, global_steplock_dir().as_deref()) {
Expand All @@ -44,56 +84,22 @@ fn main() {
}
}
}
[cmd] if cmd == "clean" => {
if let Err(e) = run_clean(&env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) {
eprintln!("steplock: clean failed: {e}");
process::exit(1);
}
}
[cmd, flag] if cmd == "clean" && flag == "--global" => {
if let Err(e) = clean_sessions(&require_global_dir()) {
eprintln!("steplock: clean failed: {e}");
process::exit(1);
}
Some(CliCommand::Clean { global: false }) => {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
exit_on_error("clean", run_clean(&cwd));
}
[] => run_hook(),
_ => {
eprintln!("steplock: unknown arguments");
eprintln!("Run 'steplock --help' for usage.");
process::exit(1);
Some(CliCommand::Clean { global: true }) => {
exit_on_error("clean", clean_sessions(&require_global_dir()));
}
}
}

fn print_help() {
println!(
"steplock {}

Stateful quality gate for AI coding agents.

USAGE:
steplock Read hook event from stdin and respond (used by polyhook)
steplock init Create .steplock/checklists/ in the current directory
steplock init --global Create checklists/ in the global steplock directory
steplock validate Check all project and global checklist configs for errors
steplock clean Remove all session state (forces checklists to restart)
steplock clean --global
Remove all session state in the global steplock directory
steplock --version Print version

CHECKLIST FILES:
.steplock/checklists/<name>/config.toml Gate trigger and reset configuration
.steplock/checklists/<name>/flow.mmd Mermaid stateDiagram-v2 checklist flow

GLOBAL CHECKLISTS:
Checklists in <global>/checklists/<name>/ apply to every project. They run after
the project checklists. A project checklist with the same name replaces the global one.
<global> is $STEPLOCK_GLOBAL_DIR, else $XDG_CONFIG_HOME/steplock, else
~/.config/steplock. Set STEPLOCK_GLOBAL_DIR=\"\" to turn global checklists off.

For more information: https://github.com/polyhook/steplock",
env!("CARGO_PKG_VERSION")
);
/// Print `steplock: <command> failed: <error>` and exit 1 when `result` is an error.
fn exit_on_error(command: &str, result: io::Result<()>) {
if let Err(e) = result {
eprintln!("steplock: {command} failed: {e}");
process::exit(1);
}
}

/// Validate all checklists in `.steplock/checklists/` and in the global steplock directory.
Expand Down
Loading
Loading