Sync primitives
What this layer does
Phase 1 builds the smallest possible client surface for moving bytes between Joe's machine and DO Spaces through the Worker. Joe never holds DO credentials. The Worker is the only ingress. The client is a single no-deps Node script plus a versioned manifest. Everything else in snappy-os layers on top of these four primitives: push, pull, status, doctor.
Files involved
bin/do-spaces.js— 80-line pure-Node SigV4 signer for S3 v4. Reads
DO_SPACES_KEY / DO_SPACES_SECRET from env (Worker side) or stdin (Robert-side admin). Joe never calls this directly.
bin/cli.js— entry point exposingpush,pull,status,doctor,
snapshot, restore, rollback, --version, alert.
state/bin/sync/sync-rules.json— explicitSYNC_ALLOWand
SYNC_DENY lists. Every path under state/log/* must match one or the other. The sync-rules-coverage.ts lint enforces it.
state/bin/sync/manifest.ts— versionedSyncManifesttype and
helpers. Stored at state/log/sync-manifest.json.
state/bin/sync/sha-strategy.json— locked in Phase 0 by the
x-amz-meta-sha256 round-trip probe. Either header (preferred) or sidecar (fallback writes <file>.sha256).
state/bin/sync/cron-tick.sh— cron wrapper for runtimes without
native session-lifecycle hooks (Gemini CLI, OpenClaw).
Manifest format
type SyncManifest = {
version: 1;
generated_at: string;
files: Record<string, { sha256: string; size: number; mtime: string; }>;
};
Bump version only on incompatible change. Pull refuses if the remote manifest version exceeds what the local code understands.
Operational gotchas
- Two locks, two purposes.
/tmp/snappy-os-debounce-{runtime}.lockis a
60s mtime check that exits silently. /tmp/snappy-os-upload.lock is a flock(2) held during the actual POST /_push call. Stale debounce locks (mtime over 120s) get force-removed before exit.
- Rate-limit
push --autoper runtime, not per process — three Claude
Code windows on one machine share the same lock.
program.mdandsources/require explicit--scope programor
--scope sources to push. The default --auto scope is state.
- Files over 10 MB are rejected at push time. Files between 100 KB and
10 MB get gzipped before upload (Phase 12).
- Dry mode (
--dry) walks the tree, computes the manifest delta, and
logs the would-upload list to state/log/sync-events.ndjson without contacting the Worker.
How to verify it's working
snappy-os push --drylists changed paths and exits 0 on a clean tree.snappy-os pull --dryreturns zero remote-newer files immediately
after a successful push.
snappy-os statusprints a one-screen diff of local vs remote
manifest (counts by added, modified, deleted).
snappy-os doctorexits 0 with all section-A lints green.state/log/sync-events.ndjsongains exactly one row per push / pull
call, with dur_ms, bytes, and manifest_after populated.