7.4 KiB
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):
.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<Stats, Error> |
@packages/inotes/Sources/INoteSync/SyncManager.swift (full file) |
Sender.swift |
Only if bidirectional | @packages/ical/Sources/ICalSync/Sender.swift |
Skeleton for SyncManager.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<ISafariSyncStats, ISafariSyncError> {
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<SafariSendTransport>. 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/<entity>/— schema migrations, types, reposrc/server/src/features/<module>/— service-level functions (syncBookmarks,queryBookmarks)src/server/src/surfaces/{client,my,admin}/<module>.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):
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):
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:
...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:
const BookmarksTab = lazy(() =>
import('./tabs/Bookmarks').then((m) => ({ default: m.BookmarksTab })),
);
// inside <Routes>:
<Route path="/bookmarks" element={<BookmarksTab />} />
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- isafarito themodules:list.CLAUDE.md:7-29— extend the structure block.- Add
docs/modules/isafari.mdmodelled ondocs/modules/inotes.md.
7. Tests
Mirror an existing module's Tests/<Module>SyncTests/ directory. Hermetic
test coverage is limited to what does not need the OS — see
modules/inotes.md 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.swifttarget + executable depReader.swift,APIClient.swift,SyncManager.swift(+Sender.swift)- Server
entities/<x>/migration + types + repo - Server
features/<x>/service - Server
surfaces/client/<x>.ts,surfaces/my/<x>.ts(+ admin if bidirectional) - Migrations registered in
app/server.ts - Routes registered in three
surfaces/*/index.ts - Web
api/<x>.ts,tabs/<X>/index.tsx, App.tsx route, AppShell nav entry app.manifest.yamlmodule listdocs/modules/<x>.md- Tests:
@packages/<x>/Tests/+src/server/src/features/<x>/__tests__/ swift testgreen,bun testgreen,bun run typecheckgreen