Skip to content

Commit b1a3c9e

Browse files
Tell strangers the first-run order and name --ios-version on missing runtimes.
Why: Public Use it mixed install options with hello world and did not say the app is the preferred host setup. Hello world acquired a lease without stopping on unavailable or repair_needed capacity. The app missing-CLI screen led with SIMBROKER_CLI_PATH. host init --bootstrap-config failed with runtime-not-found without naming --ios-version. Changed: README and getting-started now order Homebrew CLI, cask, then Set Up This Mac / Complete first-time setup (or host init --bootstrap-config and service start) before hello world. Capacity check gates lease acquire. Missing-CLI copy leads with the brew formula and Refresh. runtime-not-found errors name --ios-version and xcrun simctl list runtimes. Default starter iOS stays 18. Verification: npm run agent:verify -- --profile spec-only --paths README.md,docs/getting-started.md,docs/status.md,docs/test/front-door.test.mjs,CHANGELOG.md,spec/build-and-test.md,app/Sources/SharedViews.swift,app/Sources/BrokerOnboardingCommands.swift,app/Tests/BrokerOnboardingCommandsTests.swift,broker-core/index.mjs,broker-core/test/broker-core.test.mjs,broker-core/test/support/simctl-fixture.mjs,client/test/simbroker.test.mjs,client/command-dispatch.mjs --session-dir task-sessions/first-run-onboarding-20260819 npm run agent:verify -- --profile implementation --paths README.md,docs/getting-started.md,docs/status.md,docs/test/front-door.test.mjs,CHANGELOG.md,spec/build-and-test.md,app/Sources/SharedViews.swift,app/Sources/BrokerOnboardingCommands.swift,app/Tests/BrokerOnboardingCommandsTests.swift,broker-core/index.mjs,broker-core/test/broker-core.test.mjs,broker-core/test/support/simctl-fixture.mjs,client/test/simbroker.test.mjs,client/command-dispatch.mjs --session-dir task-sessions/first-run-onboarding-20260819 node --test docs/test/front-door.test.mjs npm run test:app:focus -- SimulatorBrokerAppTests/BrokerOnboardingCommandsTests Affected: README.md docs/getting-started.md docs/status.md docs/test/front-door.test.mjs CHANGELOG.md spec/build-and-test.md app/Sources/BrokerOnboardingCommands.swift app/Sources/SharedViews.swift app/Tests/BrokerOnboardingCommandsTests.swift broker-core/index.mjs broker-core/test/broker-core.test.mjs broker-core/test/support/simctl-fixture.mjs client/command-dispatch.mjs client/test/simbroker.test.mjs Refs: spec/build-and-test.md docs/getting-started.md Session: task-sessions/first-run-onboarding-20260819
1 parent 6223cb7 commit b1a3c9e

14 files changed

Lines changed: 266 additions & 34 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Public first-run order is Homebrew CLI, Homebrew cask, then **Set Up This
13+
Mac** / **Complete first-time setup** (or `host init --bootstrap-config`
14+
and `service start`). Hello world runs only after a host config exists
15+
and after `capacity check --purpose agent-ui-session`; stop on
16+
`unavailable` or `repair_needed` and use `doctor` / `simulators repair`.
17+
Homebrew does not create simulators.
18+
- The app **Finish Local Broker Installation** copy leads with
19+
`brew install fiveonecode/simulator-broker/simbroker` and **Refresh**.
20+
- `host init --bootstrap-config` `runtime-not-found` errors name
21+
`--ios-version` and `xcrun simctl list runtimes`. Default starter iOS
22+
stays `18`.
23+
1024
### Added
1125

1226
- Homebrew formula `Formula/simbroker.rb` installs the Alpha CLI tarball

README.md

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ If you only ever use one simulator by hand, you may not need this yet.
2727

2828
## Use it
2929

30-
One-line CLI install on macOS, with Node.js 20+ still required at runtime:
30+
Install the Homebrew CLI, then the Homebrew cask, then set up this Mac.
31+
The five-minute hello world is only after a host config exists. Homebrew
32+
does not create Simulator devices. Node.js 20+ is still required at
33+
runtime.
3134

