File size: 4,568 Bytes
4fc4790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import OpenClawDiscovery
import Foundation

struct DiscoveryOptions {
    var timeoutMs: Int = 2000
    var json: Bool = false
    var includeLocal: Bool = false
    var help: Bool = false

    static func parse(_ args: [String]) -> DiscoveryOptions {
        var opts = DiscoveryOptions()
        var i = 0
        while i < args.count {
            let arg = args[i]
            switch arg {
            case "-h", "--help":
                opts.help = true
            case "--json":
                opts.json = true
            case "--include-local":
                opts.includeLocal = true
            case "--timeout":
                let next = (i + 1 < args.count) ? args[i + 1] : nil
                if let next, let parsed = Int(next.trimmingCharacters(in: .whitespacesAndNewlines)) {
                    opts.timeoutMs = max(100, parsed)
                    i += 1
                }
            default:
                break
            }
            i += 1
        }
        return opts
    }
}

struct DiscoveryOutput: Encodable {
    struct Gateway: Encodable {
        var displayName: String
        var lanHost: String?
        var tailnetDns: String?
        var sshPort: Int
        var gatewayPort: Int?
        var cliPath: String?
        var stableID: String
        var debugID: String
        var isLocal: Bool
    }

    var status: String
    var timeoutMs: Int
    var includeLocal: Bool
    var count: Int
    var gateways: [Gateway]
}

func runDiscover(_ args: [String]) async {
    let opts = DiscoveryOptions.parse(args)
    if opts.help {
        print("""
        openclaw-mac discover

        Usage:
          openclaw-mac discover [--timeout <ms>] [--json] [--include-local]

        Options:
          --timeout <ms>     Discovery window in milliseconds (default: 2000)
          --json             Emit JSON
          --include-local    Include gateways considered local
          -h, --help         Show help
        """)
        return
    }

    let displayName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
    let model = await MainActor.run {
        GatewayDiscoveryModel(
            localDisplayName: displayName,
            filterLocalGateways: !opts.includeLocal)
    }

    await MainActor.run {
        model.start()
    }

    let nanos = UInt64(max(100, opts.timeoutMs)) * 1_000_000
    try? await Task.sleep(nanoseconds: nanos)

    let gateways = await MainActor.run { model.gateways }
    let status = await MainActor.run { model.statusText }

    await MainActor.run {
        model.stop()
    }

    if opts.json {
        let payload = DiscoveryOutput(
            status: status,
            timeoutMs: opts.timeoutMs,
            includeLocal: opts.includeLocal,
            count: gateways.count,
            gateways: gateways.map {
                DiscoveryOutput.Gateway(
                    displayName: $0.displayName,
                    lanHost: $0.lanHost,
                    tailnetDns: $0.tailnetDns,
                    sshPort: $0.sshPort,
                    gatewayPort: $0.gatewayPort,
                    cliPath: $0.cliPath,
                    stableID: $0.stableID,
                    debugID: $0.debugID,
                    isLocal: $0.isLocal)
            })
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        if let data = try? encoder.encode(payload),
           let json = String(data: data, encoding: .utf8)
        {
            print(json)
        } else {
            print("{\"error\":\"failed to encode JSON\"}")
        }
        return
    }

    print("Gateway Discovery (macOS NWBrowser)")
    print("Status: \(status)")
    print("Found \(gateways.count) gateway(s)\(opts.includeLocal ? "" : " (local filtered)")")
    if gateways.isEmpty { return }

    for gateway in gateways {
        let hosts = [gateway.tailnetDns, gateway.lanHost]
            .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
            .joined(separator: ", ")
        print("- \(gateway.displayName)")
        print("  hosts: \(hosts.isEmpty ? "(none)" : hosts)")
        print("  ssh: \(gateway.sshPort)")
        if let port = gateway.gatewayPort {
            print("  gatewayPort: \(port)")
        }
        if let cliPath = gateway.cliPath {
            print("  cliPath: \(cliPath)")
        }
        print("  isLocal: \(gateway.isLocal)")
        print("  stableID: \(gateway.stableID)")
        print("  debugID: \(gateway.debugID)")
    }
}