refactor: consolidate mac debug CLI
This commit is contained in:
306
apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift
Normal file
306
apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift
Normal file
@@ -0,0 +1,306 @@
|
||||
import ClawdbotKit
|
||||
import ClawdbotProtocol
|
||||
import Foundation
|
||||
|
||||
struct ConnectOptions {
|
||||
var url: String?
|
||||
var token: String?
|
||||
var password: String?
|
||||
var mode: String?
|
||||
var timeoutMs: Int = 15_000
|
||||
var json: Bool = false
|
||||
var probe: Bool = false
|
||||
var clientId: String = "clawdbot-macos"
|
||||
var clientMode: String = "ui"
|
||||
var displayName: String?
|
||||
var role: String = "operator"
|
||||
var scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"]
|
||||
var help: Bool = false
|
||||
|
||||
static func parse(_ args: [String]) -> ConnectOptions {
|
||||
var opts = ConnectOptions()
|
||||
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 "--probe":
|
||||
opts.probe = true
|
||||
case "--url":
|
||||
opts.url = self.nextValue(args, index: &i)
|
||||
case "--token":
|
||||
opts.token = self.nextValue(args, index: &i)
|
||||
case "--password":
|
||||
opts.password = self.nextValue(args, index: &i)
|
||||
case "--mode":
|
||||
if let value = self.nextValue(args, index: &i) {
|
||||
opts.mode = value
|
||||
}
|
||||
case "--timeout":
|
||||
if let raw = self.nextValue(args, index: &i),
|
||||
let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
{
|
||||
opts.timeoutMs = max(250, parsed)
|
||||
}
|
||||
case "--client-id":
|
||||
if let value = self.nextValue(args, index: &i) {
|
||||
opts.clientId = value
|
||||
}
|
||||
case "--client-mode":
|
||||
if let value = self.nextValue(args, index: &i) {
|
||||
opts.clientMode = value
|
||||
}
|
||||
case "--display-name":
|
||||
opts.displayName = self.nextValue(args, index: &i)
|
||||
case "--role":
|
||||
if let value = self.nextValue(args, index: &i) {
|
||||
opts.role = value
|
||||
}
|
||||
case "--scopes":
|
||||
if let value = self.nextValue(args, index: &i) {
|
||||
opts.scopes = value.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
private static func nextValue(_ args: [String], index: inout Int) -> String? {
|
||||
guard index + 1 < args.count else { return nil }
|
||||
index += 1
|
||||
return args[index].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectOutput: Encodable {
|
||||
var status: String
|
||||
var url: String
|
||||
var mode: String
|
||||
var role: String
|
||||
var clientId: String
|
||||
var clientMode: String
|
||||
var scopes: [String]
|
||||
var snapshot: HelloOk?
|
||||
var health: ProtoAnyCodable?
|
||||
var error: String?
|
||||
}
|
||||
|
||||
actor SnapshotStore {
|
||||
private var value: HelloOk?
|
||||
|
||||
func set(_ snapshot: HelloOk) {
|
||||
self.value = snapshot
|
||||
}
|
||||
|
||||
func get() -> HelloOk? {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
func runConnect(_ args: [String]) async {
|
||||
let opts = ConnectOptions.parse(args)
|
||||
if opts.help {
|
||||
print("""
|
||||
clawdbot-mac connect
|
||||
|
||||
Usage:
|
||||
clawdbot-mac connect [--url <ws://host:port>] [--token <token>] [--password <password>]
|
||||
[--mode <local|remote>] [--timeout <ms>] [--probe] [--json]
|
||||
[--client-id <id>] [--client-mode <mode>] [--display-name <name>]
|
||||
[--role <role>] [--scopes <a,b,c>]
|
||||
|
||||
Options:
|
||||
--url <url> Gateway WebSocket URL (overrides config)
|
||||
--token <token> Gateway token (if required)
|
||||
--password <pw> Gateway password (if required)
|
||||
--mode <mode> Resolve from config: local|remote (default: config or local)
|
||||
--timeout <ms> Request timeout (default: 15000)
|
||||
--probe Force a fresh health probe
|
||||
--json Emit JSON
|
||||
--client-id <id> Override client id (default: clawdbot-macos)
|
||||
--client-mode <m> Override client mode (default: ui)
|
||||
--display-name <n> Override display name
|
||||
--role <role> Override role (default: operator)
|
||||
--scopes <a,b,c> Override scopes list
|
||||
-h, --help Show help
|
||||
""")
|
||||
return
|
||||
}
|
||||
|
||||
let config = loadGatewayConfig()
|
||||
do {
|
||||
let endpoint = try resolveGatewayEndpoint(opts: opts, config: config)
|
||||
let displayName = opts.displayName ?? Host.current().localizedName ?? "Clawdbot macOS Debug CLI"
|
||||
let connectOptions = GatewayConnectOptions(
|
||||
role: opts.role,
|
||||
scopes: opts.scopes,
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: opts.clientId,
|
||||
clientMode: opts.clientMode,
|
||||
clientDisplayName: displayName)
|
||||
|
||||
let snapshotStore = SnapshotStore()
|
||||
let channel = GatewayChannelActor(
|
||||
url: endpoint.url,
|
||||
token: endpoint.token,
|
||||
password: endpoint.password,
|
||||
pushHandler: { push in
|
||||
if case let .snapshot(ok) = push {
|
||||
await snapshotStore.set(ok)
|
||||
}
|
||||
},
|
||||
connectOptions: connectOptions)
|
||||
|
||||
let params: [String: KitAnyCodable]? = opts.probe ? ["probe": KitAnyCodable(true)] : nil
|
||||
let data = try await channel.request(
|
||||
method: "health",
|
||||
params: params,
|
||||
timeoutMs: Double(opts.timeoutMs))
|
||||
let health = try? JSONDecoder().decode(ProtoAnyCodable.self, from: data)
|
||||
let snapshot = await snapshotStore.get()
|
||||
await channel.shutdown()
|
||||
|
||||
let output = ConnectOutput(
|
||||
status: "ok",
|
||||
url: endpoint.url.absoluteString,
|
||||
mode: endpoint.mode,
|
||||
role: opts.role,
|
||||
clientId: opts.clientId,
|
||||
clientMode: opts.clientMode,
|
||||
scopes: opts.scopes,
|
||||
snapshot: snapshot,
|
||||
health: health,
|
||||
error: nil)
|
||||
printConnectOutput(output, json: opts.json)
|
||||
} catch {
|
||||
let endpoint = bestEffortEndpoint(opts: opts, config: config)
|
||||
let fallbackMode = (opts.mode ?? config.mode ?? "local").lowercased()
|
||||
let output = ConnectOutput(
|
||||
status: "error",
|
||||
url: endpoint?.url.absoluteString ?? "unknown",
|
||||
mode: endpoint?.mode ?? fallbackMode,
|
||||
role: opts.role,
|
||||
clientId: opts.clientId,
|
||||
clientMode: opts.clientMode,
|
||||
scopes: opts.scopes,
|
||||
snapshot: nil,
|
||||
health: nil,
|
||||
error: error.localizedDescription)
|
||||
printConnectOutput(output, json: opts.json)
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private func printConnectOutput(_ output: ConnectOutput, json: Bool) {
|
||||
if json {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
if let data = try? encoder.encode(output),
|
||||
let text = String(data: data, encoding: .utf8)
|
||||
{
|
||||
print(text)
|
||||
} else {
|
||||
print("{\"error\":\"failed to encode JSON\"}")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
print("Clawdbot macOS Gateway Connect")
|
||||
print("Status: \(output.status)")
|
||||
print("URL: \(output.url)")
|
||||
print("Mode: \(output.mode)")
|
||||
print("Client: \(output.clientId) (\(output.clientMode))")
|
||||
print("Role: \(output.role)")
|
||||
print("Scopes: \(output.scopes.joined(separator: ", "))")
|
||||
if let snapshot = output.snapshot {
|
||||
print("Protocol: \(snapshot._protocol)")
|
||||
if let version = snapshot.server["version"]?.value as? String {
|
||||
print("Server: \(version)")
|
||||
}
|
||||
}
|
||||
if let health = output.health,
|
||||
let ok = (health.value as? [String: ProtoAnyCodable])?["ok"]?.value as? Bool
|
||||
{
|
||||
print("Health: \(ok ? "ok" : "error")")
|
||||
} else if output.health != nil {
|
||||
print("Health: received")
|
||||
}
|
||||
if let error = output.error {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveGatewayEndpoint(opts: ConnectOptions, config: GatewayConfig) throws -> GatewayEndpoint {
|
||||
let resolvedMode = (opts.mode ?? config.mode ?? "local").lowercased()
|
||||
if let raw = opts.url, !raw.isEmpty {
|
||||
guard let url = URL(string: raw) else {
|
||||
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: \(raw)"])
|
||||
}
|
||||
return GatewayEndpoint(
|
||||
url: url,
|
||||
token: resolvedToken(opts: opts, mode: resolvedMode, config: config),
|
||||
password: resolvedPassword(opts: opts, mode: resolvedMode, config: config),
|
||||
mode: resolvedMode)
|
||||
}
|
||||
|
||||
if resolvedMode == "remote" {
|
||||
guard let raw = config.remoteUrl?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!raw.isEmpty else {
|
||||
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url is missing"])
|
||||
}
|
||||
guard let url = URL(string: raw) else {
|
||||
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: \(raw)"])
|
||||
}
|
||||
return GatewayEndpoint(
|
||||
url: url,
|
||||
token: resolvedToken(opts: opts, mode: resolvedMode, config: config),
|
||||
password: resolvedPassword(opts: opts, mode: resolvedMode, config: config),
|
||||
mode: resolvedMode)
|
||||
}
|
||||
|
||||
let port = config.port ?? 18789
|
||||
let host = "127.0.0.1"
|
||||
guard let url = URL(string: "ws://\(host):\(port)") else {
|
||||
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: ws://\(host):\(port)"])
|
||||
}
|
||||
return GatewayEndpoint(
|
||||
url: url,
|
||||
token: resolvedToken(opts: opts, mode: resolvedMode, config: config),
|
||||
password: resolvedPassword(opts: opts, mode: resolvedMode, config: config),
|
||||
mode: resolvedMode)
|
||||
}
|
||||
|
||||
private func bestEffortEndpoint(opts: ConnectOptions, config: GatewayConfig) -> GatewayEndpoint? {
|
||||
return try? resolveGatewayEndpoint(opts: opts, config: config)
|
||||
}
|
||||
|
||||
private func resolvedToken(opts: ConnectOptions, mode: String, config: GatewayConfig) -> String? {
|
||||
if let token = opts.token, !token.isEmpty { return token }
|
||||
if let token = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_TOKEN"], !token.isEmpty {
|
||||
return token
|
||||
}
|
||||
if mode == "remote" {
|
||||
return config.remoteToken
|
||||
}
|
||||
return config.token
|
||||
}
|
||||
|
||||
private func resolvedPassword(opts: ConnectOptions, mode: String, config: GatewayConfig) -> String? {
|
||||
if let password = opts.password, !password.isEmpty { return password }
|
||||
if let password = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PASSWORD"], !password.isEmpty {
|
||||
return password
|
||||
}
|
||||
if mode == "remote" {
|
||||
return config.remotePassword
|
||||
}
|
||||
return config.password
|
||||
}
|
||||
Reference in New Issue
Block a user