diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..2d8814f --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,217 @@ +# ARCHITECTURE.md + +Design document for sgard (Shimmering Clarity Gardener), a dotfiles manager. + +## Overview + +sgard manages dotfiles by checkpointing them into a portable repository and +restoring them to their original locations. The repository is a single +directory that can live anywhere — local disk, USB drive, NFS mount — making +it portable between machines. + +## Tech Stack + +**Language: Go** (`github.com/kisom/sgard`) + +- Static binaries by default, no runtime dependencies on target machines. +- First-class gRPC and protobuf support for the future remote mode. +- Standard library covers all core needs: file I/O (`os`, `path/filepath`), + hashing (`crypto/sha256`), and cross-platform path handling. +- Trivial cross-compilation via `GOOS`/`GOARCH`. + +**CLI framework: cobra** + +**Manifest format: YAML** (via `gopkg.in/yaml.v3`) + +- Human-readable and supports comments (unlike JSON). +- Natural syntax for lists of structured entries (unlike TOML's `[[array_of_tables]]`). +- File modes stored as quoted strings (`"0644"`) to avoid YAML's octal coercion. + +## Repository Layout on Disk + +A sgard repository is a single directory with this structure: + +``` +/ + manifest.yaml # single manifest tracking all files + blobs/ + a1/b2/a1b2c3d4... # content-addressable file storage +``` + +### Manifest Schema + +```yaml +version: 1 +created: "2026-03-23T12:00:00Z" +updated: "2026-03-23T14:30:00Z" +message: "pre-upgrade checkpoint" # optional + +files: + - path: ~/.bashrc # original location (default restore target) + hash: a1b2c3d4e5f6... # SHA-256 of file contents + type: file # file | directory | link + mode: "0644" # permissions (quoted to avoid YAML coercion) + updated: "2026-03-23T14:30:00Z" # last checkpoint time for this file + + - path: ~/.config/nvim + type: directory + mode: "0755" + updated: "2026-03-23T14:30:00Z" + # directories have no hash or blob — they're structural entries + + - path: ~/.vimrc + type: link + target: ~/.config/nvim/init.vim # symlink target + updated: "2026-03-23T14:30:00Z" + # links have no hash or blob — just the target path + + - path: ~/.ssh/config + hash: d4e5f6a1b2c3... + type: file + mode: "0600" + updated: "2026-03-23T14:30:00Z" +``` + +### Blob Store + +Files are stored by their SHA-256 hash in a two-level directory structure: + +``` +blobs/// +``` + +Example: a file with hash `a1b2c3d4e5...` is stored at `blobs/a1/b2/a1b2c3d4e5...` + +Properties: +- **Deduplication**: identical files across different paths share one blob. +- **Rename-safe**: moving a dotfile to a new path updates only the manifest. +- **Integrity**: the filename *is* the expected hash — corruption is trivially detectable. +- **Directories and symlinks** are manifest-only entries. No blobs are stored for them. + +## CLI Commands + +All commands operate on a repository directory (default: `~/.sgard`, override with `--repo`). + +### Phase 1 — Local + +| Command | Description | +|---|---| +| `sgard init [--repo ]` | Create a new repository | +| `sgard add ...` | Track files; copies them into the blob store and adds manifest entries | +| `sgard remove ...` | Untrack files; removes manifest entries (blobs cleaned up on next checkpoint) | +| `sgard checkpoint [-m ]` | Re-hash all tracked files, store any changed blobs, update manifest | +| `sgard restore [...] [--force]` | Restore files from manifest to their original locations | +| `sgard status` | Compare current files against manifest: modified, missing, ok | +| `sgard verify` | Check all blobs against manifest hashes (integrity check) | +| `sgard list` | List all tracked files | +| `sgard diff []` | Show content diff between current file and stored blob | + +**Workflow example:** + +```sh +# Initialize a repo on a USB drive +sgard init --repo /mnt/usb/dotfiles + +# Track some files +sgard add ~/.bashrc ~/.gitconfig ~/.ssh/config --repo /mnt/usb/dotfiles + +# Checkpoint current state +sgard checkpoint -m "initial" --repo /mnt/usb/dotfiles + +# On a new machine, restore +sgard restore --repo /mnt/usb/dotfiles +``` + +### Phase 2 — Remote (Future) + +| Command | Description | +|---|---| +| `sgard push` | Push checkpoint to remote gRPC server | +| `sgard pull` | Pull checkpoint from remote gRPC server | +| `sgard serve` | Run the gRPC daemon | + +## Go Package Structure + +``` +sgard/ + cmd/ + sgard/ # CLI entry point + main.go # cobra root command, --repo flag + init.go # sgard init + add.go # sgard add + remove.go # sgard remove + checkpoint.go # sgard checkpoint + restore.go # sgard restore + status.go # sgard status + verify.go # sgard verify + list.go # sgard list + diff.go # sgard diff + sgardd/ # gRPC server entry point (Phase 2) + + garden/ # Core business logic + garden.go # Garden struct: orchestrates manifest + store + filesystem + garden_test.go + hasher.go # SHA-256 file hashing + diff.go # File diff generation + + manifest/ # YAML manifest parsing + manifest.go # Manifest and Entry structs, Load/Save + manifest_test.go + + store/ # Content-addressable blob storage + store.go # Store struct: Write/Read/Exists/Delete + store_test.go + + proto/ # gRPC service definition (Phase 2) + sgard/v1/ + sgard.proto + + server/ # gRPC server implementation (Phase 2) +``` + +### Key Architectural Rule + +**The `garden` package contains all logic. The `cmd` package is pure CLI wiring.** + +The `Garden` struct is the central coordinator: + +```go +type Garden struct { + manifest *manifest.Manifest + store *store.Store + root string // repository root directory +} + +func (g *Garden) Add(paths []string) error +func (g *Garden) Remove(paths []string) error +func (g *Garden) Checkpoint(message string) error +func (g *Garden) Restore(paths []string, force bool) error +func (g *Garden) Status() ([]FileStatus, error) +func (g *Garden) Verify() ([]VerifyResult, error) +func (g *Garden) Diff(path string) (string, error) +``` + +This separation means the future gRPC server calls the same `Garden` methods +as the CLI — no logic duplication. + +## Design Decisions + +**Paths in manifest use `~` unexpanded.** The `garden` package expands `~` to +`$HOME` at runtime. This makes the manifest portable across machines with +different usernames. + +**No history.** Phase 1 stores only the latest checkpoint. For versioning, +place the manifest under git. The `blobs/` directory should be gitignored — +blob durability (backup, replication) is deferred to a future phase. + +**Per-file timestamps.** Each manifest entry records an `updated` timestamp +set at checkpoint time. On restore, if the manifest entry is newer than the +file on disk (by mtime), the restore proceeds without prompting. If the file +on disk is newer or the times match, sgard prompts for confirmation. +`--force` always skips the prompt. + +**Atomic writes.** Checkpoint writes `manifest.yaml.tmp` then renames to +`manifest.yaml`. A crash cannot corrupt the manifest. + +**Old C++/proto source files** are retained in the git history for reference +and will be removed as part of the Go rewrite. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..27c0105 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Critical: Keep Project Docs Updated + +Any change to the codebase MUST be reflected in these files: + +- **ARCHITECTURE.md** — design decisions, data model, package structure +- **PROJECT_PLAN.md** — implementation steps; check off completed items +- **PROGRESS.md** — current status, change log; update after completing any step + +If another agent or engineer picks this up later, these files are how they +resume. Keeping them accurate is not optional. + +## Project + +sgard (Shimmering Clarity Gardener) — a dotfiles manager. +Module: `github.com/kisom/sgard`. Author: K. Isom . + +## Build + +```bash +go build ./cmd/sgard +``` + +Run tests: +```bash +go test ./... +``` + +## Dependencies + +- `gopkg.in/yaml.v3` — manifest serialization +- `github.com/spf13/cobra` — CLI framework + +## Package Structure + +``` +cmd/sgard/ CLI entry point (cobra commands, pure wiring) +garden/ Core business logic (Garden struct orchestrating everything) +manifest/ YAML manifest parsing (Manifest/Entry structs, Load/Save) +store/ Content-addressable blob storage (SHA-256 keyed) +``` + +Key rule: all logic lives in `garden/`. The `cmd/` layer only parses flags +and calls `Garden` methods. This enables the future gRPC server to reuse +the same logic with zero duplication. + +## Legacy Files + +Old C++ and proto source files may still be present. They are retained in +git history for reference and should be removed as part of the Go rewrite +(see PROJECT_PLAN.md Step 1). diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..2e382d2 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,38 @@ +# PROGRESS.md + +Tracks implementation status. See PROJECT_PLAN.md for the full plan and +ARCHITECTURE.md for design details. + +**If you are picking this up mid-implementation, read this file first.** + +## Current Status + +**Phase:** Pre-implementation — design complete, ready to begin Step 1. + +**Last updated:** 2026-03-23 + +## Completed Steps + +(none yet) + +## In Progress + +(none yet) + +## Up Next + +Step 1: Project Scaffolding — remove old C++ files, initialize Go module, +create directory structure, set up cobra root command. + +## Known Issues / Decisions Deferred + +- **Blob durability**: blobs are not stored in git. A strategy for backup or + replication is deferred to a future phase. +- **gRPC remote mode**: Phase 2. Package structure is designed to accommodate + it (garden core separates logic from CLI wiring). + +## Change Log + +| Date | Step | Summary | +|---|---|---| +| 2026-03-23 | — | Design phase complete. ARCHITECTURE.md and PROJECT_PLAN.md written. | diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md new file mode 100644 index 0000000..7e48773 --- /dev/null +++ b/PROJECT_PLAN.md @@ -0,0 +1,99 @@ +# PROJECT_PLAN.md + +Implementation plan for sgard. See ARCHITECTURE.md for design details. + +## Step 1: Project Scaffolding + +Remove old C++ source files and set up the Go project. + +- [ ] Remove old files: `sgard.cc`, `proto/`, `CMakeLists.txt`, `scripts/`, `.clang-format`, `.clang-tidy`, `.idea/` +- [ ] `go mod init github.com/kisom/sgard` +- [ ] Add dependencies: `gopkg.in/yaml.v3`, `github.com/spf13/cobra` +- [ ] Create directory structure: `cmd/sgard/`, `manifest/`, `store/`, `garden/` +- [ ] Set up `cmd/sgard/main.go` with cobra root command and `--repo` persistent flag +- [ ] Update CLAUDE.md to reflect Go project +- [ ] Verify: `go build ./...` compiles clean + +## Step 2: Manifest Package + +*Can be done in parallel with Step 3.* + +- [ ] `manifest/manifest.go`: `Manifest` and `Entry` structs with YAML tags + - Entry types: `file`, `directory`, `link` + - Mode as string type to avoid YAML octal coercion + - Per-file `updated` timestamp +- [ ] `manifest/manifest.go`: `Load(path)` and `Save(path)` functions + - Save uses atomic write (write to `.tmp`, rename) +- [ ] `manifest/manifest_test.go`: round-trip marshal/unmarshal, atomic save, entry type validation + +## Step 3: Store Package + +*Can be done in parallel with Step 2.* + +- [ ] `store/store.go`: `Store` struct with `root` path +- [ ] `store/store.go`: `Write(data) (hash, error)` — hash content, write to `blobs/XX/YY/` +- [ ] `store/store.go`: `Read(hash) ([]byte, error)` — read blob by hash +- [ ] `store/store.go`: `Exists(hash) bool` — check if blob exists +- [ ] `store/store.go`: `Delete(hash) error` — remove a blob +- [ ] `store/store_test.go`: write/read round-trip, integrity check, missing blob error + +## Step 4: Garden Core — Init and Add + +Depends on Steps 2 and 3. + +- [ ] `garden/hasher.go`: `HashFile(path) (string, error)` — SHA-256 of a file +- [ ] `garden/garden.go`: `Garden` struct tying manifest + store + root path +- [ ] `garden/garden.go`: `Open(root) (*Garden, error)` — load existing repo +- [ ] `garden/garden.go`: `Init(root) (*Garden, error)` — create new repo (dirs + empty manifest) +- [ ] `garden/garden.go`: `Add(paths []string) error` — hash files, store blobs, add manifest entries +- [ ] `garden/garden_test.go`: init creates correct structure, add stores blob and updates manifest +- [ ] Wire up CLI: `cmd/sgard/init.go`, `cmd/sgard/add.go` +- [ ] Verify: `go build ./cmd/sgard && ./sgard init && ./sgard add ~/.bashrc` + +## Step 5: Checkpoint and Status + +Depends on Step 4. + +- [ ] `garden/garden.go`: `Checkpoint(message string) error` — re-hash all tracked files, store changed blobs, update manifest timestamps +- [ ] `garden/garden.go`: `Status() ([]FileStatus, error)` — compare current hashes to manifest; report modified/missing/ok +- [ ] `garden/garden_test.go`: checkpoint detects changed files, status reports correctly +- [ ] Wire up CLI: `cmd/sgard/checkpoint.go`, `cmd/sgard/status.go` + +## Step 6: Restore + +Depends on Step 5. + +- [ ] `garden/garden.go`: `Restore(paths []string, force bool) error` + - Restore all files if paths is empty, otherwise just the specified paths + - Timestamp comparison: skip prompt if manifest `updated` is newer than file mtime + - Prompt user if file on disk is newer or times match (unless `--force`) + - Create parent directories as needed + - Recreate symlinks for `link` type entries + - Set file permissions from manifest `mode` +- [ ] `garden/garden_test.go`: restore writes correct content, respects permissions, handles symlinks +- [ ] Wire up CLI: `cmd/sgard/restore.go` + +## Step 7: Remaining Commands + +*These can be done in parallel with each other.* + +- [ ] `garden/garden.go`: `Remove(paths []string) error` — remove manifest entries +- [ ] `garden/garden.go`: `Verify() ([]VerifyResult, error)` — check blobs against manifest hashes +- [ ] `garden/garden.go`: `List() []Entry` — return all manifest entries +- [ ] `garden/diff.go`: `Diff(path string) (string, error)` — diff stored blob vs current file +- [ ] Wire up CLI: `cmd/sgard/remove.go`, `cmd/sgard/verify.go`, `cmd/sgard/list.go`, `cmd/sgard/diff.go` +- [ ] Tests for each + +## Step 8: Polish + +- [ ] Lint setup (golangci-lint config) +- [ ] End-to-end test: init → add → checkpoint → modify file → status → restore → verify +- [ ] Ensure `go vet ./...` and `go test ./...` pass clean +- [ ] Update CLAUDE.md, ARCHITECTURE.md, PROGRESS.md + +## Future Steps (Not Phase 1) + +- Blob durability (backup/replication strategy) +- gRPC remote mode (push/pull/serve) +- Proto definitions for wire format +- Shell completion via cobra