3235
```bash
3336
brew install fiveonecode/simulator-broker/simbroker
@@ -39,6 +42,28 @@ Homebrew clones
3942
for that tap name. `Formula/` and `Casks/` in this repository stay the
4043
source of truth.
4144

45+
```bash
46+
brew install --cask fiveonecode/simulator-broker/simulator-broker
47+
```
48+
49+
That cask downloads `Simulator-Broker-<version>.zip` from
50+
[GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases).
51+
52+
The app is the preferred first-run host setup. Open `Simulator Broker.app`.
53+
If it shows **Set Up This Mac**, click **Complete first-time setup**. That
54+
runs the same broker commands as:
55+
56+
```bash
57+
simbroker host init --bootstrap-config
58+
simbroker service start
59+
```
60+
61+
`host init --bootstrap-config` prints a warning and then creates real
62+
Simulator devices. Do not run it casually on a machine whose simulator
63+
inventory you cannot afford to change.
64+
65+
Other CLI install options:
66+
4267
```bash
4368
npm install -g https://github.com/fiveonecode/simulator-broker/releases/download/v0.1.0-alpha.1/simbroker-0.1.0-alpha.1.tgz
4469
simbroker --help
@@ -58,27 +83,14 @@ build the macOS app. If Homebrew is installed, `simbroker` lands in
5883
and one guarded login-shell PATH line. Open a new terminal if this shell still
5984
cannot resolve `simbroker`. `source .../env.sh` remains a fallback.
6085

61-
The Homebrew cask installs the signed, notarized operator app:
62-
63-
```bash
64-
brew install --cask fiveonecode/simulator-broker/simulator-broker
65-
```
66-
67-
That cask downloads `Simulator-Broker-<version>.zip` from
68-
[GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases).
69-
7086
Xcode is still required to create and run iOS Simulators. Alpha CLI tarballs
7187
are also attached to those releases. Extract a tarball and run
7288
`./bin/simbroker --help`.
7389

7490
`simbroker` help and `simbroker doctor` print human-readable text by default.
7591
Pass `--json` for machine-readable payloads.
7692

77-
First-run host setup is `simbroker host init --bootstrap-config`. It prints a
78-
warning and then creates real Simulator devices. Do not run it casually on a
79-
machine whose simulator inventory you cannot afford to change.
80-
81-
To install the operator app as well, use the contributor command in
93+
To build the operator app from this checkout, use the contributor command in
8294
[Develop it](#develop-it).
8395

8496
## Develop it
@@ -100,21 +112,28 @@ keep the `agent:context` / `agent:verify` / `agent:complete` track.
100112

101113
## Five-minute hello world
102114

103-
After the CLI resolves and this Mac has a host config:
115+
After the CLI resolves and this Mac has a host config from the first-run step
116+
above:
104117

105118
```bash
106119
mkdir -p /tmp/sample-broker-repo && cd /tmp/sample-broker-repo
107120
simbroker project init
108121
simbroker project validate
109122
simbroker capacity check --purpose agent-ui-session --json
123+
```
124+
125+
If that check is `unavailable` or `repair_needed`, stop. Run
126+
`simbroker doctor` and `simbroker simulators repair` instead of acquiring
127+
a lease.
128+
129+
If the purpose is available:
130+
131+
```bash
110132
simbroker lease acquire --purpose agent-ui-session --lease-file /tmp/simbroker-hello-lease.json
111133
simbroker host status
112134
simbroker lease release --lease-file /tmp/simbroker-hello-lease.json
113135
```
114136

115-
`host init --bootstrap-config` provisions real Simulator devices. Do not run it
116-
casually on a machine whose simulator inventory you cannot afford to change.
117-
118137
## Next reading
119138

120139
- [Getting started](docs/getting-started.md) — install, first-run, reinstall, and uninstall

app/Sources/BrokerOnboardingCommands.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ struct BrokerCLIInvocationFormatter {
3131
}
3232
}
3333

