Step 12: GardenSync gRPC server with 5 RPC handlers — PushManifest (timestamp comparison, missing blob detection), PushBlobs (chunked streaming, manifest replacement), PullManifest, PullBlobs, Prune. Added store.List() and garden.ListBlobs()/DeleteBlob() for prune. In-process tests via bufconn. Step 12b: Add now recurses directories (walks files/symlinks, skips dir entries). Mirror up syncs filesystem → manifest (add new, remove deleted, rehash changed). Mirror down syncs manifest → filesystem (restore + delete untracked with optional confirm). 7 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.3 KiB
8.3 KiB
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/,.trunk/ 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.gowith cobra root command and--repopersistent 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:ManifestandEntrystructs with YAML tags- Entry types:
file,directory,link - Mode as string type to avoid YAML octal coercion
- Per-file
updatedtimestamp
- Entry types:
manifest/manifest.go:Load(path)andSave(path)functions- Save uses atomic write (write to
.tmp, rename)
- Save uses atomic write (write to
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:Storestruct withrootpathstore/store.go:Write(data) (hash, error)— hash content, write toblobs/XX/YY/<hash>store/store.go:Read(hash) ([]byte, error)— read blob by hashstore/store.go:Exists(hash) bool— check if blob existsstore/store.go:Delete(hash) error— remove a blobstore/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 filegarden/garden.go:Gardenstruct tying manifest + store + root pathgarden/garden.go:Open(root) (*Garden, error)— load existing repogarden/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 entriesgarden/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 timestampsgarden/garden.go:Status() ([]FileStatus, error)— compare current hashes to manifest; report modified/missing/okgarden/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, confirm func) error- Restore all files if paths is empty, otherwise just the specified paths
- Timestamp comparison: skip prompt if manifest
updatedis 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
linktype 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/remove.go:Remove(paths []string) error— remove manifest entriesgarden/verify.go:Verify() ([]VerifyResult, error)— check blobs against manifest hashesgarden/list.go:List() []Entry— return all manifest entriesgarden/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)
- Clock abstraction: inject
jonboulle/clockworkinto Garden for deterministic timestamp tests - End-to-end test: init → add → checkpoint → modify file → status → restore → verify
- Ensure
go vet ./...andgo test ./...pass clean - Update CLAUDE.md, ARCHITECTURE.md, PROGRESS.md
Phase 2: gRPC Remote Sync
Step 9: Proto Definitions + Code Gen
- Write
proto/sgard/v1/sgard.proto— 5 RPCs (PushManifest, PushBlobs, PullManifest, PullBlobs, Prune), all messages - Add Makefile target for protoc code generation
- Add grpc, protobuf, x/crypto deps to go.mod
- Update flake.nix devShell with protoc tools
- Verify:
go build ./sgardpbcompiles
Step 10: Garden Accessor Methods
Can be done in parallel with Step 11.
garden/garden.go:GetManifest(),BlobExists(),ReadBlob(),WriteBlob(),ReplaceManifest()- Tests for each accessor
- Verify:
go test ./garden/...
Step 11: Proto-Manifest Conversion
Can be done in parallel with Step 10.
server/convert.go:ManifestToProto,ProtoToManifest, entry helpersserver/convert_test.go: round-trip test- Verify:
go test ./server/...
Step 12: Server Implementation (No Auth)
Depends on Steps 9, 10, 11.
server/server.go: Server struct with RWMutex, 5 RPC handlers (+ Prune)- PushManifest: timestamp compare, compute missing blobs
- PushBlobs: receive stream, write to store, replace manifest
- PullManifest: return manifest
- PullBlobs: stream requested blobs (64 KiB chunks)
- Prune: remove orphaned blobs (added store.List + garden.ListBlobs/DeleteBlob)
server/server_test.go: in-process test with bufconn, push+pull+prune
Step 12b: Directory Recursion and Mirror Command
garden/garden.go:Addrecurses directories — walk all files/symlinks, add each as its own entrygarden/mirror.go:MirrorUp(paths []string) error— walk directory, add new files, remove entries for files gone from disk, re-hash changedgarden/mirror.go:MirrorDown(paths []string, force bool, confirm func(string) bool) error— restore all tracked files under path, delete anything not in manifestgarden/mirror_test.go: tests for recursive add, mirror up (detects new/removed), mirror down (cleans extras)cmd/sgard/mirror.go:sgard mirror up <path>,sgard mirror down <path> [--force]- Update existing add tests for directory recursion
Step 13: Client Library (No Auth)
Depends on Step 12.
client/client.go: Client struct,Push(),Pull()methodsclient/client_test.go: integration test against in-process server
Step 14: SSH Key Auth
server/auth.go: AuthInterceptor, parse authorized_keys, verify SSH signaturesclient/auth.go: LoadSigner (ssh-agent or key file), PerRPCCredentialsserver/auth_test.go: in-memory ed25519 key pair, reject unauthenticatedclient/auth_test.go: metadata generation test
Step 15: CLI Wiring + Prune
Depends on Steps 13, 14.
garden/prune.go:Prune() (int, error)— collect referenced hashes from manifest, delete orphaned blobs, return count removedgarden/prune_test.go: add file, remove it, prune removes orphaned blobserver/server.go: addPruneRPC — server-side prune, returns countproto/sgard/v1/sgard.proto: addrpc Prune(PruneRequest) returns (PruneResponse)client/client.go: addPrune()methodcmd/sgard/prune.go: local prune; with--remoteflag prunes remote insteadcmd/sgard/main.go: add--remote,--ssh-keypersistent flagscmd/sgard/push.go,cmd/sgard/pull.gocmd/sgardd/main.go: flags, garden open, auth interceptor, gRPC serve- Verify: both binaries compile
Step 16: Polish + Release
- Update ARCHITECTURE.md, README.md, CLAUDE.md, PROGRESS.md
- Update flake.nix (add sgardd, protoc to devShell)
- Update .goreleaser.yaml (add sgardd build)
- E2e integration test: init two repos, push from one, pull into other
- Verify: all tests pass, full push/pull cycle works
Future Steps (Not Phase 2)
- Shell completion via cobra
- TLS transport (optional --tls-cert/--tls-key on sgardd)
- Multiple repo support on server