Files
notfiles/docs/superpowers/specs/2026-03-31-notfiles-workspace-architecture-design.md

196 lines
7.6 KiB
Markdown
Raw Permalink Normal View History

# notfiles Workspace Architecture Design
**Date:** 2026-03-31
**Status:** Approved
## Overview
`notfiles` is a pure-Rust dotfiles manager replacing GNU Stow. This design expands it into a **Cargo workspace** of focused crates that together replace the entire `dotfiles/` shell script ecosystem — including bootstrapping a new machine from scratch.
The primary pain points being solved:
- New machine setup requires too many manual steps before the bootstrap script even runs
- Secrets/1Password setup is fragile — keys aren't available early enough
- Too many shell scripts that are hard to maintain and test
- Nix/Homebrew split is unclear; Rust tools are preferred
---
## Workspace Structure
```
notfiles/ # Cargo workspace root
├── Cargo.toml # [workspace] members
├── notfiles.toml # dotfiles symlink config
├── notstrap.toml # bootstrap hook/phase config
├── crates/
│ ├── notcore/ # shared types, config, paths, errors
│ ├── notfiles/ # symlink engine (stow replacement)
│ ├── notsecrets/ # age key retrieval + sops decrypt
│ ├── nothooks/ # hook execution engine
│ └── notstrap/ # new-machine bootstrap orchestrator
└── tests/ # workspace-level integration tests
```
### Crate Summary
| Crate | Type | Role |
|-------|------|------|
| `notcore` | lib | Shared types, config, paths, errors |
| `notfiles` | lib + bin | Symlink engine (stow replacement) |
| `notsecrets` | lib | Age key retrieval → sops decrypt |
| `nothooks` | lib + bin | Hook execution, phase tracking |
| `notstrap` | bin | Orchestrates everything on a new machine |
---
## Crate Designs
### `notcore` — Shared Foundation
Pure library. No binary. Every other crate depends on it; it depends on nothing in the workspace.
**Contents:**
- `NotfilesError` — unified error enum via `thiserror`
- `Config` — parses `notfiles.toml` + `notstrap.toml`
- `Paths``expand_tilde`, `dotfiles_dir()`, standard path resolution
- `HookSpec` — name, script path, phase (`dot` | `setup`)
- `PackageSpec` — name, link method (`symlink` | `copy`), target dir
- `Report` / `Step` — shared types for the end-of-run summary table
**Migration from current code:** `config.rs`, `paths.rs`, `error.rs` move from the current `notfiles/src/` into `notcore`.
---
### `notfiles` — Symlink Engine
Library + binary. The stow replacement. Operates on a dotfiles directory of "packages" (subdirectories) and symlinks their contents into a target (typically `$HOME`).
**Responsibilities:**
- `link` — create symlinks or copies from package dirs to target
- `unlink` — remove symlinks, clean up empty parent dirs
- `status` — diff expected state (config + disk) vs actual state (symlinks + state file)
- `State` — serialize/deserialize `.notfiles-state.toml` (tracks every linked file)
- Conflict detection and `--force` backup handling
- Glob-based ignore matching via `globset`
**Migration from current code:** `linker.rs`, `package.rs`, `ignore.rs`, `status.rs` stay in `notfiles`. Config/paths/error types are imported from `notcore`.
Used as a library by `notstrap` (`notfiles::link()` called directly).
---
### `notsecrets` — Secrets Bootstrap
Library. Retrieves the age private key and decrypts `secrets.sops.env` before anything else runs.
**Age key sources (tried in order):**
1. **Bitwarden CLI** (`bw`) — non-interactive if session is cached; prompts for master password if not
2. **File**`--key-file <path>` (USB drive, external storage)
3. **Interactive prompt** — user pastes the age key directly
Implemented as a port: `AgeKeySource` trait with three implementations (`BitwardenSource`, `FileSource`, `PromptSource`). `notstrap` iterates sources until one succeeds.
Once the age key is in hand: writes it to `~/.config/sops/age/keys.txt`, then runs `sops --decrypt secrets.sops.env` to produce a live env file. The decrypted env contains all critical credentials: op, bw, github, openai, anthropic, etc.
1Password (`op`) is NOT used at this stage — it's installed later as a hook, after secrets are already available.
---
### `nothooks` — Hook Execution Engine
Library + binary. Replaces `run_hook`, `run_dot_hooks`, `run_setup_hooks` from `bootstrap.sh`.
**Hook phases:**
- `dot` — fast, re-runnable (shell config, git config, AI tool configs)
- `setup` — slow, run-once (package installs, language runtimes, dev tools)
**Responsibilities:**
- Read hook specs from `notstrap.toml` — ordered list with name, script path, phase
- Execute hooks in declared order, capturing stdout/stderr
- Track which `setup` hooks have already run in `.nothooks-state.toml` — skip on re-run unless `--force`
- Feed per-hook pass/skip/fail results into `Report` from `notcore`
**Does NOT handle:** package management (invoked as hooks), secret handling (done before hooks run).
CLI: `nothooks run --phase dot` or `nothooks run --phase setup`
---
### `notstrap` — New Machine Orchestrator
Binary only. The single entry point on a fresh machine.
**Install:**
```bash
cargo install notstrap
notstrap run
```
**Execution order:**
1. Check prerequisites (`op`, `bw`, `sops`, `age` — print exactly what's missing and stop)
2. Run `notsecrets` — retrieve age key → decrypt `secrets.sops.env` → inject into env
3. Clone dotfiles repo if not present (URL from `notstrap.toml`)
4. Run `notfiles link` — stow all packages
5. Run `nothooks run --phase dot` — fast hooks
6. Run `nothooks run --phase setup` — slow hooks (includes `op` install + sign-in)
7. Print `Report` summary table
---
## Day-One Flow
```
cargo install notstrap
notstrap run
└─ notsecrets
├─ try: bw unlock → age key
├─ fallback: --key-file <path>
└─ fallback: interactive prompt
└─ sops decrypt secrets.sops.env → env injected
└─ notfiles link (all packages)
└─ nothooks --phase dot
└─ shell config, git config, AI tool configs (~seconds)
└─ nothooks --phase setup
└─ Homebrew/Nix packages, mise runtimes, dev tools, op install (~minutes)
└─ Report: ✓ linked 142 files, ✓ 3 dot hooks, ✓ 7 setup hooks
```
---
## Migration Strategy
Migration from `dotfiles/` is gradual — shell scripts die as their Rust equivalents ship:
| Phase | What ships | What it replaces |
|-------|-----------|-----------------|
| 1 | `notfiles` workspace (this design) | GNU Stow |
| 2 | `notsecrets` + `notstrap` skeleton | `setup-secrets.sh`, `install.sh` |
| 3 | `nothooks` | `bootstrap.sh` hook runner |
| 4 | Hook-by-hook Rust rewrites | Individual `setup-*.sh` scripts |
| 5 | `dotfiles/` repo archived | — |
Existing `.rs` scripts (`drift-check.rs`, `redact-audit.rs`, `claude-sessions.rs`, etc.) can migrate into `nothooks` hook scripts or dedicated crates as needed.
---
## Dependencies
| Crate | Key dependencies |
|-------|-----------------|
| `notcore` | `serde`, `toml`, `thiserror`, `dirs`, `anyhow` |
| `notfiles` | `notcore`, `clap`, `globset`, `chrono` |
| `notsecrets` | `notcore`, `clap`, `rpassword`, `which` |
| `nothooks` | `notcore`, `clap`, `serde`, `toml` |
| `notstrap` | `notcore`, `notfiles`, `notsecrets`, `nothooks`, `clap` |
---
## Open Questions
- Should `notstrap` be published to crates.io, or installed from the dotfiles repo directly via `cargo install --path`?
- Should `notsecrets` support a fourth source: a second Bitwarden-compatible backend (Vaultwarden self-hosted)?
- Hook scripts: keep as shell scripts invoked by `nothooks`, or migrate each to a Rust binary over time?