Skip to content

Commit 217c35f

Browse files
v0.1 W2: ClaudeCodeSurface install/uninstall + settings.json merge
Implements W2 of the v0.1 milestone (#2). Per design.md §3.1, §5, §7: - klasp-agents-claude::hook_template — three-line bash shim with the `# klasp:managed` marker and `KLASP_GATE_SCHEMA=N` export, parameterised by schema version so the contract test (W3) can verify the binary and fixture stay in lockstep. - klasp-agents-claude::settings — surgical serde_json-based merge that preserves every sibling key in `.claude/settings.json`, idempotent on exact `command` match. Inverse `unmerge_hook_entry` cleans up empty matchers so install→uninstall round-trips a fresh repo to `{}`. - klasp-agents-claude::surface — `ClaudeCodeSurface` impl of `klasp_core::AgentSurface`. Marker-comment idempotency check, atomic writes via tempfile, chmod 0o755 (Unix). `MarkerConflict` unless `--force` when an existing hook lacks our marker. - klasp/src/registry — `SurfaceRegistry` with `ClaudeCodeSurface` pre-registered. Lives in the binary crate so klasp-core stays free of per-agent dependencies (relevant for the v0.3 plugin model). - klasp/src/cmd/install + uninstall — CLI wiring per design §5: filter surfaces by `--agent` and `detect()`, build `InstallContext`, dispatch. Tests: - 19 unit tests in settings.rs (empty-input, sibling preservation, idempotency, malformed JSON, shape errors, uninstall round-trip) - 3 integration tests against a realistic fallow-shaped fixture (proves fallow's hook entry survives byte-for-byte) - 8 integration tests in klasp/tests/install_claude_code.rs (install, re-install no-op, dry-run, sibling preservation, marker-conflict refusal, --force overwrite, uninstall, uninstall dry-run) - 2 insta snapshot tests of the rendered hook script (v1 + v7 to exercise schema-version interpolation) The hook entry's `command` resolves `${CLAUDE_PROJECT_DIR}` at hook execution time, so the same `.claude/settings.json` works regardless of the cwd Claude is invoked from. Closes #2.
1 parent 4e8de12 commit 217c35f

17 files changed

Lines changed: 1703 additions & 23 deletions

Cargo.lock

Lines changed: 336 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

klasp-agents-claude/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,10 @@ publish = false
1111

1212
[dependencies]
1313
klasp-core = { path = "../klasp-core" }
14+
serde_json = { workspace = true }
15+
tempfile = { workspace = true }
16+
thiserror = { workspace = true }
17+
18+
[dev-dependencies]
19+
insta = { version = "1", features = ["yaml"] }
20+
tempfile = { workspace = true }
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//! The bash shim klasp drops into `.claude/hooks/klasp-gate.sh`.
2+
//!
3+
//! Per [docs/design.md §7], the script is intentionally three lines of work
4+
//! around a marker comment: the schema-version export is the wire-protocol
5+
//! handshake, and `exec klasp gate "$@"` hands off to the Rust binary. The
6+
//! marker substring [`MANAGED_MARKER`] is the idempotency anchor — install
7+
//! re-greps for it to decide whether the file is klasp's to touch.
8+
9+
/// The literal substring `install` looks for to recognise klasp's managed
10+
/// hook script. Stable across schema bumps; the version digit follows.
11+
pub const MANAGED_MARKER: &str = "# klasp:managed";
12+
13+
/// Render the hook script body for the given wire-protocol schema version.
14+
///
15+
/// Pure: no filesystem, no env. Used by both `install` (to write the file)
16+
/// and `--dry-run` (to preview it).
17+
pub fn render(schema_version: u32) -> String {
18+
format!(
19+
"#!/usr/bin/env bash\n\
20+
{marker} v{ver} — generated by `klasp install`. Do not edit; re-run install instead.\n\
21+
export KLASP_GATE_SCHEMA={ver}\n\
22+
exec klasp gate \"$@\"\n",
23+
marker = MANAGED_MARKER,
24+
ver = schema_version,
25+
)
26+
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::*;
31+
32+
#[test]
33+
fn render_v1_contains_marker_export_and_exec() {
34+
let s = render(1);
35+
assert!(s.starts_with("#!/usr/bin/env bash\n"));
36+
assert!(s.contains(MANAGED_MARKER));
37+
assert!(s.contains("export KLASP_GATE_SCHEMA=1\n"));
38+
assert!(s.contains("exec klasp gate \"$@\"\n"));
39+
}
40+
41+
#[test]
42+
fn render_parameterises_schema_version() {
43+
let s = render(7);
44+
assert!(s.contains("# klasp:managed v7"));
45+
assert!(s.contains("export KLASP_GATE_SCHEMA=7"));
46+
}
47+
48+
#[test]
49+
fn render_ends_with_newline() {
50+
// POSIX: text files end with newlines. The script gets `chmod +x`'d
51+
// and an interactive editor that does "ensure final newline" should
52+
// produce no diff.
53+
assert!(render(1).ends_with('\n'));
54+
}
55+
}

klasp-agents-claude/src/lib.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
//! `klasp-agents-claude` — Claude Code `AgentSurface` impl.
22
//!
3-
//! W1 ships only the crate skeleton; the real implementation (settings.json
4-
//! merge, hook script template, idempotency checks) lands in W2 per
5-
//! [docs/roadmap.md] §"Timeline". The empty struct exists so the workspace
6-
//! compiles and the `klasp` binary can take this crate as a dependency
7-
//! today.
3+
//! See [docs/design.md] §3.1 (trait), §5 (install flow), §7 (hook script).
84
9-
/// Claude Code agent surface. The `AgentSurface` impl lands in W2 — see
10-
/// [docs/design.md] §3.1 and §5 for the contract this struct must satisfy.
11-
pub struct ClaudeCodeSurface;
5+
pub mod hook_template;
6+
pub mod settings;
7+
pub mod surface;
128

13-
impl ClaudeCodeSurface {
14-
pub const AGENT_ID: &'static str = "claude_code";
15-
}
9+
pub use hook_template::{render as render_hook_script, MANAGED_MARKER};
10+
pub use settings::{merge_hook_entry, unmerge_hook_entry, SettingsError};
11+
pub use surface::ClaudeCodeSurface;

0 commit comments

Comments
 (0)