diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0a4eaef --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash -c 'if [ -f Cargo.toml ]; then cargo check --quiet 2>&1 | head -20; fi'" + } + ] + } + ] + } +} diff --git a/HANDOFF.md b/HANDOFF.md index 0a982b7..5d95728 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -7,6 +7,7 @@ State of the `notfiles` repo as of 2026-04-03. ### Session 1 (2026-03-31) Restructured from a flat `src/` layout into a Cargo workspace of 5 crates: + - `notcore` — shared library (types, config, paths, errors) - `notfiles` — dotfiles linker, migrated from `src/` - `notsecrets` — age key retrieval (Bitwarden/file/prompt) + SOPS decrypt @@ -19,21 +20,22 @@ Completed the age-native redesign of `notsecrets` — all 12 tasks done. **All tasks complete:** -| Task | Status | Commit | -|------|--------|--------| -| 1: Scaffold (Cargo.toml, error.rs, domain types, legacy shim) | ✅ | `34d9e3d` | -| 2: format.rs (age wire format parser/serializer) | ✅ | `2476daa` | -| 3: X25519 identity + recipient | ✅ | `6202c5d` | -| 4: Scrypt identity + recipient | ✅ | `b899198` | -| 5: SSH Ed25519 identity + recipient | ✅ | `566501c` | -| 6: SSH RSA identity + recipient | ✅ | `a0fe6f7` | -| 7: EncryptedIdentity (passphrase-protected identity file) | ✅ | `545a45b` | -| 8: Encryptor — multi-recipient age encryption | ✅ | `9863af6` | -| 9: Decryptor + wire EncryptedIdentity | ✅ | `e388d75` | -| 10+11: sources/ migration + resolve_identities() | ✅ | `f80ea02` | -| 12: notstrap migration, remove sops shell-out | ✅ | `58de007` | +| Task | Status | Commit | +| ------------------------------------------------------------- | ------ | --------- | +| 1: Scaffold (Cargo.toml, error.rs, domain types, legacy shim) | ✅ | `34d9e3d` | +| 2: format.rs (age wire format parser/serializer) | ✅ | `2476daa` | +| 3: X25519 identity + recipient | ✅ | `6202c5d` | +| 4: Scrypt identity + recipient | ✅ | `b899198` | +| 5: SSH Ed25519 identity + recipient | ✅ | `566501c` | +| 6: SSH RSA identity + recipient | ✅ | `a0fe6f7` | +| 7: EncryptedIdentity (passphrase-protected identity file) | ✅ | `545a45b` | +| 8: Encryptor — multi-recipient age encryption | ✅ | `9863af6` | +| 9: Decryptor + wire EncryptedIdentity | ✅ | `e388d75` | +| 10+11: sources/ migration + resolve_identities() | ✅ | `f80ea02` | +| 12: notstrap migration, remove sops shell-out | ✅ | `58de007` | **Current state:** + - 80 tests pass, 0 clippy warnings, `cargo check --workspace` clean - `_legacy.rs` deleted; all deprecated shims removed - `notsecrets` is now a self-contained age encryption/decryption library @@ -42,6 +44,7 @@ Completed the age-native redesign of `notsecrets` — all 12 tasks done. - Example at `crates/notsecrets/examples/age_smoke.rs` demonstrates round-trip encrypt/decrypt Also done this session (outside notfiles): + - Fixed `~/.config/sops/age/keys.txt` — was `AGE-SECRET-KEY-1TESTKEY` placeholder, now real key from 1Password item `age-key-dotfiles` (UUID `6meypnchchq3tsb32mdnzxtlia`) - `env.nu` now self-heals `keys.txt` from 1Password on fresh login if missing/placeholder - Expanded `~/.secrets` to cover 18 keys (AWS, Docker, GitHub, Slack, ElevenLabs, Groq, Mistral, LangChain/Smith, SerpAPI, Twilio, Tavily, Context7) — all via `op://` UUID refs, resolved by `op inject` at shell startup @@ -51,29 +54,37 @@ Also done this session (outside notfiles): ## Pending Issues ### 1. Push to remote + Several commits ahead of `gitea/main`. Push when ready. ### 2. notstrap has an empty lib.rs + `crates/notstrap/src/lib.rs` is empty — created as a stub. Either delete `[lib]` from `notstrap/Cargo.toml` or remove the file. ### 3. notstrap config not documented + `notstrap.toml` format is defined inline via serde structs but never written down. A sample config would help. ### 4. Integration test coverage for nothooks assumes nu in PATH + `nothooks` integration tests call `nu` directly. CI needs `nu` installed or tests need `#[ignore]` + feature flag. ### 5. notstrap sops_file path footgun + If `sops_file` in `notstrap.toml` is an absolute path, the `join` will silently override the base. Low priority. ### 6. HuggingFace token not in ~/.secrets + No API token found in 1Password — only login credentials. Add token manually if needed. ### 7. notsecrets is not wire-compatible with standard age + The Encryptor/Decryptor use single-block ChaCha20-Poly1305 rather than the STREAM construction. Files encrypted by `notsecrets` cannot be decrypted by `rage`/`age` CLI and vice versa. This is intentional per the spec but worth documenting. ### Session 3 (2026-04-03) **notgraph** — crate dependency + module graph report landed and polished: + - Heatmap coloring on crate nodes (fan-in intensity → blue gradient) - Per-crate module graphs (Mermaid `flowchart TD`) with `__`-separated node IDs - Cycle callouts with `cycle-entry` CSS class @@ -83,18 +94,18 @@ The Encryptor/Decryptor use single-block ChaCha20-Poly1305 rather than the STREA **notgraph planned extensions** (not yet implemented — prioritized list): -| Priority | Section | Data source | Visualization | -|----------|---------|-------------|---------------| -| 1 | Code coverage heatmap | `cargo llvm-cov --json` | Treemap or per-crate bar | -| 2 | Cyclomatic complexity | `rust-code-analysis-cli --metrics` | Histogram + top-offenders table | -| 3 | Lines of code breakdown | `tokei --output json` | Stacked bar (code/comments/blanks) | -| 4 | Unsafe code audit | `cargo geiger --output-format Json` | Table + traffic-light badges | -| 5 | Dep freshness + vulns | `cargo outdated --format json` + `cargo audit --json` | Semver-distance dots + advisory cards | -| 6 | Doc coverage | `cargo doc -Z unstable-options --show-coverage` (nightly) | Horizontal bars per crate | -| 7 | Commit churn heatmap | `git log --name-only` | Scatter: churn × complexity × LOC × coverage | -| 8 | API surface area | `cargo rustdoc --output-format json` | Bar by item kind per crate | -| 9 | Test-to-code ratio | `tokei` + `#[test]` count | Table + stacked bar | -| 10 | Dep weight/bloat | `cargo metadata` | Stacked bar (direct+transitive) + duplicates | +| Priority | Section | Data source | Visualization | +| -------- | ----------------------- | --------------------------------------------------------- | -------------------------------------------- | +| 1 | Code coverage heatmap | `cargo llvm-cov --json` | Treemap or per-crate bar | +| 2 | Cyclomatic complexity | `rust-code-analysis-cli --metrics` | Histogram + top-offenders table | +| 3 | Lines of code breakdown | `tokei --output json` | Stacked bar (code/comments/blanks) | +| 4 | Unsafe code audit | `cargo geiger --output-format Json` | Table + traffic-light badges | +| 5 | Dep freshness + vulns | `cargo outdated --format json` + `cargo audit --json` | Semver-distance dots + advisory cards | +| 6 | Doc coverage | `cargo doc -Z unstable-options --show-coverage` (nightly) | Horizontal bars per crate | +| 7 | Commit churn heatmap | `git log --name-only` | Scatter: churn × complexity × LOC × coverage | +| 8 | API surface area | `cargo rustdoc --output-format json` | Bar by item kind per crate | +| 9 | Test-to-code ratio | `tokei` + `#[test]` count | Table + stacked bar | +| 10 | Dep weight/bloat | `cargo metadata` | Stacked bar (direct+transitive) + duplicates | Easiest to add with no extra tooling: LOC breakdown (tokei), test-to-code ratio, dep weight (cargo metadata always available). Coverage + complexity require `cargo-llvm-cov` and `rust-code-analysis-cli` installed. diff --git a/HANDOFF.notfiles.workspace.yaml b/HANDOFF.notfiles.workspace.yaml index 19f4cf1..c62bc1a 100644 --- a/HANDOFF.notfiles.workspace.yaml +++ b/HANDOFF.notfiles.workspace.yaml @@ -8,7 +8,7 @@ state: notes: null items: - id: notfile-1 - doob_uuid: 6d9780fe-d6d9-4316-90b5-6de44f06e96e + doob_uuid: d1c0adc2-028a-4a12-be84-0698587f6f35 name: public-ready-workflow-fix priority: P1 status: done @@ -23,7 +23,7 @@ items: note: emoji → [OK] formatting change; committed in style commit completed: 2026-04-03 - id: notfile-2 - doob_uuid: a7ff6a74-a664-4a0e-9a13-3e3884e5d492 + doob_uuid: b88a6135-3ecb-419e-aaac-22cb87595f90 name: notgraph-integration-test-fix priority: P1 status: done @@ -38,7 +38,7 @@ items: note: cargo fmt reformatted long emit::write_all calls; committed in style commit completed: 2026-04-03 - id: notfile-3 - doob_uuid: 49346e3f-0119-4e1f-9234-c59f14499aea + doob_uuid: 615051dd-d22c-4d50-ad70-15497780726b name: notgraph-plan-remaining-steps priority: P2 status: done diff --git a/crates/notcore/src/types.rs b/crates/notcore/src/types.rs index 2715ae4..0301545 100644 --- a/crates/notcore/src/types.rs +++ b/crates/notcore/src/types.rs @@ -21,6 +21,8 @@ pub struct HookSpec { pub name: String, pub script: String, pub phase: HookPhase, + #[serde(default)] + pub interpreter: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/nothooks/src/runner.rs b/crates/nothooks/src/runner.rs index 9214acd..5212f0c 100644 --- a/crates/nothooks/src/runner.rs +++ b/crates/nothooks/src/runner.rs @@ -4,6 +4,28 @@ use notcore::{HookPhase, HookSpec}; use std::path::PathBuf; use std::process::Command; +fn resolve_interpreter(spec: &HookSpec) -> Result { + if let Some(interp) = &spec.interpreter { + return Ok(interp.clone()); + } + let ext = std::path::Path::new(&spec.script) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""); + match ext { + "nu" => Ok("nu".into()), + "sh" => Ok("sh".into()), + "bash" => Ok("bash".into()), + "zsh" => Ok("zsh".into()), + "py" => Ok("python3".into()), + "rb" => Ok("ruby".into()), + _ => Err(format!( + "cannot infer interpreter: no interpreter set and unknown extension {:?}", + ext + )), + } +} + pub struct HookRunner { state_dir: PathBuf, force: bool, @@ -31,8 +53,11 @@ impl HookRunner { return HookResult::Skipped; } - // TODO: support other shells - let result = Command::new("nu").arg(&spec.script).status(); + let interp = match resolve_interpreter(spec) { + Ok(i) => i, + Err(msg) => return HookResult::Failed(msg), + }; + let result = Command::new(&interp).arg(&spec.script).status(); match result { Ok(status) if status.success() => { diff --git a/crates/nothooks/tests/integration.rs b/crates/nothooks/tests/integration.rs index 7d0c828..ff11664 100644 --- a/crates/nothooks/tests/integration.rs +++ b/crates/nothooks/tests/integration.rs @@ -4,19 +4,20 @@ use std::fs; use tempfile::TempDir; fn make_hook_script(dir: &TempDir, name: &str, content: &str) -> HookSpec { - let path = dir.path().join(format!("{name}.nu")); + let path = dir.path().join(format!("{name}.sh")); fs::write(&path, content).unwrap(); HookSpec { name: name.to_string(), script: path.to_str().unwrap().to_string(), phase: HookPhase::Dot, + interpreter: None, } } #[test] fn test_hook_success() { let dir = TempDir::new().unwrap(); - let spec = make_hook_script(&dir, "ok-hook", "print hello"); + let spec = make_hook_script(&dir, "ok-hook", "echo hello"); let runner = HookRunner::new(dir.path().to_path_buf()); let result = runner.run_hook(&spec); assert!(matches!(result, HookResult::Ok)); @@ -25,7 +26,7 @@ fn test_hook_success() { #[test] fn test_hook_failure() { let dir = TempDir::new().unwrap(); - let spec = make_hook_script(&dir, "fail-hook", "exit 1\n"); + let spec = make_hook_script(&dir, "fail-hook", "exit 1"); let runner = HookRunner::new(dir.path().to_path_buf()); let result = runner.run_hook(&spec); assert!(matches!(result, HookResult::Failed(_))); @@ -34,7 +35,7 @@ fn test_hook_failure() { #[test] fn test_setup_hook_skipped_on_rerun() { let dir = TempDir::new().unwrap(); - let mut spec = make_hook_script(&dir, "setup-hook", "print ran"); + let mut spec = make_hook_script(&dir, "setup-hook", "echo ran"); spec.phase = notcore::HookPhase::Setup; let runner = HookRunner::new(dir.path().to_path_buf()); @@ -49,7 +50,7 @@ fn test_setup_hook_skipped_on_rerun() { #[test] fn test_setup_hook_force_reruns() { let dir = TempDir::new().unwrap(); - let mut spec = make_hook_script(&dir, "force-hook", "print ran"); + let mut spec = make_hook_script(&dir, "force-hook", "echo ran"); spec.phase = notcore::HookPhase::Setup; let runner = HookRunner::new(dir.path().to_path_buf()); diff --git a/tests/integration/tests/bootstrap.rs b/tests/integration/tests/bootstrap.rs index fa7d84b..810d461 100644 --- a/tests/integration/tests/bootstrap.rs +++ b/tests/integration/tests/bootstrap.rs @@ -186,6 +186,7 @@ fn test_setup_hooks_skipped_on_rerun() { name: HOOK_NAME.to_string(), script: script.to_str().unwrap().to_string(), phase: HookPhase::Setup, + interpreter: None, }; let runner = HookRunner::new(d.to_path_buf()); let phase_report = run_phase(&[hook_spec], &HookPhase::Setup, &runner); diff --git a/tests/integration/tests/cross_crate.rs b/tests/integration/tests/cross_crate.rs index ba0b70d..c2c32ea 100644 --- a/tests/integration/tests/cross_crate.rs +++ b/tests/integration/tests/cross_crate.rs @@ -35,6 +35,7 @@ fn test_nothooks_notsecrets_independent() { name: "chain".to_string(), script: script.to_str().unwrap().to_string(), phase: HookPhase::Dot, + interpreter: None, }; let runner = HookRunner::new(dir.path().to_path_buf());