|
| 1 | +# Architecture |
| 2 | + |
| 3 | +A macOS CLI tool that exports Apple Notes to a Git repository as Markdown files, with optional Google Drive sync via rclone. Single-run binary — scheduling is handled externally via launchd. |
| 4 | + |
| 5 | +## Package Map |
| 6 | + |
| 7 | +``` |
| 8 | +cmd/apple-notes-sync/main.go Cobra CLI entrypoint, dependency wiring |
| 9 | +internal/ |
| 10 | + model/model.go Domain types: Note, Folder, Attachment, SyncResult |
| 11 | + shell/command.go CommandExecutor interface — the single mock seam for all subprocesses |
| 12 | + config/config.go Config loading via viper (CLI flags > ENV > YAML > defaults) |
| 13 | + logging/logger.go Zap logger factory (NewLogger) |
| 14 | + applescript/ |
| 15 | + scripts/*.applescript AppleScript files embedded via go:embed |
| 16 | + parser.go Parses delimiter-separated osascript output into structs |
| 17 | + extractor.go NoteExtractor interface + AppleScriptExtractor implementation |
| 18 | + converter/converter.go MarkdownConverter interface — HTML to Markdown via html-to-markdown/v2 |
| 19 | + filesystem/writer.go NoteWriter interface — writes .md files, saves attachments, cleans orphans |
| 20 | + gitops/git.go GitClient interface — init, add, commit, push via shell |
| 21 | + rclone/sync.go Syncer interface — rclone availability check + sync |
| 22 | + syncer/syncer.go Orchestrator — runs the full sync pipeline |
| 23 | +``` |
| 24 | + |
| 25 | +## Data Flow |
| 26 | + |
| 27 | +``` |
| 28 | +AppleScript (osascript) |
| 29 | + │ |
| 30 | + ▼ |
| 31 | +Parser (delimiter protocol → []model.Note with attachment metadata) |
| 32 | + │ |
| 33 | + ▼ |
| 34 | +ResolveAttachments (walks ~/Library/Group Containers/group.com.apple.notes/ to read attachment files) |
| 35 | + │ |
| 36 | + ▼ |
| 37 | +Filters (exclude folders, accounts, protected, shared) |
| 38 | + │ |
| 39 | + ▼ |
| 40 | +Converter (HTML → Markdown) |
| 41 | + │ |
| 42 | + ▼ |
| 43 | +Writer (Markdown files to disk + attachments to _attachments/) |
| 44 | + │ |
| 45 | + ▼ |
| 46 | +Git (add, commit, push) |
| 47 | + │ |
| 48 | + ▼ |
| 49 | +Rclone (sync to Google Drive) |
| 50 | +``` |
| 51 | + |
| 52 | +## Interfaces |
| 53 | + |
| 54 | +| Interface | Package | Implementation | Purpose | |
| 55 | +|-----------|---------|----------------|---------| |
| 56 | +| `CommandExecutor` | `shell` | `OSCommandExecutor` | Runs subprocesses (osascript, git, rclone) | |
| 57 | +| `NoteExtractor` | `applescript` | `AppleScriptExtractor` | Extracts notes/folders, resolves attachments | |
| 58 | +| `MarkdownConverter` | `converter` | `HTMLToMDConverter` | Converts Apple Notes HTML to Markdown | |
| 59 | +| `NoteWriter` | `filesystem` | `FSNoteWriter` | Writes notes/attachments to disk, cleans orphans | |
| 60 | +| `GitClient` | `gitops` | `ShellGitClient` | Git operations via subprocess | |
| 61 | +| `Syncer` (rclone) | `rclone` | `RcloneSyncer` | Rclone availability check + sync | |
| 62 | + |
| 63 | +## Key Design Decisions |
| 64 | + |
| 65 | +### Delimiter protocol for AppleScript output |
| 66 | +AppleScript has poor JSON support. Notes are output as fields separated by `|||FIELD|||` with records separated by `|||NOTE|||`. Attachments within a note use `|||ATTACH|||` and `|||AFIELD|||` sub-delimiters. The parser (`parser.go`) handles all delimiter splitting and date parsing. |
| 67 | + |
| 68 | +### CommandExecutor as single mock seam |
| 69 | +All external process calls (osascript, git, rclone) go through `shell.CommandExecutor`. Tests mock this one interface to control subprocess behavior. The syncer tests mock the higher-level interfaces (NoteExtractor, NoteWriter, etc.) instead. |
| 70 | + |
| 71 | +### go:embed for AppleScript files |
| 72 | +Scripts live in `internal/applescript/scripts/` and are embedded into the binary via `//go:embed scripts`. No external file dependencies at runtime. |
| 73 | + |
| 74 | +### Attachment resolution |
| 75 | +Attachments are extracted in two phases: |
| 76 | +1. **AppleScript** outputs attachment metadata (name + content identifier) per note |
| 77 | +2. **ResolveAttachments** walks `~/Library/Group Containers/group.com.apple.notes/` to build a filename→filepath index, then reads matching files. Content identifier is used for disambiguation when filenames collide. |
| 78 | + |
| 79 | +### Real filesystem in writer tests |
| 80 | +`filesystem` tests use `t.TempDir()` for real filesystem operations rather than mocking the FS. This catches real path handling issues. |
| 81 | + |
| 82 | +### Note file format |
| 83 | +Each `.md` file has: `# Title` heading at top, body content, then a `---` divider with a metadata table (ID, Created, Modified, Account, Shared) at the bottom. Configurable via `front_matter` setting. |
| 84 | + |
| 85 | +## Configuration |
| 86 | + |
| 87 | +Precedence: CLI flags > ENV vars (`ANS_` prefix) > YAML config (`~/.apple-notes-sync.yaml`) > defaults. |
| 88 | + |
| 89 | +Key config: `repo_path` (required), `git.enabled/push`, `rclone.enabled`, `attachments.enabled/max_size_mb`, `filter.*`, `clean_orphans`, `dry_run`, `front_matter`. |
| 90 | + |
| 91 | +See `configs/config.example.yaml` for full reference. |
| 92 | + |
| 93 | +## Test Strategy |
| 94 | + |
| 95 | +| Package | Approach | |
| 96 | +|---------|----------| |
| 97 | +| `shell` | Real subprocess calls (`echo`, `ls`) | |
| 98 | +| `config` | Temp YAML files via `t.TempDir()` | |
| 99 | +| `applescript/parser` | Pure functions, no mocks | |
| 100 | +| `applescript/extractor` | Mock `CommandExecutor` | |
| 101 | +| `converter` | Real html-to-markdown library | |
| 102 | +| `filesystem` | Real FS via `t.TempDir()` | |
| 103 | +| `gitops` | Mock `CommandExecutor` | |
| 104 | +| `rclone` | Mock `CommandExecutor` | |
| 105 | +| `syncer` | Mock all interfaces (extractor, converter, writer, git, rclone) | |
| 106 | + |
| 107 | +Coverage target: ≥80%. CI runs on ubuntu-latest — all tests mock osascript so macOS is not required. |
| 108 | + |
| 109 | +## Dependencies |
| 110 | + |
| 111 | +- `spf13/cobra` — CLI framework |
| 112 | +- `spf13/viper` — Configuration loading |
| 113 | +- `go.uber.org/zap` — Structured logging |
| 114 | +- `JohannesKaufmann/html-to-markdown/v2` — HTML→Markdown conversion |
| 115 | +- `stretchr/testify` — Test assertions and mocks |
0 commit comments