1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::adopt::plan::AdoptMode;
use crate::cmd;
#[derive(Debug, Parser)]
#[command(
name = "klasp",
version,
about = "Block AI coding agents on the same quality gates your humans hit.",
long_about = None,
)]
pub struct Cli {
#[command(subcommand)]
pub command: Cmd,
}
#[derive(Debug, Subcommand)]
pub enum Cmd {
/// Scaffold a klasp.toml in the current repo.
Init(InitArgs),
/// Install klasp's gate hook into the detected agent surfaces.
Install(InstallArgs),
/// Remove klasp's gate hook from the detected agent surfaces.
Uninstall(UninstallArgs),
/// Gate runtime — invoked by the generated hook script with tool-call JSON on stdin.
Gate(GateArgs),
/// Diagnose the local install (config, hook script, schema version).
Doctor(DoctorArgs),
/// Manage klasp plugins.
Plugins(PluginsArgs),
/// One-command first-run setup: detect gates, narrow agents, write config, install, doctor.
Setup(SetupArgs),
}
#[derive(Debug, Args)]
pub struct InitArgs {
/// Overwrite an existing klasp.toml without prompting.
#[arg(long)]
pub force: bool,
/// Detect existing quality gates (pre-commit, Husky, Lefthook,
/// lint-staged, plain `.git/hooks`) and propose a `klasp.toml`
/// that mirrors them. Combine with `--mode` to control the
/// destructiveness. See klasp-dev/klasp#97.
#[arg(long)]
pub adopt: bool,
/// Adoption destructiveness. `inspect` prints the plan only.
/// `mirror` writes a klasp.toml that mirrors detected gates.
/// `chain` integrates klasp into existing hook managers (v1: not
/// supported; rejected with an explanatory message). Implies `--adopt`.
#[arg(long, value_enum, default_value_t = AdoptModeArg::Inspect)]
pub mode: AdoptModeArg,
}
/// Clap-visible adoption mode. Controls how destructive `--adopt` is.
///
/// Maps to [`crate::adopt::plan::AdoptMode`] at dispatch time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum AdoptModeArg {
/// Print the detected plan only; never touch the filesystem.
Inspect,
/// Write or update `klasp.toml` to mirror detected gates.
/// Never modifies existing hook files.
Mirror,
/// Integrate klasp into existing hook managers (v1: not yet supported).
Chain,
}
impl From<AdoptModeArg> for AdoptMode {
fn from(arg: AdoptModeArg) -> Self {
match arg {
AdoptModeArg::Inspect => AdoptMode::Inspect,
AdoptModeArg::Mirror => AdoptMode::Mirror,
AdoptModeArg::Chain => AdoptMode::Chain,
}
}
}
#[derive(Debug, Args)]
pub struct InstallArgs {
/// Restrict installation to one agent (`claude_code`, `codex`, `aider`) or
/// pass `all` to install every surface listed in `klasp.toml`'s
/// `[gate].agents`. Omit to install all auto-detected surfaces.
#[arg(long)]
pub agent: Option<String>,
/// Print what would be written without touching the filesystem.
#[arg(long)]
pub dry_run: bool,
/// Install even when the agent surface is not auto-detected, or overwrite
/// a non-managed hook file.
#[arg(long)]
pub force: bool,
/// Override the repo root (defaults to `git rev-parse --show-toplevel`).
#[arg(long)]
pub repo_root: Option<PathBuf>,
}
#[derive(Debug, Args)]
pub struct UninstallArgs {
/// Restrict removal to one agent (`claude_code`, `codex`, `aider`) or pass
/// `all` to uninstall every surface listed in `klasp.toml`'s
/// `[gate].agents`. Omit to walk every registered surface.
#[arg(long)]
pub agent: Option<String>,
/// Print what would be removed without touching the filesystem.
#[arg(long)]
pub dry_run: bool,
/// Override the repo root.
#[arg(long)]
pub repo_root: Option<PathBuf>,
}
/// Output format for `klasp gate`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum OutputFormat {
/// Human-readable terminal text written to stderr (default).
#[default]
Terminal,
/// JUnit XML (Surefire schema) written to stdout or `--output`.
Junit,
/// SARIF 2.1.0 JSON written to stdout or `--output`.
Sarif,
/// Stable JSON output (KLASP_OUTPUT_SCHEMA = 1) written to stdout or `--output`.
Json,
}
#[derive(Debug, Args)]
pub struct GateArgs {
/// Output format. Default is human-readable terminal text.
#[arg(long, value_enum, default_value_t = OutputFormat::Terminal)]
pub format: OutputFormat,
/// Write formatter output to this path. Defaults to stdout for `junit`/`sarif`,
/// stderr for `terminal`.
#[arg(long)]
pub output: Option<PathBuf>,
/// Informational: the agent surface that invoked the gate. Klasp resolves
/// the effective agent from `KLASP_AGENT_ID` and the tool-input payload;
/// this flag is accepted-and-ignored so installed hooks (Codex git-hooks,
/// Aider commit-cmd-pre) can pass it without clap rejecting them.
/// Future versions may use this as a hint for trigger classification.
#[arg(long)]
pub agent: Option<String>,
/// Informational: the trigger kind (`commit` or `push`). Same accept-and-
/// ignore semantics as `--agent`. Klasp resolves the trigger from the
/// tool-input payload's `hook_event_name`.
#[arg(long)]
pub trigger: Option<String>,
}
#[derive(Debug, Args)]
pub struct DoctorArgs {}
#[derive(Debug, Args)]
pub struct SetupArgs {
/// Walk through detected gates and agents with y/n prompts before
/// writing klasp.toml and running install.
#[arg(long)]
pub interactive: bool,
/// Print the detection plan and computed config without writing any files.
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct PluginsArgs {
#[command(subcommand)]
pub action: PluginsAction,
}
#[derive(Debug, Subcommand)]
pub enum PluginsAction {
/// List klasp-plugin-* binaries found on $PATH.
List,
/// Print the plugin's --describe output (protocol version, capabilities).
Info {
/// Plugin name (without the `klasp-plugin-` prefix).
name: String,
},
/// Add a plugin to the per-user disable list.
///
/// Disabled plugins are skipped silently during `klasp gate`. The disable
/// list is stored at $KLASP_DISABLED_PLUGINS_FILE or
/// ~/.config/klasp/disabled-plugins.toml.
Disable {
/// Plugin name (without the `klasp-plugin-` prefix).
name: String,
},
}
pub fn run() -> ExitCode {
let cli = Cli::parse();
match &cli.command {
Cmd::Init(args) => cmd::init::run(args),
Cmd::Install(args) => cmd::install::run(args),
Cmd::Uninstall(args) => cmd::uninstall::run(args),
Cmd::Gate(args) => cmd::gate::run(args),
Cmd::Doctor(args) => cmd::doctor::run(args),
Cmd::Plugins(args) => cmd::plugins::run(&args.action),
Cmd::Setup(args) => cmd::setup::run(args),
}
}