# Adding a module A walk-through for adding a 7th module — say, `ISafariSync` for syncing Safari bookmarks — following the patterns already in place for the six shipping modules. Every step cites where the equivalent already lives. ## 0. Decide direction and OS surface - **Direction**: read-only (like iPhoto), 2-way via SendQueueClient (like iCal), or 2-way via AppleScript (like iNotes)? - **OS surface**: which framework? Safari bookmarks live in `~/Library/Safari/Bookmarks.plist`; assume a plist reader for this example. - **Interval**: how often to tick? Cheap reads (EventKit) every 300s; slow AppleScript every 600s; near-realtime queries (chat.db) every 30s. Document this in a one-line summary you can paste into the README table. ## 1. SwiftPM target Add a target block in `Package.swift` modelled on `Package.swift:80-87` (`IReminderSync` is the closest analogue): ```swift .target( name: "ISafariSync", dependencies: ["MacSyncShared"], path: "@packages/isafari/Sources/ISafariSync" // linkerSettings only if you need a framework like EventKit ), .testTarget( name: "ISafariSyncTests", dependencies: ["ISafariSync"], path: "@packages/isafari/Tests/ISafariSyncTests" ), ``` Add `"ISafariSync"` to the `MacSyncApp` executable dependencies block (`Package.swift:97-110`). ## 2. Module source files Create four files under `@packages/isafari/Sources/ISafariSync/`: | File | Role | Example to copy | |-------------------|-----------------------------------------------------|------------------------------------------------------------------------------| | `Reader.swift` | Talks to the OS (plist parse, EventKit, AppleScript)| `@packages/inotes/Sources/INoteSync/Reader.swift` | | `APIClient.swift` | Typed HTTPS calls over `BaseAPIClient` | `@packages/inotes/Sources/INoteSync/APIClient.swift` (esp. lines 60-130) | | `SyncManager.swift`| Subclass of `BaseSyncManager` | `@packages/inotes/Sources/INoteSync/SyncManager.swift` (full file) | | `Sender.swift` | Only if bidirectional | `@packages/ical/Sources/ICalSync/Sender.swift` | Skeleton for `SyncManager.swift`: ```swift import MacSyncShared public struct ISafariSyncStats: Equatable, Sendable { public var bookmarkCount: Int = 0 public init() {} } public enum ISafariSyncError: Equatable, Sendable { case none case bookmarksUnavailable case connectionFailed(String) } @MainActor public final class SyncManager: BaseSyncManager { public static let shared = SyncManager() private init() { super.init( initialStats: ISafariSyncStats(), noError: .none, persistenceKey: "isafari", // becomes the UserDefaults watermark namespace timerInterval: 600 ) } public override func performSync() async { // Reader.fetch -> APIClient.sync(payloads) -> stats update } } ``` If bidirectional, also override `didStartSync()` / `willStopSync()` and own a `lazy var sendQueueClient: SendQueueClient`. The two existing patterns: - EventKit: `@packages/ical/Sources/ICalSync/SyncManager.swift:54-73` - AppleScript: `@packages/inotes/Sources/INoteSync/SyncManager.swift:53-72` ## 3. Server entities, features, surfaces The repo follows a strict three-layer split: - `src/server/src/entities//` — schema migrations, types, repo - `src/server/src/features//` — service-level functions (`syncBookmarks`, `queryBookmarks`) - `src/server/src/surfaces/{client,my,admin}/.ts` — HTTP routes Files to create for read-only iSafari (mirror `inotes/`): ``` src/server/src/entities/bookmark/{schema.ts,types.ts,repo.ts,index.ts} src/server/src/features/isafari/{index.ts,syncService.ts} src/server/src/surfaces/client/isafari.ts src/server/src/surfaces/my/bookmarks.ts ``` If bidirectional, also create: ``` src/server/src/entities/bookmarkSendItem/{schema.ts,types.ts,index.ts} src/server/src/features/isafari/sendService.ts src/server/src/surfaces/admin/bookmark-send-queue.ts ``` `bookmarkSendItem/schema.ts` reuses the table-builder (`src/server/src/entities/noteSendItem/schema.ts:6-8` for the pattern): ```ts import { sendQueueTableSql } from '@/shared/sendQueue/SendQueueRepo'; export const bookmarkSendItemMigrations = [ { id: '2026-05-13_bookmark_send_queue_initial', sql: sendQueueTableSql('icloud.bookmark_send_queue') }, ]; ``` `features/isafari/sendService.ts` (mirroring `src/server/src/features/inotes/sendService.ts:11-19`): ```ts const BOOKMARK_SEND_ACTIONS = ['create_bookmark', 'update_bookmark', 'delete_bookmark'] as const; const payloadSchema = z.object({ /* ... */ }); const repo = createSendQueueRepo({ tableName: 'icloud.bookmark_send_queue', payloadSchema, allowedActions: BOOKMARK_SEND_ACTIONS, }); ``` ## 4. Wire the routes Three index files need a one-line edit each (the rest of the surface tree is discovered automatically): - `src/server/src/surfaces/client/index.ts:11-19` — add `.route('/isafari', isafariClientRouter)` - `src/server/src/surfaces/my/index.ts:11-19` — add `.route('/bookmarks', bookmarksMyRouter)` - `src/server/src/surfaces/admin/index.ts:10-17` — add `.route('/bookmark-send-queue', bookmarkSendQueueAdminRouter)` if bidirectional Migrations also need registration in `src/server/src/app/server.ts:36-52`: ```ts ...bookmarkMigrations, ...bookmarkSendItemMigrations, // if bidirectional ``` ## 5. Web tab Create `web/src/api/bookmarks.ts` and `web/src/tabs/Bookmarks/index.tsx`, modelled on `web/src/api/notes.ts` and `web/src/tabs/Notes/index.tsx`. Then add a route in `web/src/App.tsx:55-65`: ```tsx const BookmarksTab = lazy(() => import('./tabs/Bookmarks').then((m) => ({ default: m.BookmarksTab })), ); // inside : } /> ``` And a nav entry inside `web/src/layout/AppShell` (not pasted here — copy the Notes entry). ## 6. App manifest & docs - `app.manifest.yaml:6-12` — append `- isafari` to the `modules:` list. - `CLAUDE.md:7-29` — extend the structure block. - Add `docs/modules/isafari.md` modelled on `docs/modules/inotes.md`. ## 7. Tests Mirror an existing module's `Tests/SyncTests/` directory. Hermetic test coverage is limited to what does not need the OS — see [modules/inotes.md](./modules/inotes.md#tests) for the breakdown. For the server, follow `src/server/src/shared/sendQueue/__tests__/SendQueueRepo.test.ts` (plan reference: `~/.claude/plans/magical-tumbling-peach.md:66`) for queue round-trip tests against a test database. ## Checklist - [ ] `Package.swift` target + executable dep - [ ] `Reader.swift`, `APIClient.swift`, `SyncManager.swift` (+ `Sender.swift`) - [ ] Server `entities//` migration + types + repo - [ ] Server `features//` service - [ ] Server `surfaces/client/.ts`, `surfaces/my/.ts` (+ admin if bidirectional) - [ ] Migrations registered in `app/server.ts` - [ ] Routes registered in three `surfaces/*/index.ts` - [ ] Web `api/.ts`, `tabs//index.tsx`, App.tsx route, AppShell nav entry - [ ] `app.manifest.yaml` module list - [ ] `docs/modules/.md` - [ ] Tests: `@packages//Tests/` + `src/server/src/features//__tests__/` - [ ] `swift test` green, `bun test` green, `bun run typecheck` green