Add optional sync between Macs with a shared 8-digit code
Notes and photos were stuck on one machine. A small Go/Postgres backend
stores each group's payload under a code that doubles as the shared
secret, so a second Mac just needs the code — no account or login.
Applying a remote change must not push it straight back, otherwise the
two Macs would bump the version back and forth forever, so payloads are
fingerprinted and identical ones are skipped.
💘 Generated with Crush
Assisted-by: Crush:kimi-k3
This commit is contained in:
@@ -38,19 +38,62 @@ clicking an item copies it back to the clipboard.
|
||||
pasteboard item, simulates Cmd+C, polls `changeCount` for ≤0.5s, reads the
|
||||
string, restores the backup. Runs off the main thread (hotkey dispatches to
|
||||
a global queue); only the store append hops back to main.
|
||||
- `Sources/Prompt/SyncService.swift` — optional sync client. Holds the group
|
||||
code and server URL in `UserDefaults` (`sync.code`, `sync.serverURL`,
|
||||
`sync.version`), pushes the whole store as JSON, and polls every 5s.
|
||||
- `Server/main.go` — the Go sync backend (single file, `net/http` + `lib/pq`).
|
||||
|
||||
## Sync protocol
|
||||
|
||||
No authentication by design: a sync group is named by an 8-digit code
|
||||
(`1234-5678`) that doubles as the shared secret. The server stores the client
|
||||
payload as opaque `JSONB`; it never parses note contents.
|
||||
|
||||
| Endpoint | Behaviour |
|
||||
| --- | --- |
|
||||
| `POST /api/codes` | Generates a unique code and inserts an empty group |
|
||||
| `GET /api/sync/{code}` | `{"version":N,"updatedAt":…,"data":{…}\|null}`, 404 if unknown |
|
||||
| `PUT /api/sync/{code}` | Upserts the payload, returns the incremented version |
|
||||
| `GET /healthz` | Liveness |
|
||||
|
||||
Client rules:
|
||||
|
||||
- Payload = `{sections, activeSectionID, images}`, where `images` maps a
|
||||
filename to base64 PNG so photos travel with the notes.
|
||||
- Version counter is server-owned; the client stores the last version it saw
|
||||
in `sync.version` and only applies a remote payload when `remote.version >
|
||||
lastVersion`.
|
||||
- **Never push a payload that came from the server.** `pushNow()` compares a
|
||||
sorted-keys encoding against `lastSyncedPayload` and skips identical
|
||||
payloads. Without this, applying a remote change would immediately push it
|
||||
back, bump the version, and the two Macs would ping-pong forever. The
|
||||
`applyingRemote` flag alone is not enough: the save-triggered push is
|
||||
dispatched asynchronously, so it runs *after* the flag is cleared.
|
||||
- Conflict resolution is last-writer-wins at whole-store granularity. That is
|
||||
acceptable for personal use; it is not a merge.
|
||||
- `apply()` writes incoming images, deletes image files no longer referenced,
|
||||
and replaces the store in one shot.
|
||||
- `Prompt --sync-check` runs `SyncService.runDiagnostic()`: pushes any local
|
||||
change and prints local vs remote versions, then exits. Use it to debug a
|
||||
second Mac without the UI.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
swift build # debug build
|
||||
swift run Prompt # dev run (menu bar icon appears)
|
||||
./Prompt.app/Contents/MacOS/Prompt --sync-check # push + report sync state, then exit
|
||||
./build-app.sh 0.1.0 # release build + .app bundle + icon + codesign
|
||||
./release.sh 0.1.0 # build-app.sh + dist/Prompt.zip + notarize + sha256
|
||||
scripts/make-icon.sh # regenerate Prompt.app/Contents/Resources/Icon.icns
|
||||
|
||||
# sync server (Go, needs Postgres)
|
||||
createdb prompt_sync
|
||||
cd Server && go run . # :8080, PROMPT_SYNC_DATABASE_URL / PROMPT_SYNC_ADDR
|
||||
```
|
||||
|
||||
There is no test target and no linter/formatter configured. Verification =
|
||||
`swift build` and manual run.
|
||||
`swift build`, `go build ./...`, and manual run (`--sync-check` for sync).
|
||||
|
||||
## Bundle build & signing
|
||||
|
||||
@@ -102,5 +145,11 @@ brew upgrade --cask prompt
|
||||
- The double-Shift tap needs Accessibility; without it the event tap silently
|
||||
fails and only the Carbon fallbacks work. The panel banner and the status
|
||||
menu both surface the permission state.
|
||||
- `Info.plist` sets `NSAllowsArbitraryLoads` because the sync server URL is
|
||||
user-supplied and is often plain HTTP (LAN address, self-hosted box). ATS
|
||||
would otherwise block it with a confusing error.
|
||||
- The server keeps `data` as opaque `JSONB`, so the payload shape can evolve
|
||||
without a migration — but both Macs should run the same app version, since a
|
||||
newer client writing unknown fields will round-trip them to an older one.
|
||||
- `.gitignore` covers `.build/`, `Prompt.app/`, `dist/` — build artifacts
|
||||
should never be committed.
|
||||
|
||||
Reference in New Issue
Block a user