Add design docs for Go rewrite.
ARCHITECTURE.md: tech stack (Go, YAML, cobra), repo layout, manifest schema, content-addressable blob store, CLI commands, package structure. PROJECT_PLAN.md: 8-step implementation sequence with parallelism notes. PROGRESS.md: status tracker for resumable implementation. CLAUDE.md: project guidance for Claude Code, references design docs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
217
ARCHITECTURE.md
Normal file
217
ARCHITECTURE.md
Normal file
@@ -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:
|
||||||
|
|
||||||
|
```
|
||||||
|
<repo>/
|
||||||
|
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/<first 2 hex chars>/<next 2 hex chars>/<full 64-char hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <path>]` | Create a new repository |
|
||||||
|
| `sgard add <path>...` | Track files; copies them into the blob store and adds manifest entries |
|
||||||
|
| `sgard remove <path>...` | Untrack files; removes manifest entries (blobs cleaned up on next checkpoint) |
|
||||||
|
| `sgard checkpoint [-m <message>]` | Re-hash all tracked files, store any changed blobs, update manifest |
|
||||||
|
| `sgard restore [<path>...] [--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 [<path>]` | 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.
|
||||||
54
CLAUDE.md
Normal file
54
CLAUDE.md
Normal file
@@ -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 <kyle@imap.cc>.
|
||||||
|
|
||||||
|
## 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).
|
||||||
38
PROGRESS.md
Normal file
38
PROGRESS.md
Normal file
@@ -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. |
|
||||||
99
PROJECT_PLAN.md
Normal file
99
PROJECT_PLAN.md
Normal file
@@ -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/<hash>`
|
||||||
|
- [ ] `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
|
||||||
Reference in New Issue
Block a user