34+
enum BrokerMissingCLISetupCopy {
35+
static let brewInstallCommand = "brew install fiveonecode/simulator-broker/simbroker"
36+
static let refreshActionTitle = "Refresh"
37+
static let heroMessage =
38+
"Install the Homebrew CLI with `brew install fiveonecode/simulator-broker/simbroker`, then click Refresh. The app cannot finish first-run setup until that CLI is on this Mac."
39+
static let manualFallbackText =
40+
"The public path is the Homebrew formula, then Refresh. Development builds may still set SIMBROKER_CLI_PATH."
41+
static let manualFallbackCommands = [brewInstallCommand]
42+
}
43+
3444
struct RepoOnboardingCommandGuide {
3545
let commands: [String]
3646
let shellHelpText: String

app/Sources/SharedViews.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ struct BrokerSetupView: View {
346346
.disabled(store.isApplyingAction || primaryActionEnabled == false)
347347
}
348348

349-
Button("Refresh") {
349+
Button(BrokerMissingCLISetupCopy.refreshActionTitle) {
350350
store.refreshNow()
351351
}
352352
.buttonStyle(.bordered)
@@ -422,7 +422,7 @@ struct BrokerSetupView: View {
422422
private var heroMessage: String {
423423
switch store.startupState {
424424
case .missingCLI:
425-
return "The app cannot find a usable `simbroker` CLI yet, so it cannot self-serve machine setup. Once the CLI is available, this screen can initialize the Mac and start brokerd for you."
425+
return BrokerMissingCLISetupCopy.heroMessage
426426
case .needsHostBootstrap:
427427
return "This Mac has not been initialized for Simulator Broker yet. First-run setup will create the starter simulator pool, write host config, and start brokerd."
428428
case .needsServiceStart:
@@ -475,9 +475,7 @@ struct BrokerSetupView: View {
475475

476476
switch store.startupState {
477477
case .missingCLI:
478-
return [
479-
"export SIMBROKER_CLI_PATH=/absolute/path/to/simbroker",
480-
]
478+
return BrokerMissingCLISetupCopy.manualFallbackCommands
481479
case .needsHostBootstrap:
482480
return [
483481
formatter.command("host init --bootstrap-config --host-config \"\(store.hostConfigPath)\" --state-root \"\(store.stateRootPath)\""),
@@ -501,7 +499,7 @@ struct BrokerSetupView: View {
501499
private var manualFallbackText: String {
502500
switch store.startupState {
503501
case .missingCLI:
504-
return "If you are running a development build, point the app at a repo-local CLI with `SIMBROKER_CLI_PATH` or reinstall the packaged broker so \(store.cliHintPath) exists."
502+
return BrokerMissingCLISetupCopy.manualFallbackText
505503
case .needsHostBootstrap:
506504
return "CLI fallback for the same first-run setup flow."
507505
case .needsServiceStart, .readOnlySnapshot:

app/Tests/BrokerOnboardingCommandsTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,38 @@ import XCTest
22
@testable import SimulatorBrokerApp
33

44
final class BrokerOnboardingCommandsTests: XCTestCase {
5+
func testMissingCLICopyLeadsWithHomebrewFormulaAndRefresh() {
6+
XCTAssertEqual(
7+
BrokerMissingCLISetupCopy.brewInstallCommand,
8+
"brew install fiveonecode/simulator-broker/simbroker"
9+
)
10+
XCTAssertEqual(BrokerMissingCLISetupCopy.refreshActionTitle, "Refresh")
11+
XCTAssertTrue(
12+
BrokerMissingCLISetupCopy.heroMessage.contains(BrokerMissingCLISetupCopy.brewInstallCommand)
13+
)
14+
XCTAssertTrue(
15+
BrokerMissingCLISetupCopy.heroMessage.contains(BrokerMissingCLISetupCopy.refreshActionTitle)
16+
)
17+
XCTAssertEqual(
18+
BrokerMissingCLISetupCopy.manualFallbackCommands,
19+
[BrokerMissingCLISetupCopy.brewInstallCommand]
20+
)
21+
XCTAssertTrue(
22+
BrokerMissingCLISetupCopy.manualFallbackText.hasPrefix("The public path is the Homebrew formula, then Refresh.")
23+
)
24+
XCTAssertFalse(
25+
BrokerMissingCLISetupCopy.heroMessage.contains("SIMBROKER_CLI_PATH"),
26+
"missing-CLI hero must not lead with SIMBROKER_CLI_PATH"
27+
)
28+
let fallback = BrokerMissingCLISetupCopy.manualFallbackText
29+
let brewIndex = fallback.range(of: "Homebrew formula")?.lowerBound
30+
let envIndex = fallback.range(of: "SIMBROKER_CLI_PATH")?.lowerBound
31+
XCTAssertNotNil(brewIndex)
32+
if let brewIndex, let envIndex {
33+
XCTAssertLessThan(brewIndex, envIndex)
34+
}
35+
}
36+
537
func testRepoGuideUsesQuotedAbsoluteCLIPathWhenAvailable() {
638
let guide = RepoOnboardingCommandGuide(
739
cliPath: "/tmp/custom broker/simbroker",

broker-core/index.mjs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,10 +1064,13 @@ function selectRuntimeForAlias(simctl, iosVersion) {
10641064
.sort((left, right) => compareVersions(right.version, left.version));
10651065

10661066
if (runtimes.length === 0) {
1067-
throw new BrokerError(`No available iOS runtime matched ${iosVersion}.`, {
1068-
iosVersion,
1069-
reasonCode: "runtime-not-found",
1070-
});
1067+
throw new BrokerError(
1068+
`No available iOS runtime matched ${iosVersion}. Pass --ios-version to match an installed runtime from \`xcrun simctl list runtimes\`.`,
1069+
{
1070+
iosVersion,
1071+
reasonCode: "runtime-not-found",
1072+
},
1073+
);
10711074
}
10721075

10731076
return runtimes[0];

broker-core/test/broker-core.test.mjs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,51 @@ test("host init can bootstrap a starter host config when none exists yet", () =>
814814
assert.ok(simctlState.devices.some((device) => device.name === "Simulator Broker bootstrap-host manual-1"));
815815
});
816816

817+
test("host init --bootstrap-config names --ios-version when no runtime matches the starter iOS version", () => {
818+
const root = makeTempDir();
819+
const simctl = createSimctlFixture(root, {
820+
runtimes: [
821+
{
822+
identifier: "com.apple.CoreSimulator.SimRuntime.iOS-26-0",
823+
isAvailable: true,
824+
supportedDeviceTypes: [
825+
{
826+
identifier: "com.apple.CoreSimulator.SimDeviceType.iPhone-16",
827+
name: "iPhone 16",
828+
productFamily: "iPhone",
829+
},
830+
{
831+
identifier: "com.apple.CoreSimulator.SimDeviceType.iPad-A16",
832+
name: "iPad (A16)",
833+
productFamily: "iPad",
834+
},
835+
],
836+
version: "26.0",
837+
},
838+
],
839+
});
840+
const resolvedPaths = resolveBrokerPaths({
841+
hostConfigPath: path.join(root, "host-config.json"),
842+
stateRoot: path.join(root, "state"),
843+
});
844+
845+
try {
846+
initBroker(resolvedPaths, {
847+
bootstrapConfig: true,
848+
hostId: "missing-runtime-host",
849+
processExists: () => true,
850+
simctlAdapter: simctl.adapter,
851+
});
852+
assert.fail("expected runtime-not-found");
853+
} catch (error) {
854+
assert.equal(error instanceof BrokerError, true);
855+
assert.equal(error.payload.reasonCode, "runtime-not-found");
856+
assert.match(error.message, /--ios-version/);
857+
assert.match(error.message, /xcrun simctl list runtimes/);
858+
assert.match(error.message, /\b18\b/);
859+
}
860+
});
861+
817862
test("host init warns before creating bootstrap simulators", () => {
818863
const paths = makePaths();
819864
const resolvedPaths = brokerPaths(paths);

broker-core/test/support/simctl-fixture.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ export function createDeviceRecord({
6565
};
6666
}
6767

68-
export function createSimctlFixture(root, { devices = [] } = {}) {
68+
export function createSimctlFixture(root, { devices = [], runtimes } = {}) {
6969
const statePath = path.join(root, "simctl-state.json");
70-
writeJson(statePath, defaultSimctlFixtureState({ devices }));
70+
const state = defaultSimctlFixtureState({ devices });
71+
if (runtimes !== undefined) {
72+
state.runtimes = runtimes;
73+
}
74+
writeJson(statePath, state);
7175
return {
7276
adapter: createFixtureSimctlAdapter({ statePath }),
7377
env: {

client/command-dispatch.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,7 @@ function helpPayload(group) {
617617
],
618618
notes: [
619619
"host init --bootstrap-config creates real iOS Simulator devices on this Mac.",
620+
"If no iOS runtime matches the requested version, pass --ios-version from `xcrun simctl list runtimes`. Default starter iOS is 18.",
620621
],
621622
group: "host",
622623
usage: "simbroker host <command>",

client/test/simbroker.test.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,42 @@ test("host init can bootstrap a starter host config on a fresh machine path", ()
324324
assert.equal(result.json.bootstrapWarning, HOST_BOOTSTRAP_DEVICE_WARNING);
325325
});
326326

327+
test("host init --bootstrap-config reports runtime-not-found with --ios-version guidance", () => {
328+
const root = makeTempDir();
329+
const fixture = {
330+
hostConfigPath: path.join(root, "host-config.json"),
331+
simctl: createSimctlFixture(root, {
332+
runtimes: [
333+
{
334+
identifier: "com.apple.CoreSimulator.SimRuntime.iOS-26-0",
335+
isAvailable: true,
336+
supportedDeviceTypes: [
337+
{
338+
identifier: "com.apple.CoreSimulator.SimDeviceType.iPhone-16",
339+
name: "iPhone 16",
340+
productFamily: "iPhone",
341+
},
342+
{
343+
identifier: "com.apple.CoreSimulator.SimDeviceType.iPad-A16",
344+
name: "iPad (A16)",
345+
productFamily: "iPad",
346+
},
347+
],
348+
version: "26.0",
349+
},
350+
],
351+
}),
352+
stateRoot: path.join(root, "state"),
353+
};
354+
355+
const result = runCli(fixture, "host", "init", "--bootstrap-config", "--host-id", "cli-missing-runtime");
356+
assert.notEqual(result.status, 0);
357+
assert.equal(result.json.ok, false);
358+
assert.equal(result.json.reasonCode, "runtime-not-found");
359+
assert.match(result.json.error, /--ios-version/);
360+
assert.match(result.json.error, /xcrun simctl list runtimes/);
361+
});
362+
327363
test("host init --bootstrap-config warns on stdout-adjacent stderr before devices exist", () => {
328364
const root = makeTempDir();
329365
const fixture = {

0 commit comments

Comments
 (0)