11import Foundation
22import AppKit
3+ import os
4+
5+ private let logger = Logger ( subsystem: " dev.epithet.agent " , category: " BrokerManager " )
6+
7+ /// Creates a Logger for a specific broker's output.
8+ private func brokerLogger( name: String ) -> Logger {
9+ Logger ( subsystem: " dev.epithet.agent " , category: " Broker: \( name) " )
10+ }
11+
12+ /// Parses a log line from the epithet binary and extracts the level and message.
13+ /// Format: "[ANSI]HH:MM:SS[ANSI] [ANSI]LVL[ANSI] message" where LVL is DBG/INF/WRN/ERR
14+ private func parseLogLine( _ line: String ) -> ( level: OSLogType , message: String ) ? {
15+ // Strip ANSI escape codes: \x1B[...m
16+ let stripped = line. replacingOccurrences (
17+ of: " \\ x1B \\ [[0-9;]*m " ,
18+ with: " " ,
19+ options: . regularExpression
20+ )
21+
22+ // Format after stripping: "HH:MM:SS LVL message..."
23+ // Find the level token after the timestamp.
24+ let parts = stripped. split ( separator: " " , maxSplits: 2 , omittingEmptySubsequences: true )
25+ guard parts. count >= 2 else { return nil }
26+
27+ let levelStr = String ( parts [ 1 ] )
28+ let message = parts. count > 2 ? String ( parts [ 2 ] ) : " "
29+
30+ let level : OSLogType
31+ switch levelStr {
32+ case " DBG " :
33+ level = . debug
34+ case " INF " :
35+ level = . info
36+ case " WRN " :
37+ level = . default // .warning doesn't exist, use .default
38+ case " ERR " :
39+ level = . error
40+ default :
41+ // Not a standard log line, treat as info.
42+ return ( . info, stripped)
43+ }
44+
45+ return ( level, message)
46+ }
347
448class BrokerManager {
549 static let shared = BrokerManager ( )
@@ -24,21 +68,34 @@ class BrokerManager {
2468 private init ( ) { }
2569
2670 var epithetBinaryPath : String {
27- // When running from bundle, use bundled binary
71+ // When running from bundle, use bundled binary.
2872 if let resourcePath = Bundle . main. resourcePath {
2973 let bundledPath = ( resourcePath as NSString ) . appendingPathComponent ( " epithet " )
3074 if FileManager . default. fileExists ( atPath: bundledPath) {
3175 return bundledPath
3276 }
3377 }
34- // Fallback for development: use Resources/epithet relative to executable
78+ // Fallback for development: use Resources/epithet relative to executable.
3579 let executablePath = Bundle . main. executablePath ?? " "
3680 let executableDir = ( executablePath as NSString ) . deletingLastPathComponent
3781 let devPath = ( executableDir as NSString ) . appendingPathComponent ( " ../../Resources/epithet " )
3882 if FileManager . default. fileExists ( atPath: devPath) {
3983 return ( devPath as NSString ) . standardizingPath
4084 }
41- // Last resort: check current directory
85+ // When running from Xcode, try the source directory.
86+ // The executable is in DerivedData, but we can find the source via the project structure.
87+ #if DEBUG
88+ let sourceResourcesPath = URL ( fileURLWithPath: #file)
89+ . deletingLastPathComponent ( ) // Sources/EpithetAgent
90+ . deletingLastPathComponent ( ) // Sources
91+ . deletingLastPathComponent ( ) // epithet-macos
92+ . appendingPathComponent ( " Resources/epithet " )
93+ . path
94+ if FileManager . default. fileExists ( atPath: sourceResourcesPath) {
95+ return sourceResourcesPath
96+ }
97+ #endif
98+ // Last resort: check current directory.
4299 return " ./Resources/epithet "
43100 }
44101
@@ -56,7 +113,7 @@ class BrokerManager {
56113
57114 func start( broker: BrokerConfig ) {
58115 guard states [ broker. name] ? . isRunning != true else {
59- print ( " Broker \( broker. name) is already running " )
116+ logger . debug ( " Broker \( broker. name) is already running " )
60117 return
61118 }
62119
@@ -127,6 +184,17 @@ class BrokerManager {
127184 logs [ brokerName] = String ( log. suffix ( Self . trimmedLogSize) )
128185 }
129186
187+ // Forward each line to the unified logger.
188+ let brokerLog = brokerLogger ( name: brokerName)
189+ for line in text. split ( separator: " \n " , omittingEmptySubsequences: true ) {
190+ let lineStr = String ( line)
191+ if let ( level, message) = parseLogLine ( lineStr) {
192+ brokerLog. log ( level: level, " \( message) " )
193+ } else {
194+ brokerLog. info ( " \( lineStr) " )
195+ }
196+ }
197+
130198 onLogUpdate ? ( brokerName)
131199 }
132200
0 commit comments