Skip to content

Commit a765cd2

Browse files
Discover the Homebrew simbroker binary from the macOS app.
Why: Finish Local Broker Installation told strangers to brew install then Refresh, but the app only looked at SIMBROKER_CLI_PATH, install.json, and the clone-install default bin. Homebrew writes prefix bin/simbroker and does not write install.json, so Refresh stayed on the missing-CLI screen. Changed: CLI search now checks Homebrew prefix bin after explicit overrides and install.json, then falls back to the clone-install default bin. Unit tests pin that order. Verification: npm run agent:verify -- --profile spec-only --paths app/Sources/BrokerSnapshotLoader.swift,app/Tests/BrokerRuntimePathsTests.swift,CHANGELOG.md,spec/build-and-test.md --session-dir task-sessions/homebrew-cli-discovery-20260819 npm run agent:verify -- --profile implementation --paths app/Sources/BrokerSnapshotLoader.swift,app/Tests/BrokerRuntimePathsTests.swift,CHANGELOG.md,spec/build-and-test.md --session-dir task-sessions/homebrew-cli-discovery-20260819 npm run test:app:focus -- SimulatorBrokerAppTests/BrokerRuntimePathsTests Affected: app/Sources/BrokerSnapshotLoader.swift app/Tests/BrokerRuntimePathsTests.swift CHANGELOG.md spec/build-and-test.md Refs: #14 spec/build-and-test.md Session: task-sessions/homebrew-cli-discovery-20260819
1 parent 5df75b7 commit a765cd2

4 files changed

Lines changed: 116 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
create simulators.
2020
- The app **Finish Local Broker Installation** copy leads with
2121
`brew install fiveonecode/simulator-broker/simbroker` and **Refresh**.
22+
Refresh now discovers `simbroker` in Homebrew prefix `bin` (`/opt/homebrew`
23+
and `/usr/local`) as well as `install.json` and `~/.local/bin`.
2224
- `host init --bootstrap-config` `runtime-not-found` errors name
2325
`--ios-version` and `xcrun simctl list runtimes`. Default starter iOS
2426
stays `18`.

app/Sources/BrokerSnapshotLoader.swift

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,47 @@ struct BrokerRuntimePaths: Sendable {
5050
.appending(path: "simbroker")
5151
}
5252

