From 4645f512d174186e39a5096e7436b6e76cfe000e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Dec 2025 05:34:25 +0100 Subject: [PATCH] fix: reuse resolver for agent rpc launch --- apps/macos/Sources/Clawdis/AgentRPC.swift | 11 ++- .../Sources/Clawdis/RelayProcessManager.swift | 75 ++----------------- apps/macos/Sources/Clawdis/Utilities.swift | 75 +++++++++++++++++++ apps/macos/Sources/Clawdis/XPCService.swift | 18 +++-- 4 files changed, 100 insertions(+), 79 deletions(-) diff --git a/apps/macos/Sources/Clawdis/AgentRPC.swift b/apps/macos/Sources/Clawdis/AgentRPC.swift index f187f30b9..ef81df76e 100644 --- a/apps/macos/Sources/Clawdis/AgentRPC.swift +++ b/apps/macos/Sources/Clawdis/AgentRPC.swift @@ -81,10 +81,13 @@ actor AgentRPC { func start() async throws { let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") - process.arguments = ["pnpm", "clawdis", "rpc"] - let projectRoot = await RelayProcessManager.shared.projectRootPath() - process.currentDirectoryURL = URL(fileURLWithPath: projectRoot) + let command = CommandResolver.clawdisCommand(subcommand: "rpc") + process.executableURL = URL(fileURLWithPath: command.first ?? "/usr/bin/env") + process.arguments = Array(command.dropFirst()) + process.currentDirectoryURL = URL(fileURLWithPath: CommandResolver.projectRootPath()) + var env = ProcessInfo.processInfo.environment + env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":") + process.environment = env let stdinPipe = Pipe() let stdoutPipe = Pipe() diff --git a/apps/macos/Sources/Clawdis/RelayProcessManager.swift b/apps/macos/Sources/Clawdis/RelayProcessManager.swift index b8c373732..79d300c3c 100644 --- a/apps/macos/Sources/Clawdis/RelayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/RelayProcessManager.swift @@ -14,10 +14,6 @@ import SystemPackage final class RelayProcessManager: ObservableObject { static let shared = RelayProcessManager() - private enum Defaults { - static let projectRootPath = "clawdis.relayProjectRootPath" - } - enum Status: Equatable { case stopped case starting @@ -203,87 +199,28 @@ final class RelayProcessManager: ObservableObject { return override.split(separator: " ").map(String.init) } - if let clawdisPath = self.findExecutable(named: "clawdis") { - return [clawdisPath, "relay"] - } - if let pnpm = self.findExecutable(named: "pnpm") { - // Run pnpm from the project root (workingDirectory handles cwd). - return [pnpm, "clawdis", "relay"] - } - if let node = self.findExecutable(named: "node") { - let clawdis = self.defaultProjectRoot().appendingPathComponent("bin/clawdis.js").path - if FileManager.default.isReadableFile(atPath: clawdis) { - return [node, clawdis, "relay"] - } - } - return ["clawdis", "relay"] + return CommandResolver.clawdisCommand(subcommand: "relay") } private func makeEnvironment() -> Environment { - let merged = self.preferredPaths().joined(separator: ":") + let merged = CommandResolver.preferredPaths().joined(separator: ":") return .inherit.updating([ "PATH": merged, "PNPM_HOME": FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/pnpm").path, - "CLAWDIS_PROJECT_ROOT": self.defaultProjectRoot().path, + "CLAWDIS_PROJECT_ROOT": CommandResolver.projectRoot().path, ]) } - private func preferredPaths() -> [String] { - let current = ProcessInfo.processInfo.environment["PATH"]? - .split(separator: ":").map(String.init) ?? [] - let extras = [ - self.defaultProjectRoot().appendingPathComponent("node_modules/.bin").path, - FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/pnpm").path, - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - ] - var seen = Set() - return (extras + current).filter { seen.insert($0).inserted } - } - - private func findExecutable(named name: String) -> String? { - for dir in self.preferredPaths() { - let candidate = (dir as NSString).appendingPathComponent(name) - if FileManager.default.isExecutableFile(atPath: candidate) { - return candidate - } - } - return nil - } - private func defaultProjectRoot() -> URL { - if let stored = UserDefaults.standard.string(forKey: Defaults.projectRootPath), - let url = self.expandPath(stored) - { - return url - } - let fallback = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Projects/clawdis") - if FileManager.default.fileExists(atPath: fallback.path) { - return fallback - } - return FileManager.default.homeDirectoryForCurrentUser + CommandResolver.projectRoot() } func setProjectRoot(path: String) { - UserDefaults.standard.set(path, forKey: Defaults.projectRootPath) + CommandResolver.setProjectRoot(path) } func projectRootPath() -> String { - UserDefaults.standard.string(forKey: Defaults.projectRootPath) - ?? FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Projects/clawdis").path - } - - private func expandPath(_ path: String) -> URL? { - var expanded = path - if expanded.hasPrefix("~") { - let home = FileManager.default.homeDirectoryForCurrentUser.path - expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home) - } - return URL(fileURLWithPath: expanded) + CommandResolver.projectRootPath() } } diff --git a/apps/macos/Sources/Clawdis/Utilities.swift b/apps/macos/Sources/Clawdis/Utilities.swift index 6d9cdee22..a775d6d2b 100644 --- a/apps/macos/Sources/Clawdis/Utilities.swift +++ b/apps/macos/Sources/Clawdis/Utilities.swift @@ -166,3 +166,78 @@ enum CLIInstaller { "'" + path.replacingOccurrences(of: "'", with: "'\"'\"'") + "'" } } + +enum CommandResolver { + private static let projectRootDefaultsKey = "clawdis.relayProjectRootPath" + + static func projectRoot() -> URL { + if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey), + let url = self.expandPath(stored) { + return url + } + let fallback = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Projects/clawdis") + if FileManager.default.fileExists(atPath: fallback.path) { + return fallback + } + return FileManager.default.homeDirectoryForCurrentUser + } + + static func setProjectRoot(_ path: String) { + UserDefaults.standard.set(path, forKey: self.projectRootDefaultsKey) + } + + static func projectRootPath() -> String { + self.projectRoot().path + } + + static func preferredPaths() -> [String] { + let current = ProcessInfo.processInfo.environment["PATH"]? + .split(separator: ":").map(String.init) ?? [] + let extras = [ + self.projectRoot().appendingPathComponent("node_modules/.bin").path, + FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/pnpm").path, + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + ] + var seen = Set() + return (extras + current).filter { seen.insert($0).inserted } + } + + static func findExecutable(named name: String) -> String? { + for dir in self.preferredPaths() { + let candidate = (dir as NSString).appendingPathComponent(name) + if FileManager.default.isExecutableFile(atPath: candidate) { + return candidate + } + } + return nil + } + + static func clawdisCommand(subcommand: String, extraArgs: [String] = []) -> [String] { + if let clawdisPath = self.findExecutable(named: "clawdis") { + return [clawdisPath, subcommand] + extraArgs + } + if let node = self.findExecutable(named: "node") { + let cli = self.projectRoot().appendingPathComponent("bin/clawdis.js").path + if FileManager.default.isReadableFile(atPath: cli) { + return [node, cli, subcommand] + extraArgs + } + } + if let pnpm = self.findExecutable(named: "pnpm") { + return [pnpm, "clawdis", subcommand] + extraArgs + } + return ["clawdis", subcommand] + extraArgs + } + + private static func expandPath(_ path: String) -> URL? { + var expanded = path + if expanded.hasPrefix("~") { + let home = FileManager.default.homeDirectoryForCurrentUser.path + expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home) + } + return URL(fileURLWithPath: expanded) + } +} diff --git a/apps/macos/Sources/Clawdis/XPCService.swift b/apps/macos/Sources/Clawdis/XPCService.swift index f1839f5d6..ae2c6b040 100644 --- a/apps/macos/Sources/Clawdis/XPCService.swift +++ b/apps/macos/Sources/Clawdis/XPCService.swift @@ -91,15 +91,21 @@ final class ClawdisXPCService: NSObject, ClawdisXPCProtocol { thinking: String?, session: String) async -> (ok: Bool, text: String?, error: String?) { - let projectRoot = await MainActor.run { RelayProcessManager.shared.projectRootPath() } + let projectRoot = CommandResolver.projectRootPath() + var command = CommandResolver.clawdisCommand(subcommand: "agent") + command += ["--message", message, "--json"] + if !session.isEmpty { command += ["--to", session] } + if let thinking { command += ["--thinking", thinking] } + let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") - var args = ["pnpm", "clawdis", "agent", "--message", message, "--json"] - if !session.isEmpty { args += ["--to", session] } - if let thinking { args += ["--thinking", thinking] } - process.arguments = args + process.executableURL = URL(fileURLWithPath: command.first ?? "/usr/bin/env") + process.arguments = Array(command.dropFirst()) process.currentDirectoryURL = URL(fileURLWithPath: projectRoot) + var env = ProcessInfo.processInfo.environment + env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":") + process.environment = env + let outPipe = Pipe() let errPipe = Pipe() process.standardOutput = outPipe