Server setup in the app
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import ByreAPI
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
typealias Permission = Components.Schemas.Permission
|
||||
|
||||
struct LaunchAgent: Decodable {
|
||||
let programArguments: [String]
|
||||
|
||||
@@ -18,6 +21,143 @@ struct ServerState {
|
||||
var address: String?
|
||||
}
|
||||
|
||||
struct Check: Decodable {
|
||||
let name: String
|
||||
let need: String
|
||||
let `for`: String
|
||||
let ok: Bool
|
||||
let fix: String
|
||||
let blocking: Bool
|
||||
}
|
||||
|
||||
struct Checks: Decodable {
|
||||
let ready: Bool
|
||||
let checks: [Check]
|
||||
}
|
||||
|
||||
struct ChecksView: View {
|
||||
let checks: [Check]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
ForEach(checks, id: \.name) { check in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 8) {
|
||||
Text(check.name).font(Fonts.font(13, weight: 700))
|
||||
status(check)
|
||||
}
|
||||
Text("\(need(check)), for \(check.for)").foregroundStyle(Palette.muted)
|
||||
if !check.ok {
|
||||
Text(check.fix)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func status(_ check: Check) -> some View {
|
||||
if check.ok {
|
||||
return Text("Found").foregroundStyle(Palette.ok)
|
||||
}
|
||||
return Text("Missing").foregroundStyle(check.blocking ? Palette.danger : Palette.muted)
|
||||
}
|
||||
|
||||
private func need(_ check: Check) -> String {
|
||||
switch check.need {
|
||||
case "required":
|
||||
return "Required"
|
||||
case "agent":
|
||||
return "One of claude, codex and agy required"
|
||||
default:
|
||||
return "Optional"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replayd keys ScreenCaptureKit clients by executable path, so the check runs from its own copy beside client_hands' byre-hands.
|
||||
func checkLocalPermissions() async throws -> [Permission] {
|
||||
let dir = FileManager.default.temporaryDirectory.appending(path: "byre-hands-check-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: dir)
|
||||
}
|
||||
let bin = dir.appending(path: "byre-hands")
|
||||
try FileManager.default.copyItem(at: Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre-hands"), to: bin)
|
||||
let r = try await execute(bin.path, ["check"])
|
||||
guard r.status == 0, let perms = try? JSONDecoder().decode([Permission].self, from: Data(r.output.utf8)) else {
|
||||
throw ProcessFailure(errorDescription: "byre-hands check failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))")
|
||||
}
|
||||
return perms
|
||||
}
|
||||
|
||||
struct PermissionsCheck: View {
|
||||
let run: () async throws -> [Permission]
|
||||
@ViewState private var result: Result<[Permission], any Error>?
|
||||
@ViewState private var busy = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Button(busy ? "Checking…" : "Check permissions", action: check)
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.disabled(busy)
|
||||
switch result {
|
||||
case .success(let perms):
|
||||
ForEach(perms, id: \.name) { perm in
|
||||
row(perm)
|
||||
}
|
||||
case .failure(let error):
|
||||
Text(errorText(error))
|
||||
.foregroundStyle(Palette.danger)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
case nil:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
private func row(_ perm: Permission) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 8) {
|
||||
Text(perm.name).font(Fonts.font(13, weight: 700))
|
||||
switch perm.status {
|
||||
case .granted:
|
||||
Text("Granted").foregroundStyle(Palette.ok)
|
||||
case .missing:
|
||||
Text("Missing").foregroundStyle(Palette.danger)
|
||||
case .unchecked:
|
||||
Text("Not checked").foregroundStyle(Palette.muted)
|
||||
}
|
||||
}
|
||||
Text(perm.detail)
|
||||
.foregroundStyle(Palette.muted)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if perm.status == .missing, let url = URL(string: perm.settings) {
|
||||
Button("Open System Settings") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func check() {
|
||||
busy = true
|
||||
Task {
|
||||
do {
|
||||
result = .success(try await run())
|
||||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProcessFailure: LocalizedError {
|
||||
let errorDescription: String?
|
||||
}
|
||||
@@ -65,9 +205,21 @@ enum Server {
|
||||
}
|
||||
|
||||
// byre install copies its PATH into the service, which needs the login shell's to find claude, codex, agy and npx.
|
||||
static func install() async throws -> URL {
|
||||
private static func helper(_ command: String) async throws -> (status: Int32, output: String) {
|
||||
let shell = unsafe String(cString: getpwuid(getuid()).pointee.pw_shell)
|
||||
let r = try await execute(shell, ["-l", "-c", "exec \"$BYRE_HELPER\" install"], env: ["BYRE_HELPER": serverHelper])
|
||||
return try await execute(shell, ["-l", "-c", "exec \"$BYRE_HELPER\" \(command)"], env: ["BYRE_HELPER": serverHelper])
|
||||
}
|
||||
|
||||
static func checks() async throws -> Checks {
|
||||
let r = try await helper("check")
|
||||
guard r.status == 0, let line = r.output.split(separator: "\n").last, let checks = try? JSONDecoder().decode(Checks.self, from: Data(line.utf8)) else {
|
||||
throw ProcessFailure(errorDescription: "byre check failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))")
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
static func install() async throws -> URL {
|
||||
let r = try await helper("install")
|
||||
let prefix = "byre is at "
|
||||
guard r.status == 0, let line = r.output.split(separator: "\n").first(where: { $0.hasPrefix(prefix) }), let url = URL(string: String(line.dropFirst(prefix.count))) else {
|
||||
throw ProcessFailure(errorDescription: "byre install failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))")
|
||||
@@ -92,6 +244,7 @@ enum Server {
|
||||
struct ServerView: View {
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var state: ServerState?
|
||||
@ViewState private var checks: Result<Checks, any Error>?
|
||||
@ViewState private var busy = false
|
||||
|
||||
var body: some View {
|
||||
@@ -103,6 +256,30 @@ struct ServerView: View {
|
||||
if let state {
|
||||
details(state)
|
||||
}
|
||||
Text("Tools on this Mac")
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.padding(.top, 16)
|
||||
switch checks {
|
||||
case .success(let checks):
|
||||
ChecksView(checks: checks.checks)
|
||||
case .failure(let error):
|
||||
Text(errorText(error))
|
||||
.foregroundStyle(Palette.danger)
|
||||
.textSelection(.enabled)
|
||||
case nil:
|
||||
EmptyView()
|
||||
}
|
||||
if let address = state?.address, let origin = URL(string: address) {
|
||||
Text("Agents' permissions")
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.padding(.top, 16)
|
||||
Text("The macOS privacy grants the server's agents use to see and control this Mac. Checking asks macOS for any that are missing.")
|
||||
.foregroundStyle(Palette.muted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
PermissionsCheck {
|
||||
try await API(origin: origin).client.checkPermissions().ok.body.json
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
@@ -156,6 +333,16 @@ struct ServerView: View {
|
||||
}
|
||||
|
||||
private func refresh() async {
|
||||
async let checked = check()
|
||||
state = await Server.state()
|
||||
checks = await checked
|
||||
}
|
||||
|
||||
private func check() async -> Result<Checks, any Error> {
|
||||
do {
|
||||
return .success(try await Server.checks())
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user