diff --git a/AGENTS.md b/AGENTS.md index c48e141..6142300 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ These are requirements, not preferences. Changing one needs the owner's agreemen - Never deploy unless the owner asks for it in the current turn. A finished change is reported undeployed; the owner deploys it, or asks you to. Never run `go install ./cmd/byre` or restart the service on your own initiative, not even to check that a change works. - `docs/operations.md` describes the layout, data, service and APIs; keep it current with every change. -- Never add tests. byre has no test suite; check a change with `go vet ./...`, `go generate ./web` (which type-checks the UI) and, when `mac/` changed, `swift build --package-path mac`. +- Never add tests. byre has no test suite; check a change with `go vet ./...`, `go generate ./web` (which type-checks the UI) and, when `mac/` changed, `mac/bundle.sh` (release only; debug builds are refused). - The web UI (`web/src/`) and the macOS app (`mac/Sources/Byre/`) are two clients of the same views. A change to what a client shows or does, such as Markdown rendering, goes into both in the same task; never leave one client behind. ## Deploying diff --git a/docs/macos.md b/docs/macos.md index 997dca2..fa7371e 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -18,7 +18,9 @@ The first version copies the web client: the same two equal panes, tabs, labels, - `UserNotifications` for notifications. - Third-party code: Apple's `swift-openapi-generator` (with `swift-openapi-runtime` and `swift-openapi-urlsession`) for the API types and client, and SwiftTerm for the terminal. -The host has the Command Line Tools and no Xcode (`xcode-select --print-path` prints which), so the project is a Swift package under `mac/`, built with `swift build`, not an Xcode project. `mac/bundle.sh` builds the release configuration and assembles `mac/.build/byre.app` (the executable, `mac/Info.plist` with the fixed bundle identifier `run.fc.byre.mac`, the `web/fonts/` files and an `.icns` rendered from `web/favicon-tile.svg`) and signs it ad hoc; notification permission needs the bundle identifier. The app keeps its server origin in the `byre.server` default (`defaults read run.fc.byre.mac byre.server` prints it). +The project is a Swift package under `mac/`, built with `swift build`, not an Xcode project. It builds with Xcode's toolchain: `xcode-select --print-path` must print `/Applications/Xcode.app/Contents/Developer`. With the Command Line Tools selected while Xcode is installed, Swift Build finds two toolchains for the Command Line Tools path and picks one at random on each run; the pick is part of the build request, so every change of pick rebuilds every target. `mac/bundle.sh` builds the release configuration and assembles `mac/.build/byre.app` (the executable, `mac/Info.plist` with the fixed bundle identifier `run.fc.byre.mac`, the `web/fonts/` files and an `.icns` rendered from `web/favicon-tile.svg`, cached as `mac/.build/byre.icns` until the SVG changes) and signs it ad hoc; notification permission needs the bundle identifier. + +Only the release configuration is built. A debug build stops at an `#error` in `ByreApp.swift`, and even a failed one leaves the next release build recompiling every target, since the two configurations share `mac/.build`. The generated API client is its own target, `ByreAPI`, so an edit to the app recompiles only the `Byre` target. SwiftPM builds release targets whole-module, so that is one compile of the whole `Byre` target, and a one-file change still costs that. The app keeps its server origin in the `byre.server` default (`defaults read run.fc.byre.mac byre.server` prints it). SwiftUI's own macros don't build here: the SDK declares `@State`, `@Entry`, `@Animatable` and the rest as macros implemented in the `SwiftUIMacros` plugin, which ships with Xcode and not with the Command Line Tools (`ls /Library/Developer/CommandLineTools/usr/lib/swift/host/plugins` lists the plugins there). `@State` is reached through its property-wrapper type instead, as `typealias ViewState = State` and `@ViewState var …`; other SwiftUI macros aren't used. `@Observable` and `@Bindable` (Observation's plugin, which the Command Line Tools have), `@Environment` and `@FocusState` work as usual. @@ -28,7 +30,7 @@ Values the web client defines are generated into Swift, never copied by hand, so | Value | Source | Swift | | --- | --- | --- | -| API types and client | `api/openapi.yaml` | Generated at build time by the `swift-openapi-generator` plugin (`mac/Sources/Byre/openapi-generator-config.yaml`, idiomatic naming: `display_name` is `displayName`) from the symlink `mac/Sources/Byre/openapi.yaml` to the spec. The output lands in `mac/.build/plugins/outputs/`, not in git | +| API types and client | `api/openapi.yaml` | Generated at build time into the `ByreAPI` target by the `swift-openapi-generator` plugin (`mac/Sources/ByreAPI/openapi-generator-config.yaml`, public access, idiomatic naming: `display_name` is `displayName`) from the symlink `mac/Sources/ByreAPI/openapi.yaml` to the spec. App files that use it `import ByreAPI`. The output lands in `mac/.build/plugins/outputs/`, not in git | | Palette, light and dark | Custom properties at the top of `web/style.css` (`:root` and its `prefers-color-scheme: dark` block) | `go generate ./mac` (`mac/palette`) writes `Palette.swift`: one dynamic `Color` per hex property, `--accent-tint` as `Palette.accentTint`, resolving light or dark with the window's appearance. Properties that aren't a hex colour (`--task`, `--shadow`, `--font`) are left out. The file is committed, so `swift build` needs no Go; a property missing from either block fails the generator | | Font | `web/fonts/` | The build script copies the woff2 files into the bundle; the app loads each with `CTFontManagerCreateFontDescriptorsFromURL` (see "Gaps in SwiftUI") | | App icon | `web/favicon-tile.svg` | The build script renders it to an `.icns` with `rsvg-convert` and `iconutil` | @@ -188,7 +190,7 @@ Platform differences the copy can't avoid, each with what the web client does an ## Building and checking -`swift build --package-path mac` is the compile check for changes under `mac/`, alongside the checks `AGENTS.md` already names. No tests are added. Installing the app is a separate action from deploying the byre service. +`mac/bundle.sh` is the compile check for changes under `mac/`, alongside the checks `AGENTS.md` already names. No tests are added. Installing the app is a separate action from deploying the byre service. ## Not in version one diff --git a/mac/Package.swift b/mac/Package.swift index c815d58..86a30d7 100644 --- a/mac/Package.swift +++ b/mac/Package.swift @@ -11,14 +11,21 @@ let package = Package( .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.1.0"), ], targets: [ - .executableTarget( - name: "Byre", + .target( + name: "ByreAPI", dependencies: [ - .product(name: "SwiftTerm", package: "SwiftTerm"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), - .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), ], plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] ), + .executableTarget( + name: "Byre", + dependencies: [ + "ByreAPI", + .product(name: "SwiftTerm", package: "SwiftTerm"), + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), + ] + ), ] ) diff --git a/mac/Sources/Byre/API.swift b/mac/Sources/Byre/API.swift index 9b0a2d1..421cdeb 100644 --- a/mac/Sources/Byre/API.swift +++ b/mac/Sources/Byre/API.swift @@ -1,3 +1,4 @@ +import ByreAPI import Foundation import HTTPTypes import OpenAPIRuntime diff --git a/mac/Sources/Byre/ByreApp.swift b/mac/Sources/Byre/ByreApp.swift index 6a18738..7984bc2 100644 --- a/mac/Sources/Byre/ByreApp.swift +++ b/mac/Sources/Byre/ByreApp.swift @@ -1,6 +1,10 @@ import AppKit import SwiftUI +#if DEBUG +#error("debug builds are unsupported: build with mac/bundle.sh") +#endif + typealias ViewState = State @MainActor diff --git a/mac/Sources/Byre/Changes.swift b/mac/Sources/Byre/Changes.swift index 997be65..1b087cc 100644 --- a/mac/Sources/Byre/Changes.swift +++ b/mac/Sources/Byre/Changes.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias TaskChanges = Components.Schemas.TaskChanges diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 2fe47b1..d39aba5 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -1,4 +1,5 @@ import AppKit +import ByreAPI import SwiftUI enum ChatKind: Equatable { diff --git a/mac/Sources/Byre/Composer.swift b/mac/Sources/Byre/Composer.swift index 2c136cb..1de4fff 100644 --- a/mac/Sources/Byre/Composer.swift +++ b/mac/Sources/Byre/Composer.swift @@ -1,4 +1,5 @@ import AppKit +import ByreAPI import OpenAPIRuntime import SwiftUI import UniformTypeIdentifiers diff --git a/mac/Sources/Byre/Files.swift b/mac/Sources/Byre/Files.swift index 910b894..bcca51e 100644 --- a/mac/Sources/Byre/Files.swift +++ b/mac/Sources/Byre/Files.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias DirListing = Components.Schemas.DirListing diff --git a/mac/Sources/Byre/Format.swift b/mac/Sources/Byre/Format.swift index 0ba0dba..fc16b3c 100644 --- a/mac/Sources/Byre/Format.swift +++ b/mac/Sources/Byre/Format.swift @@ -1,4 +1,5 @@ import AppKit +import ByreAPI import OpenAPIRuntime import SwiftUI diff --git a/mac/Sources/Byre/Inbox.swift b/mac/Sources/Byre/Inbox.swift index 1b48c9a..a16f176 100644 --- a/mac/Sources/Byre/Inbox.swift +++ b/mac/Sources/Byre/Inbox.swift @@ -1,3 +1,4 @@ +import ByreAPI import OpenAPIRuntime import SwiftUI diff --git a/mac/Sources/Byre/MergeDialog.swift b/mac/Sources/Byre/MergeDialog.swift index cb1cb38..c08589b 100644 --- a/mac/Sources/Byre/MergeDialog.swift +++ b/mac/Sources/Byre/MergeDialog.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias Participant = Components.Schemas.Participant diff --git a/mac/Sources/Byre/PaneFocus.swift b/mac/Sources/Byre/PaneFocus.swift index 7a19612..e3639af 100644 --- a/mac/Sources/Byre/PaneFocus.swift +++ b/mac/Sources/Byre/PaneFocus.swift @@ -1,4 +1,5 @@ import AppKit +import ByreAPI import SwiftUI @MainActor diff --git a/mac/Sources/Byre/Panes.swift b/mac/Sources/Byre/Panes.swift index bb882a6..57834a3 100644 --- a/mac/Sources/Byre/Panes.swift +++ b/mac/Sources/Byre/Panes.swift @@ -1,4 +1,5 @@ import AppKit +import ByreAPI import SwiftUI enum TabBadge { diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index 57279fa..eaef8f2 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias Todo = Components.Schemas.Todo diff --git a/mac/Sources/Byre/Reviews.swift b/mac/Sources/Byre/Reviews.swift index 6d00369..ee7a19b 100644 --- a/mac/Sources/Byre/Reviews.swift +++ b/mac/Sources/Byre/Reviews.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias FindingState = Components.Schemas.FindingState diff --git a/mac/Sources/Byre/Settings.swift b/mac/Sources/Byre/Settings.swift index 2969119..690a95d 100644 --- a/mac/Sources/Byre/Settings.swift +++ b/mac/Sources/Byre/Settings.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI import UserNotifications diff --git a/mac/Sources/Byre/Status.swift b/mac/Sources/Byre/Status.swift index 69f8a1d..3ce9361 100644 --- a/mac/Sources/Byre/Status.swift +++ b/mac/Sources/Byre/Status.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias AgentProcess = Components.Schemas.AgentProcess diff --git a/mac/Sources/Byre/Switcher.swift b/mac/Sources/Byre/Switcher.swift index fd343b6..f2fff4d 100644 --- a/mac/Sources/Byre/Switcher.swift +++ b/mac/Sources/Byre/Switcher.swift @@ -1,4 +1,5 @@ import AppKit +import ByreAPI import SwiftUI typealias WorkflowSummary = Components.Schemas.WorkflowSummary diff --git a/mac/Sources/Byre/Workflow.swift b/mac/Sources/Byre/Workflow.swift index 4be5de8..934d8c8 100644 --- a/mac/Sources/Byre/Workflow.swift +++ b/mac/Sources/Byre/Workflow.swift @@ -1,3 +1,4 @@ +import ByreAPI import SwiftUI typealias WorkflowRun = Components.Schemas.WorkflowRun diff --git a/mac/Sources/ByreAPI/ByreAPI.swift b/mac/Sources/ByreAPI/ByreAPI.swift new file mode 100644 index 0000000..f823880 --- /dev/null +++ b/mac/Sources/ByreAPI/ByreAPI.swift @@ -0,0 +1 @@ +import OpenAPIRuntime diff --git a/mac/Sources/Byre/openapi-generator-config.yaml b/mac/Sources/ByreAPI/openapi-generator-config.yaml similarity index 69% rename from mac/Sources/Byre/openapi-generator-config.yaml rename to mac/Sources/ByreAPI/openapi-generator-config.yaml index ee9d836..8e60f91 100644 --- a/mac/Sources/Byre/openapi-generator-config.yaml +++ b/mac/Sources/ByreAPI/openapi-generator-config.yaml @@ -1,5 +1,5 @@ generate: - types - client -accessModifier: internal +accessModifier: public namingStrategy: idiomatic diff --git a/mac/Sources/Byre/openapi.yaml b/mac/Sources/ByreAPI/openapi.yaml similarity index 100% rename from mac/Sources/Byre/openapi.yaml rename to mac/Sources/ByreAPI/openapi.yaml diff --git a/mac/bundle.sh b/mac/bundle.sh index 69c27c5..6640b3e 100755 --- a/mac/bundle.sh +++ b/mac/bundle.sh @@ -4,20 +4,24 @@ mac=$(cd "$(dirname "$0")" && pwd) root=$(dirname "$mac") build=$mac/.build app=$build/byre.app +svg=$root/web/favicon-tile.svg +icns=$build/byre.icns swift build --package-path "$mac" --configuration release -bin=$(swift build --package-path "$mac" --configuration release --show-bin-path) +if [ ! "$icns" -nt "$svg" ]; then + iconset=$build/byre.iconset + rm -rf "$iconset" + mkdir -p "$iconset" + for size in 16 32 128 256 512; do + rsvg-convert --width $size --height $size --output "$iconset/icon_${size}x${size}.png" "$svg" + rsvg-convert --width $((size * 2)) --height $((size * 2)) --output "$iconset/icon_${size}x${size}@2x.png" "$svg" + done + iconutil --convert icns --output "$icns" "$iconset" +fi rm -rf "$app" mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources/fonts" -cp "$bin/Byre" "$app/Contents/MacOS/Byre" +cp "$build/release/Byre" "$app/Contents/MacOS/Byre" cp "$mac/Info.plist" "$app/Contents/Info.plist" cp "$root"/web/fonts/*.woff2 "$app/Contents/Resources/fonts/" -iconset=$build/byre.iconset -rm -rf "$iconset" -mkdir -p "$iconset" -for size in 16 32 128 256 512; do - rsvg-convert --width $size --height $size --output "$iconset/icon_${size}x${size}.png" "$root/web/favicon-tile.svg" - rsvg-convert --width $((size * 2)) --height $((size * 2)) --output "$iconset/icon_${size}x${size}@2x.png" "$root/web/favicon-tile.svg" -done -iconutil --convert icns --output "$app/Contents/Resources/byre.icns" "$iconset" +cp "$icns" "$app/Contents/Resources/byre.icns" codesign --force --sign - "$app" echo "$app"