M3 follow-up: SETUP.md rewritten for single-project approach

Q: Should I just create an iOS target in the current scarf project?
   Would that be easier?
A: Yes — single scarf.xcodeproj with two targets (scarf + scarf-ios)
   is objectively easier than a separate scarf-ios.xcodeproj.

The original conservative recommendation (separate xcodeproj) was
rooted in my not wanting to hand-edit pbxproj. But you're the one
clicking through Xcode's UI to create the target, not me — Xcode
handles multi-target multi-platform projects natively, with zero
risk to the existing Mac target.

Rewrote SETUP.md to describe the single-project flow:

  - `File → New → Target` inside the existing project (not a new
    project file).
  - Both targets share the same SPM package references — ScarfCore
    is already there for Mac, you just add it + ScarfIOS to the
    scarf-ios target via General → Frameworks.
  - One Xcode window, one scheme switcher, unified signing/team
    settings.

Also threaded in M3-specific smoke-test steps (connect to a real
host → see Dashboard load via Citadel SFTP snapshot) and added a
post-M3 troubleshooting entry for the `Cannot find 'Process' in
scope` error — it should never appear now that makeProcess is
`#if !os(iOS)`-guarded, but if it does it's a leaked Mac-only file
in the scarf-ios target membership.

Milestone status table in SETUP.md updated to reflect M3 shipped.