53+
static func defaultHomebrewPrefixRoots(
54+
environment: [String: String] = ProcessInfo.processInfo.environment
55+
) -> [URL] {
56+
var roots: [URL] = []
57+
if let prefix = environment["HOMEBREW_PREFIX"], prefix.isEmpty == false {
58+
roots.append(URL(fileURLWithPath: (prefix as NSString).expandingTildeInPath))
59+
}
60+
roots.append(URL(fileURLWithPath: "/opt/homebrew"))
61+
roots.append(URL(fileURLWithPath: "/usr/local"))
62+
var seen = Set<String>()
63+
return roots.filter { seen.insert($0.standardizedFileURL.path).inserted }
64+
}
65+
66+
static func cliCandidateURLs(
67+
configuredCLIURL: URL?,
68+
installMetadataCLIPath: String?,
69+
homebrewPrefixRoots: [URL] = defaultHomebrewPrefixRoots(),
70+
defaultCLIURL: URL = defaultCLIURL()
71+
) -> [URL] {
72+
var candidates: [URL] = []
73+
if let configuredCLIURL {
74+
candidates.append(configuredCLIURL)
75+
}
76+
if let installMetadataCLIPath, installMetadataCLIPath.isEmpty == false {
77+
candidates.append(URL(fileURLWithPath: (installMetadataCLIPath as NSString).expandingTildeInPath))
78+
}
79+
for root in homebrewPrefixRoots {
80+
candidates.append(root.appending(path: "bin").appending(path: "simbroker"))
81+
}
82+
candidates.append(defaultCLIURL)
83+
var seen = Set<String>()
84+
return candidates.filter { seen.insert($0.standardizedFileURL.path).inserted }
85+
}
86+
87+
static func firstExecutableCLIURL(
88+
among candidates: [URL],
89+
isExecutable: (String) -> Bool = { FileManager.default.isExecutableFile(atPath: $0) }
90+
) -> URL? {
91+
candidates.first { isExecutable($0.path) }
92+
}
93+
5394
static func defaultInstallRoot() -> URL {
5495
FileManager.default.homeDirectoryForCurrentUser
5596
.appending(path: "Library")
@@ -285,25 +326,12 @@ actor FileBrokerSnapshotLoader: BrokerSnapshotLoading {
285326
}
286327

287328
private func resolveCLIPath(installMetadata: BrokerInstallMetadata?) -> URL? {
288-
let fileManager = FileManager.default
289-
let candidates = [
290-
paths.configuredCLIURL,
291-
installMetadata?.cliPath.flatMap { cliPath in
292-
cliPath.isEmpty ? nil : URL(fileURLWithPath: (cliPath as NSString).expandingTildeInPath)
293-
},
294-
BrokerRuntimePaths.defaultCLIURL(),
295-
]
296-
297-
for candidate in candidates {
298-
guard let candidate else {
299-
continue
300-
}
301-
if fileManager.isExecutableFile(atPath: candidate.path) {
302-
return candidate
303-
}
304-
}
305-
306-
return nil
329+
BrokerRuntimePaths.firstExecutableCLIURL(
330+
among: BrokerRuntimePaths.cliCandidateURLs(
331+
configuredCLIURL: paths.configuredCLIURL,
332+
installMetadataCLIPath: installMetadata?.cliPath
333+
)
334+
)
307335
}
308336

309337
private func normalizedPath(_ rawPath: String) -> String {

app/Tests/BrokerRuntimePathsTests.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,70 @@ final class BrokerRuntimePathsTests: XCTestCase {
7474
XCTAssertEqual(launchContext.initialSelection.projectId, "sample-project")
7575
XCTAssertNil(launchContext.initialSelection.eventId)
7676
}
77+
78+
func testCLICandidatesPreferConfiguredThenInstallThenHomebrewThenLocalDefault() {
79+
let configured = URL(fileURLWithPath: "/tmp/configured-simbroker")
80+
let installPath = "/tmp/installed-simbroker"
81+
let candidates = BrokerRuntimePaths.cliCandidateURLs(
82+
configuredCLIURL: configured,
83+
installMetadataCLIPath: installPath,
84+
homebrewPrefixRoots: [
85+
URL(fileURLWithPath: "/opt/homebrew"),
86+
URL(fileURLWithPath: "/usr/local"),
87+
],
88+
defaultCLIURL: URL(fileURLWithPath: "/tmp/home/.local/bin/simbroker")
89+
)
90+
91+
XCTAssertEqual(
92+
candidates.map(\.path),
93+
[
94+
"/tmp/configured-simbroker",
95+
"/tmp/installed-simbroker",
96+
"/opt/homebrew/bin/simbroker",
97+
"/usr/local/bin/simbroker",
98+
"/tmp/home/.local/bin/simbroker",
99+
]
100+
)
101+
}
102+
103+
func testFirstExecutableCLIPrefersHomebrewOverLocalDefault() {
104+
let homebrew = URL(fileURLWithPath: "/opt/homebrew/bin/simbroker")
105+
let localDefault = URL(fileURLWithPath: "/tmp/home/.local/bin/simbroker")
106+
let candidates = BrokerRuntimePaths.cliCandidateURLs(
107+
configuredCLIURL: nil,
108+
installMetadataCLIPath: nil,
109+
homebrewPrefixRoots: [URL(fileURLWithPath: "/opt/homebrew")],
110+
defaultCLIURL: localDefault
111+
)
112+
let resolved = BrokerRuntimePaths.firstExecutableCLIURL(among: candidates) { path in
113+
path == homebrew.path || path == localDefault.path
114+
}
115+
116+
XCTAssertEqual(resolved, homebrew)
117+
}
118+
119+
func testFirstExecutableCLIFallsBackToLocalDefaultWhenHomebrewIsMissing() {
120+
let localDefault = URL(fileURLWithPath: "/tmp/home/.local/bin/simbroker")
121+
let candidates = BrokerRuntimePaths.cliCandidateURLs(
122+
configuredCLIURL: nil,
123+
installMetadataCLIPath: nil,
124+
homebrewPrefixRoots: [URL(fileURLWithPath: "/opt/homebrew")],
125+
defaultCLIURL: localDefault
126+
)
127+
let resolved = BrokerRuntimePaths.firstExecutableCLIURL(among: candidates) { path in
128+
path == localDefault.path
129+
}
130+
131+
XCTAssertEqual(resolved, localDefault)
132+
}
133+
134+
func testDefaultHomebrewPrefixesIncludeStandardRootsAndOptionalHOMEBREW_PREFIX() {
135+
let prefixes = BrokerRuntimePaths.defaultHomebrewPrefixRoots(
136+
environment: ["HOMEBREW_PREFIX": "/opt/homebrew"]
137+
)
138+
XCTAssertEqual(
139+
prefixes.map(\.path),
140+
["/opt/homebrew", "/usr/local"]
141+
)
142+
}
77143
}

spec/build-and-test.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A first extracted implementation slice now exists:
2424
stale-recovery, lock-race, scheduler, confirmed-cleanup, and failure coverage
2525
- tracked-text public-surface scanning with an ignored local denylist extension
2626
- app-side operator controls for pin create and clear, lease release, and lifecycle actions over the shared broker authority
27-
- app launch-time fixture overrides through `--state-root`, `--host-config`, optional `--cli-path`, plus direct pane/detail targeting for deterministic screenshot and smoke scenarios
27+
- app launch-time fixture overrides through `--state-root`, `--host-config`, optional `--cli-path`, plus direct pane/detail targeting for deterministic screenshot and smoke scenarios. When `--cli-path` / `SIMBROKER_CLI_PATH` and `install.json` are unset, the app also looks for an executable `simbroker` in Homebrew prefix `bin` (`/opt/homebrew`, `/usr/local`, and `HOMEBREW_PREFIX`) before `~/.local/bin`
2828
- broker-owned state artifacts are restricted to the current user, and lease, containment, pin, and lifecycle mutations share the broker mutation authority whether invoked directly, through the service, or from the app
2929
- inactive local project registrations can be removed only through the explicit, locked `project forget --project-id <id>` command; it is idempotent, refreshes the shared app snapshot, preserves the repository and audit history, and rejects projects with active leases or pins
3030
- the macOS app test wrapper now supports build-only reruns, focused `-only-testing` filters, and stable `xcresult` output for runtime triage

0 commit comments

Comments
 (0)