feat(macos): prompt for CLI install

This commit is contained in:
Peter Steinberger
2026-01-11 10:15:37 +00:00
parent 7551415db9
commit 6d2928888c
25 changed files with 204 additions and 602 deletions

View File

@@ -64,19 +64,6 @@ struct GatewayCommandResolution {
enum GatewayEnvironment {
private static let logger = Logger(subsystem: "com.clawdbot", category: "gateway.env")
private static let supportedBindModes: Set<String> = ["loopback", "tailnet", "lan", "auto"]
private static let bundledGatewayLabel = "Bundled gateway"
static func bundledGatewayExecutable() -> String? {
guard let res = Bundle.main.resourceURL else { return nil }
let path = res.appendingPathComponent("Relay/clawdbot").path
return FileManager.default.isExecutableFile(atPath: path) ? path : nil
}
static func bundledNodeExecutable() -> String? {
guard let res = Bundle.main.resourceURL else { return nil }
let path = res.appendingPathComponent("Relay/node").path
return FileManager.default.isExecutableFile(atPath: path) ? path : nil
}
static func gatewayPort() -> Int {
if let raw = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PORT"] {
@@ -112,32 +99,6 @@ enum GatewayEnvironment {
}
let expected = self.expectedGatewayVersion()
if let bundled = self.bundledGatewayExecutable() {
let installed = self.readGatewayVersion(binary: bundled)
let bundledNode = self.bundledNodeExecutable()
let bundledNodeVersion = bundledNode.flatMap { self.readNodeVersion(binary: $0) }
if let expected, let installed, !installed.compatible(with: expected) {
let message = self.bundledGatewayIncompatibleMessage(
installed: installed,
expected: expected)
return GatewayEnvironmentStatus(
kind: .incompatible(found: installed.description, required: expected.description),
nodeVersion: bundledNodeVersion,
gatewayVersion: installed.description,
requiredGateway: expected.description,
message: message)
}
let gatewayVersionText = installed?.description ?? "unknown"
return GatewayEnvironmentStatus(
kind: .ok,
nodeVersion: bundledNodeVersion,
gatewayVersion: gatewayVersionText,
requiredGateway: expected?.description,
message: self.bundledGatewayStatusMessage(
gatewayVersion: gatewayVersionText,
nodeVersion: bundledNodeVersion))
}
let projectRoot = CommandResolver.projectRoot()
let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot)
@@ -208,7 +169,6 @@ enum GatewayEnvironment {
let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot)
let status = self.check()
let gatewayBin = CommandResolver.clawdbotExecutable()
let bundled = self.bundledGatewayExecutable()
let runtime = RuntimeLocator.resolve(searchPaths: CommandResolver.preferredPaths())
guard case .ok = status.kind else {
@@ -216,20 +176,17 @@ enum GatewayEnvironment {
}
let port = self.gatewayPort()
if let bundled {
let bind = self.preferredGatewayBind() ?? "loopback"
let cmd = [bundled, "gateway-daemon", "--port", "\(port)", "--bind", bind]
return GatewayCommandResolution(status: status, command: cmd)
}
if let gatewayBin {
let cmd = [gatewayBin, "gateway", "--port", "\(port)"]
let bind = self.preferredGatewayBind() ?? "loopback"
let cmd = [gatewayBin, "gateway-daemon", "--port", "\(port)", "--bind", bind]
return GatewayCommandResolution(status: status, command: cmd)
}
if let entry = projectEntrypoint,
case let .success(resolvedRuntime) = runtime
{
let cmd = [resolvedRuntime.path, entry, "gateway", "--port", "\(port)"]
let bind = self.preferredGatewayBind() ?? "loopback"
let cmd = [resolvedRuntime.path, entry, "gateway-daemon", "--port", "\(port)", "--bind", bind]
return GatewayCommandResolution(status: status, command: cmd)
}
@@ -363,40 +320,4 @@ enum GatewayEnvironment {
return Semver.parse(version)
}
private static func readNodeVersion(binary: String) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: binary)
process.arguments = ["--version"]
process.environment = ["PATH": CommandResolver.preferredPaths().joined(separator: ":")]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
do {
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readToEndSafely()
let raw = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "^v", with: "", options: .regularExpression)
return raw?.isEmpty == false ? raw : nil
} catch {
return nil
}
}
private static func bundledGatewayStatusMessage(
gatewayVersion: String,
nodeVersion: String?) -> String
{
"\(self.bundledGatewayLabel) \(gatewayVersion) (node \(nodeVersion ?? "unknown"))"
}
private static func bundledGatewayIncompatibleMessage(
installed: Semver,
expected: Semver) -> String
{
"\(self.bundledGatewayLabel) \(installed.description) is incompatible with app " +
"\(expected.description); rebuild the app bundle."
}
}