Files
busymirror/AGENTS.md
T
tomas.kracmar f625ecc263 Release 1.4.0
Fixes:
- Sandbox: add LaunchAgent temporary-exception entitlement
- Mirror URL: fix broken buildMirrorURL (URLComponents with ; separator)
- Cleanup: add bounds check to prevent crash on missing source
- State safety: pass MirrorConfig instead of mutating global @State
- KVC: remove misleading do-catch around setValue:forKey:
- Log cap: limit in-memory log to 2000 lines
- CLI: fix race with calendar loading
- launchCtl: separate stdout/stderr pipes

Features:
- Cancel button for long-running mirrors
- Progress indicator for multi-route runs (Route X of Y)
- Target event cache across routes

Code quality:
- Extract BlockMath, MirrorUtils, EventFilters, MirrorConfig
- Add 45 unit tests across 3 test files
- Refactor mergeGapMin to computed property
- Make log editor read-only

Build:
- Bump version to 1.4.0 (build 18)
- Add LSMinimumSystemVersion 15.5
2026-05-27 11:00:18 +02:00

155 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BusyMirror — Agent Reference
> This file is written for AI coding agents. It assumes you know nothing about the project.
## Project Overview
**BusyMirror** is a macOS menu-bar utility that mirrors calendar events from a source calendar into one or more target calendars, creating busy-placeholder events so availability stays consistent across accounts and devices.
It is a single-platform macOS app written in **Swift 5** and **SwiftUI**, using **EventKit** to read and write calendar data. The app runs as a menu-bar-only app (`LSUIElement`) with no Dock icon.
Key capabilities:
- Manual or route-driven multi-source mirroring
- Privacy modes: hide details (placeholder title) or mark events Private server-side (best-effort)
- DRY-RUN mode to preview changes without writing
- Scheduled headless runs via a self-installed `launchd` LaunchAgent
- Settings autosave/restore, plus Import/Export JSON
- CLI support for headless/scripted runs
## Technology Stack
| Layer | Technology |
|-------|------------|
| Language | Swift 5.0 |
| UI Framework | SwiftUI + AppKit (menu bar, panels) |
| Calendar API | EventKit (`EKEventStore`, `EKEvent`, `EKCalendar`) |
| Persistence | `UserDefaults` (JSON-encoded settings), `@AppStorage` |
| Scheduling | `launchd` / `launchctl` (user LaunchAgent) |
| Build System | Xcode project (`BusyMirror.xcodeproj`) + Makefile |
| Target OS | macOS 15.5+ |
| Signing | Ad-hoc (`CODE_SIGN_IDENTITY = "-"`) — not notarized |
No external Swift Package Manager dependencies are used. The project is self-contained.
## Project Structure
```
BusyMirror/
├── BusyMirrorApp.swift # App entry point; defines Window + MenuBarExtra
├── ContentView.swift # Main UI, mirror engine, settings, CLI, scheduling (≈2600 lines)
├── MenuBarSupport.swift # `BusyMirrorAppController` (state coordinator) + menu bar view
├── Info.plist # LSUIElement, calendar usage descriptions
├── BusyMirror.entitlements # App sandbox + calendar access entitlement
└── Assets.xcassets/ # AppIcon set and accent color
BusyMirror.xcodeproj/ # Xcode project
BusyMirrorTests/ # Empty (no tests implemented)
BusyMirrorUITests/ # Empty (no tests implemented)
```
**Architecture note:** nearly all business logic lives inside `ContentView.swift`. This includes:
- SwiftUI view body and subviews
- EventKit mirror engine (read, deduplicate, merge, create/update/delete)
- Settings serialization/deserialization
- CLI argument parsing
- `launchd` LaunchAgent installation/removal
- Logging to file and UI
When making changes, be aware that `ContentView.swift` is a large monolithic file. Extracting helpers is fine, but keep the existing data flow ( `@EnvironmentObject`, `@AppStorage`, `@State`) intact.
## Build and Release Commands
### Makefile targets
```bash
make build-debug # Debug build via xcodebuild
make build-release # Release build via xcodebuild
make sign-app # Ad-hoc sign the Release app (strip xattr, codesign)
make app # Verify signed app exists
make package # Create BusyMirror-<version>-macOS.zip + .sha256
make clean # Clean derived data
```
Built products:
- Unsigned release: `build/DerivedData/Build/Products/Release/BusyMirror.app`
- Signed release: `build/ReleaseSigned/BusyMirror.app`
### Xcode
1. Open `BusyMirror.xcodeproj`.
2. Select **BusyMirror** scheme → **My Mac**.
3. **Product → Build** (or **Archive** for distribution).
### Versioning
- `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` live in `project.pbxproj`.
- The Makefile extracts `MARKETING_VERSION` automatically for ZIP naming.
- Update both Debug and Release build configurations when bumping the version.
## Code Style Guidelines
- **Language:** all code, comments, and user-facing strings are in **English**.
- **Concurrency:** `@MainActor` is required on methods that mutate SwiftUI `@State` or call EventKit on the main thread. The compiler enforces strict concurrency.
- **Formatting:** standard Swift style (4-space indentation). No external linter is configured.
- **Logging:** use the `log(_:)` method inside `ContentView`; it appends to both the on-screen log editor and the persistent file log (`~/Library/Logs/BusyMirror/BusyMirror.log`).
- **Error handling:** EventKit errors are caught and logged; they must never crash the app. The file logger swallows its own errors silently.
## Testing
- **There are no implemented tests.** `BusyMirrorTests/` and `BusyMirrorUITests/` exist as Xcode targets but contain no source files.
- When adding logic, prefer extracting pure functions (e.g., block merging, gap calculation, filter logic) so they can be unit-tested later.
- Manual testing checklist for releases:
1. Grant Calendar permission.
2. Select a source and target, run DRY-RUN, verify log output.
3. Toggle WRITE and run Mirror Now; verify placeholders appear in the target calendar.
4. Move a source event and re-run; verify the placeholder updates.
5. Test Cleanup Placeholders (dry-run and write).
6. Add a route, install a schedule, verify the LaunchAgent plist is created in `~/Library/LaunchAgents/`.
7. Trigger a menu-bar sync and confirm the window opens if not visible.
## Security and Privacy Considerations
- **Calendar data:** the app reads and writes the users calendars via EventKit. It must handle permission denial gracefully.
- **Sandbox:** the app uses the macOS app sandbox (`com.apple.security.app-sandbox`) and the `com.apple.security.personal-information.calendars` entitlement.
- **Signing:** releases are ad-hoc signed only (`codesign --sign -`). They are **not notarized**. Gatekeeper may block the app on first launch; users may need to right-click → Open.
- **Private events:** the "Mark Private" feature uses Objective-C runtime tricks (`perform(Selector:)`, KVC `setValue:forKey:`) because EventKit does not expose a public privacy API. This is best-effort and may silently fail on some calendar providers (e.g., some Exchange configurations).
- **Loop guard:** a `sessionGuard` set prevents mirroring an event into the same target twice in one run, and prefix-based detection (`titlePrefix`) prevents re-mirroring already-mirrored placeholders.
- **Logging:** log files are written to the users `~/Library/Logs/BusyMirror/`. No log data is transmitted externally.
## CLI and Scheduling
The binary supports headless execution:
```bash
# Run saved routes (used by the LaunchAgent)
BusyMirror.app/Contents/MacOS/BusyMirror --run-saved-routes --write 1 --exit
# Manual route via 1-based UI indices
BusyMirror.app/Contents/MacOS/BusyMirror --routes "1->2,3" --write 1 --exit
```
Relevant flags: `--privacy`, `--copy-notes`, `--all-day`, `--days-forward`, `--days-back`, `--merge-gap-hours`, `--mode`, `--exclude-titles`, `--exclude-organizers`, `--cleanup-only`, `--exit`.
Scheduled runs are implemented by generating a `launchd` plist in `~/Library/LaunchAgents/com.cqrenet.BusyMirror.saved-routes.plist` and bootstrapping it with `launchctl`. The app removes and re-bootstraps the agent on every "Install Schedule" click.
## Key Files to Know
| File | Purpose |
|------|---------|
| `BusyMirror/ContentView.swift` | UI, business logic, mirror engine, settings, CLI, scheduling |
| `BusyMirror/BusyMirrorApp.swift` | App struct, window scene, menu-bar extra |
| `BusyMirror/MenuBarSupport.swift` | `@MainActor` app controller + menu bar SwiftUI view |
| `BusyMirror/Info.plist` | `LSUIElement`, calendar usage descriptions |
| `BusyMirror/BusyMirror.entitlements` | Sandbox + calendar entitlement |
| `Makefile` | Reproducible build, sign, and package targets |
| `CHANGELOG.md` | Release notes (human-readable) |
| `ROADMAP.md` | Planned features |
## Notes for Agents
- Do **not** add third-party dependencies unless the user explicitly asks. The project intentionally has zero external packages.
- If you refactor `ContentView.swift`, preserve `@AppStorage` keys and `UserDefaults` keys exactly; users have existing settings on disk.
- The mirror engine is tightly coupled to SwiftUI state. Extract helpers for testability, but do not break the `@MainActor`/`@State` flow without careful review.
- When modifying build settings, update both Debug and Release configurations in `project.pbxproj`, and update `CHANGELOG.md` if the change is user-visible.
- Do not run `git commit`, `git push`, or similar operations unless explicitly asked.