https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
This commit is contained in:
Claude
2026-04-22 23:37:41 +00:00
parent e85a7b170c
commit 92ac102f46
+150 -125
View File
@@ -1,170 +1,195 @@
# scarf-ios — Xcode target setup
This folder contains the source tree for the iOS app (`scarf-ios`), but
**not** the Xcode project file. Creating the `.xcodeproj` is a one-time
step you do in Xcode's UI — it's about 5 minutes, and doing it by hand
produces a project file that's definitely correct for whichever Xcode
version you're running, rather than a hand-edited pbxproj that might
drift from Xcode's expectations.
This folder contains the source tree for the iOS app (`scarf-ios`).
The Xcode target itself is added to the **existing `scarf.xcodeproj`**
as a second target alongside the Mac `scarf` target — **not** a
separate `.xcodeproj`. One project, two targets.
Everything the app needs — SSH key generation, Keychain storage,
onboarding state machine, Citadel-backed connection testing — already
lives in the shared SPM packages (`Packages/ScarfCore`,
`Packages/ScarfIOS`) and is exercised by 88 passing unit tests on
Linux CI. The Xcode target is mostly just a wrapper that assembles
those packages behind a `@main` SwiftUI app.
onboarding state machine, Citadel-backed SSH client, SQLite-backed
Dashboard — already lives in the shared SPM packages
(`Packages/ScarfCore`, `Packages/ScarfIOS`) and is exercised by
96 passing unit tests on Linux CI. The iOS target is mostly just a
wrapper that assembles those packages behind a `@main` SwiftUI app.
## One-time: create the Xcode target
Creating the target is a one-time ~5-minute step in Xcode's UI.
1. Open **Xcode****File → New → Project…**
2. Choose **iOS → App**. Click Next.
3. Fill in:
## One-time: add the iOS target to the existing project
1. Open `scarf/scarf.xcodeproj` in **Xcode**.
2. Select the project in the Project Navigator.
3. **File → New → Target…** (or use the `+` button at the bottom of
the targets list).
4. Choose **iOS → App**. Click Next.
5. Fill in:
- **Product Name**: `scarf-ios`
- **Team**: `3Q6X2L86C4` (the same team the Mac app uses for notarization)
- **Team**: `3Q6X2L86C4` (the same team the Mac target uses)
- **Organization Identifier**: `com.scarf`
- **Bundle Identifier** will be `com.scarf.scarf-ios`
- **Interface**: **SwiftUI**
- **Language**: **Swift**
- **Storage**: **None** (no Core Data, no CloudKit)
- **Storage**: **None**
- **Include Tests**: unchecked (SPM covers them)
4. On the save-location sheet, navigate to `<repo>/scarf/` (the same
level as `scarf.xcodeproj`) and hit **Create**.
5. Xcode produces `<repo>/scarf/scarf-ios/scarf-ios.xcodeproj` and a
default source tree you'll immediately throw away.
6. Click **Finish**. Xcode creates the target plus a default source
tree under `scarf/scarf-ios/` (a folder you'll immediately throw
away, since our real source lives there already).
7. If Xcode asks about "Activate scheme": **yes**.
## One-time: set project settings
## One-time: target settings
In the **scarf-ios** project (target of the same name):
With the `scarf-ios` target selected in the project editor:
1. **General → Minimum Deployments → iPhone**: `iOS 18.0`.
2. **General → Supported Destinations**: keep **iPhone** only. Remove
iPad + Mac Catalyst + Vision.
3. **Info → Bundle Identifier**: `com.scarf.scarf-ios`.
4. **Signing & Capabilities → Team**: `3Q6X2L86C4` (same as Mac).
5. **Build Settings → Swift Language Version**: `Swift 5` (matches
the Mac app and both SPM packages).
3. **Signing & Capabilities → Team**: `3Q6X2L86C4` (should have
auto-filled from step 5 above).
4. **Build Settings → Swift Language Version**: `Swift 5` (matches
the Mac target + both SPM packages).
## One-time: wire the SPM packages
## One-time: link the SPM packages to the iOS target
1. **File → Add Package Dependencies…**
2. **Add Local…** button in the lower-left of the dialog.
3. Select `<repo>/scarf/Packages/ScarfCore`. Click **Add Package**.
4. Target to attach it to: **scarf-ios**. Click **Add Package**.
5. Repeat steps 14 for `<repo>/scarf/Packages/ScarfIOS`.
- `ScarfIOS` already declares `Citadel` as a dependency — Xcode
will resolve it automatically when you add the local package.
- On first resolution expect a ~30s wait while Citadel +
SwiftNIO-SSH fetch from GitHub.
`ScarfCore` and `ScarfIOS` are already in the project's package
references (the Mac target already uses ScarfCore). You only need to
add them to the NEW iOS target.
1. Select the project in the navigator → select the `scarf-ios`
target → **General → Frameworks, Libraries, and Embedded
Content**.
2. Click the `+` button.
3. Select **ScarfCore** (library from ScarfCore local package). Add.
4. Click `+` again. Select **ScarfIOS**. Add.
5. Citadel is pulled in transitively by ScarfIOS — no need to add it
explicitly. On first build Xcode resolves it from GitHub
(~30s-1min).
## One-time: replace the default source tree
1. In Xcode's Project Navigator, delete the auto-generated files
Xcode created for you:
1. In Xcode's Project Navigator, find the `scarf-ios` group Xcode
created. Delete:
- `scarf_iosApp.swift` (or `scarf-iosApp.swift`)
- `ContentView.swift`
- **`Assets.xcassets`** — yes, delete this one too. We ship a
pre-built `Assets.xcassets/` with the app icon + accent
color inside `<repo>/scarf/scarf-ios/` that we'll add in the
next step.
2. In Finder, open `<repo>/scarf/scarf-ios/`. Drag **App/**,
**Onboarding/**, **Dashboard/**, and **`Assets.xcassets/`** onto
the `scarf-ios` target in Xcode's navigator.
- In the import sheet: **Create groups**, **Add to target:
scarf-ios**.
3. Build (`⌘B`). It should compile cleanly against Citadel 0.12.1
— every API call in `CitadelSSHService` was cross-checked
against the 0.12.1 tag in April 2026. If you've bumped the pin
to 0.13+ and something fails here, the likeliest culprit is
`SSHAuthenticationMethod.ed25519(username:privateKey:)` being
renamed or refactored; check the current
`Sources/Citadel/SSHAuthenticationMethod.swift` in the new
release.
- **`Assets.xcassets`** — we ship our own pre-built one.
2. In Finder, open `<repo>/scarf/scarf-ios/`. Drag these four folders
onto the `scarf-ios` group in Xcode:
- `App/`
- `Onboarding/`
- `Dashboard/`
- `Assets.xcassets/`
3. In the import sheet:
- **Destination**: Copy items if needed — **unchecked** (they're
already in place).
- **Added folders**: **Create groups**.
- **Add to targets**: **scarf-ios** only.
4. Build (`⌘B`). Should compile cleanly against Citadel 0.12.1 —
every API call in `CitadelSSHService` + `CitadelServerTransport`
was cross-checked against the 0.12.1 tag. If you've bumped the
pin to 0.13+ and something fails, check
`Sources/Citadel/SSHAuthenticationMethod.swift` for the current
`.ed25519(username:privateKey:)` spelling.
## App icon + accent color
The `Assets.xcassets/` inside `<repo>/scarf/scarf-ios/` ships with:
Already in `Assets.xcassets/` so you don't configure anything:
- **`AppIcon.appiconset/AppIcon-1024.png`** — the 1024×1024 Scarf
icon from the Mac app's icon set. iOS 14+ renders all smaller
sizes automatically from the single 1024 image.
- **`AccentColor.colorset`** — a custom Scarf teal (`RGB
0.227 / 0.525 / 0.722` in light mode; lighter `0.400 / 0.690
/ 0.902` in dark mode). If you want a different accent, swap
the sRGB components in `Contents.json` or edit in Xcode's
color picker.
icon copied from the Mac app's icon set. iOS 14+ renders all
smaller sizes automatically from the single 1024 image.
- **`AccentColor.colorset`** — custom Scarf teal (sRGB `0.227 /
0.525 / 0.722` light mode; `0.400 / 0.690 / 0.902` dark). Edit
`Contents.json` or Xcode's color picker to change.
## Info.plist additions for M2
## Info.plist for TestFlight
None required. Citadel uses SwiftNIO, which doesn't need the
network-usage `Info.plist` key unless you hit local-network
discovery — which onboarding doesn't.
Under the scarf-ios target → **Info → Custom iOS Target Properties**:
If you want to publish to TestFlight in M2, add these under
**Info → Custom iOS Target Properties**:
- `LSRequiresIPhoneOS = YES` (defaults to YES, usually already set)
- `LSRequiresIPhoneOS = YES` (usually defaulted)
- `UIApplicationSceneManifest → UIApplicationSupportsMultipleScenes = NO`
(single-window for iPhone)
- `UILaunchScreen` — empty dictionary is fine.
(iPhone single-window)
- `UILaunchScreen` — empty dictionary is fine
## Smoke test the target
Citadel uses SwiftNIO, not Apple's local-network discovery, so no
network-usage-description key is needed.
1. Pick an iPhone simulator (any iPhone running iOS 18+) and hit ⌘R.
2. You should see the onboarding flow: **Remote host** form → **SSH
key** choice → **Generate** → **Show public key** → …
3. On **Show public key**, the OpenSSH line is selectable and
copy-able. The text renders as `ssh-ed25519 AAAA… scarf-iphone-XXXX`.
4. Tap **I've added this key**. Onboarding calls `CitadelSSHService`
to connect. With no real server, this will fail with
`.hostUnreachable` — that's expected. You should land on the
**Connection failed** screen with a "Retry" button.
5. Real end-to-end test: use a host you actually own, copy the shown
public key into its `~/.ssh/authorized_keys`, tap Retry. You
should reach the **Connected** state and then the placeholder
**Dashboard** with a Disconnect button.
## Smoke test
## TestFlight (still M2 — optional)
1. Switch the run destination to an iPhone simulator (any iPhone
running iOS 18+). Xcode's target switcher lets you toggle between
scarf (macOS) and scarf-ios.
2. ⌘R. Expect the onboarding flow:
**Remote host** → **SSH key choice** → **Generate** → **Show
public key** → **I've added this** → **Test connection** → ...
3. On **Show public key**, the OpenSSH line is selectable + copyable
(`ssh-ed25519 AAAA… scarf-iphone-XXXX`).
4. Without a real SSH server, **Test connection** will fail with
`.hostUnreachable` — that's expected. Land on the **Connection
failed** screen with a **Retry** button.
5. Real end-to-end: use a host you own. Copy the shown public key
into `~/.ssh/authorized_keys` on that host. Tap **Retry**. You
should reach **Connected** → **Dashboard**, which then does a
Citadel SFTP snapshot of `~/.hermes/state.db` and renders
session + token stats.
1. **Product → Archive** (select a physical iPhone or "Any iOS
Device (arm64)" as the target).
2. **Window → Organizer → Archives → Distribute App → App Store
Connect → Upload**. Uses your existing team signing setup.
3. First upload will trigger App Store Connect to create the app
record if it doesn't exist. Give it `com.scarf.scarf-ios` and
the same team.
4. After processing, invite testers from App Store Connect.
## TestFlight upload
## What's *not* in M2
1. Scheme selector top-left → **scarf-ios**. Destination: **Any iOS
Device (arm64)** or a physical iPhone.
2. **Product → Archive**.
3. **Window → Organizer → Archives → Distribute App → App Store
Connect → Upload**.
4. First upload creates the App Store Connect app record with
`com.scarf.scarf-ios`. Same team as the Mac app.
5. Invite testers from App Store Connect.
- Dashboard data (sessions, messages, stats) — **M3** adds a
Citadel-backed `ServerTransport` so `HermesDataService` and friends
work over SSH from iOS.
- Chat — **M4** adds an `SSHExecACPChannel` (the iOS counterpart to
`ProcessACPChannel`) so `ACPClient` runs over a Citadel exec
session.
- Memory editing, Cron, Skills, Settings — **M5**.
- Polish, App Store submission — **M6**.
## What's in each milestone
- **M2** — Onboarding (SSH key + Keychain), Citadel-based
"Test Connection", Dashboard placeholder.
- **M3** — Real Dashboard via `CitadelServerTransport` +
`HermesDataService` (you're running the M3 PR now).
- **M4** — Chat over an iOS `SSHExecACPChannel`.
- **M5** — Memory editing, Cron, Skills, Settings.
- **M6** — Polish + App Store submission.
## Troubleshooting
**Citadel fails to resolve.** Delete derived data (`~/Library/Developer/Xcode/DerivedData/scarf-ios-*`)
and `File → Packages → Reset Package Caches`, then rebuild.
**Citadel fails to resolve.** Delete DerivedData
(`~/Library/Developer/Xcode/DerivedData/scarf-*`) and **File →
Packages → Reset Package Caches**, then rebuild.
**`SSHAuthenticationMethod` has no member `ed25519`.** Shouldn't
happen against Citadel 0.12.1 (verified), but historically the
private-key variant names have changed between minor versions
(0.7 → 0.9 → 0.12). See `CitadelSSHService.buildClientSettings(...)`
there's one line to
update. Keep the protocol conformance intact.
**`Cannot find 'Process' in scope` when building scarf-ios.** Should
not happen post-M3 — the `makeProcess` protocol method is now
`#if !os(iOS)`-guarded. If you see this: grep the scarf-ios target's
source files for `Process()`, `process.isRunning`, or `terminationHandler`
something Mac-only leaked into the iOS target's membership.
**Keychain reads empty after relaunch.** Check that you haven't
accidentally set `kSecAttrAccessible` to a value that requires
biometric unlock — M2 uses `AfterFirstUnlockThisDeviceOnly` which
should always be readable.
**`SSHAuthenticationMethod` has no member `ed25519`.** Shouldn't happen
against Citadel 0.12.1 (verified), but historically the private-key
variant names have changed between minor versions (0.7 → 0.9 → 0.12).
See `CitadelSSHService.buildClientSettings(...)` in ScarfIOS — one
line to update. Keep the protocol conformance intact.
**The shown public-key line doesn't match what OpenSSH generates
from the private key.** It won't — `scarf-ios` uses a compact
internal PEM shape for the private key (see `Ed25519KeyGenerator`
for the format). The **public** key is standard OpenSSH wire format
and is interop-safe with `authorized_keys`. If you want to export
the private key for use with `ssh`, that export flow is deferred
to a future phase.
**Dashboard shows "Couldn't read the Hermes database".** Expected if
the host's `~/.hermes/state.db` doesn't exist yet (fresh Hermes
install). Start a Hermes session on the host first, then pull-to-
refresh.
**Dashboard spins forever on first connect.** Citadel connection
hand-shake + SFTP open can take 5-10s on a cold network. If it's
longer, check that the same SSH key works from a regular `ssh` client
against the same host — sometimes the issue is an authorized_keys
line with a trailing whitespace, or the host uses a restricted shell
that blocks `sqlite3`.
**Keychain reads empty after relaunch.** Check you haven't set
`kSecAttrAccessible` to a biometric-required value. We use
`AfterFirstUnlockThisDeviceOnly`, which is readable any time after
the first post-boot unlock.
**The public-key line doesn't match what `ssh-keygen` would produce
from the same private key.** The **public** key is standard OpenSSH
wire format (interop-safe with `authorized_keys`). The **private**
key uses a compact Scarf-internal PEM (see
`ScarfIOS/Ed25519KeyGenerator.swift`) — it's not directly exportable
to `~/.ssh/id_ed25519`. A future phase adds an export flow that
re-serializes to standard OpenSSH PEM.