From f6956320f937457428192006f4aba97de232f662 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 09:22:37 +0100 Subject: [PATCH] feat: centralize config paths and expose in snapshot --- .../Sources/Clawdis/ClawdisConfigFile.swift | 24 ++---------- apps/macos/Sources/Clawdis/ClawdisPaths.swift | 37 +++++++++++++++++++ .../Sources/Clawdis/DeviceModelCatalog.swift | 16 -------- .../Sources/Clawdis/GatewayConnection.swift | 8 ++++ .../Sources/Clawdis/SettingsRootView.swift | 5 ++- .../ClawdisProtocol/GatewayModels.swift | 10 ++++- src/gateway/protocol/schema.ts | 2 + src/gateway/server.test.ts | 17 +++++++-- src/gateway/server.ts | 3 ++ 9 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 apps/macos/Sources/Clawdis/ClawdisPaths.swift diff --git a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift index 63d2ee302..3a712ebce 100644 --- a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift +++ b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift @@ -2,28 +2,17 @@ import Foundation enum ClawdisConfigFile { private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "config") - private static let configPathEnv = "CLAWDIS_CONFIG_PATH" - private static let stateDirEnv = "CLAWDIS_STATE_DIR" static func url() -> URL { - if let override = self.envPath(self.configPathEnv) { - return URL(fileURLWithPath: override) - } - return self.stateDirURL() - .appendingPathComponent("clawdis.json") + ClawdisPaths.configURL } static func stateDirURL() -> URL { - if let override = self.envPath(self.stateDirEnv) { - return URL(fileURLWithPath: override, isDirectory: true) - } - return FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".clawdis", isDirectory: true) + ClawdisPaths.stateDirURL } static func defaultWorkspaceURL() -> URL { - self.stateDirURL() - .appendingPathComponent("workspace", isDirectory: true) + ClawdisPaths.workspaceURL } static func loadDict() -> [String: Any] { @@ -108,11 +97,4 @@ enum ClawdisConfigFile { self.logger.debug("agent workspace updated set=\(!trimmed.isEmpty)") } - private static func envPath(_ key: String) -> String? { - guard let value = ProcessInfo.processInfo.environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines), - !value.isEmpty else { - return nil - } - return value - } } diff --git a/apps/macos/Sources/Clawdis/ClawdisPaths.swift b/apps/macos/Sources/Clawdis/ClawdisPaths.swift new file mode 100644 index 000000000..0fdbc8d59 --- /dev/null +++ b/apps/macos/Sources/Clawdis/ClawdisPaths.swift @@ -0,0 +1,37 @@ +import Foundation + +enum ClawdisEnv { + static func path(_ key: String) -> String? { + guard let value = ProcessInfo.processInfo.environment[key]? + .trimmingCharacters(in: .whitespacesAndNewlines), + !value.isEmpty + else { + return nil + } + return value + } +} + +enum ClawdisPaths { + private static let configPathEnv = "CLAWDIS_CONFIG_PATH" + private static let stateDirEnv = "CLAWDIS_STATE_DIR" + + static var stateDirURL: URL { + if let override = ClawdisEnv.path(self.stateDirEnv) { + return URL(fileURLWithPath: override, isDirectory: true) + } + return FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".clawdis", isDirectory: true) + } + + static var configURL: URL { + if let override = ClawdisEnv.path(self.configPathEnv) { + return URL(fileURLWithPath: override) + } + return self.stateDirURL.appendingPathComponent("clawdis.json") + } + + static var workspaceURL: URL { + self.stateDirURL.appendingPathComponent("workspace", isDirectory: true) + } +} diff --git a/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift b/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift index 9d9da9987..02a1bd00f 100644 --- a/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift +++ b/apps/macos/Sources/Clawdis/DeviceModelCatalog.swift @@ -129,22 +129,6 @@ enum DeviceModelCatalog { if let bundle = self.bundleIfContainsDeviceModels(Bundle.main) { return bundle } - - if let resourceURL = Bundle.main.resourceURL { - if let enumerator = FileManager.default.enumerator( - at: resourceURL, - includingPropertiesForKeys: [.isDirectoryKey], - options: [.skipsHiddenFiles]) { - for case let url as URL in enumerator { - guard url.pathExtension == "bundle" else { continue } - if let bundle = Bundle(url: url), - self.bundleIfContainsDeviceModels(bundle) != nil { - return bundle - } - } - } - } - return nil } diff --git a/apps/macos/Sources/Clawdis/GatewayConnection.swift b/apps/macos/Sources/Clawdis/GatewayConnection.swift index cd87eea2c..52ab9ce6f 100644 --- a/apps/macos/Sources/Clawdis/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdis/GatewayConnection.swift @@ -228,6 +228,14 @@ actor GatewayConnection { return trimmed.isEmpty ? nil : trimmed } + func snapshotPaths() -> (configPath: String?, stateDir: String?) { + guard let snapshot = self.lastSnapshot else { return (nil, nil) } + let configPath = snapshot.snapshot.configpath?.trimmingCharacters(in: .whitespacesAndNewlines) + let stateDir = snapshot.snapshot.statedir?.trimmingCharacters(in: .whitespacesAndNewlines) + return (configPath?.isEmpty == false ? configPath : nil, + stateDir?.isEmpty == false ? stateDir : nil) + } + func subscribe(bufferingNewest: Int = 100) -> AsyncStream { let id = UUID() let snapshot = self.lastSnapshot diff --git a/apps/macos/Sources/Clawdis/SettingsRootView.swift b/apps/macos/Sources/Clawdis/SettingsRootView.swift index f53ae9d31..655bd9644 100644 --- a/apps/macos/Sources/Clawdis/SettingsRootView.swift +++ b/apps/macos/Sources/Clawdis/SettingsRootView.swift @@ -105,8 +105,9 @@ struct SettingsRootView: View { } private var nixManagedBanner: some View { - let configPath = ClawdisConfigFile.url().path - let stateDir = ClawdisConfigFile.stateDirURL().path + let snapshotPaths = GatewayConnection.shared.snapshotPaths() + let configPath = snapshotPaths.configPath ?? ClawdisPaths.configURL.path + let stateDir = snapshotPaths.stateDir ?? ClawdisPaths.stateDirURL.path return VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { diff --git a/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift index 687313a53..2441f205d 100644 --- a/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift +++ b/apps/macos/Sources/ClawdisProtocol/GatewayModels.swift @@ -250,23 +250,31 @@ public struct Snapshot: Codable { public let health: AnyCodable public let stateversion: StateVersion public let uptimems: Int + public let configpath: String? + public let statedir: String? public init( presence: [PresenceEntry], health: AnyCodable, stateversion: StateVersion, - uptimems: Int + uptimems: Int, + configpath: String?, + statedir: String? ) { self.presence = presence self.health = health self.stateversion = stateversion self.uptimems = uptimems + self.configpath = configpath + self.statedir = statedir } private enum CodingKeys: String, CodingKey { case presence case health case stateversion = "stateVersion" case uptimems = "uptimeMs" + case configpath = "configPath" + case statedir = "stateDir" } } diff --git a/src/gateway/protocol/schema.ts b/src/gateway/protocol/schema.ts index ffd5260f8..8ba174494 100644 --- a/src/gateway/protocol/schema.ts +++ b/src/gateway/protocol/schema.ts @@ -37,6 +37,8 @@ export const SnapshotSchema = Type.Object( health: HealthSnapshotSchema, stateVersion: StateVersionSchema, uptimeMs: Type.Integer({ minimum: 0 }), + configPath: Type.Optional(NonEmptyString), + stateDir: Type.Optional(NonEmptyString), }, { additionalProperties: false }, ); diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index f90df1c50..bffd10021 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -6,7 +6,12 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; import { agentCommand } from "../commands/agent.js"; -import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js"; +import { + CONFIG_PATH_CLAWDIS, + STATE_DIR_CLAWDIS, + readConfigFileSnapshot, + writeConfigFile, +} from "../config/config.js"; import { emitAgentEvent } from "../infra/agent-events.js"; import { GatewayLockError } from "../infra/gateway-lock.js"; import { emitHeartbeatEvent } from "../infra/heartbeat-events.js"; @@ -190,6 +195,7 @@ vi.mock("../config/config.js", () => { return { CONFIG_PATH_CLAWDIS: resolveConfigPath(), + STATE_DIR_CLAWDIS: path.dirname(resolveConfigPath()), isNixMode: false, loadConfig: () => ({ agent: { @@ -2046,9 +2052,12 @@ describe("gateway server", () => { (o) => o.type === "res" && o.id === id, ); expect(res.ok).toBe(true); - expect((res.payload as { type?: unknown } | undefined)?.type).toBe( - "hello-ok", - ); + const payload = res.payload as + | { type?: unknown; snapshot?: { configPath?: string; stateDir?: string } } + | undefined; + expect(payload?.type).toBe("hello-ok"); + expect(payload?.snapshot?.configPath).toBe(CONFIG_PATH_CLAWDIS); + expect(payload?.snapshot?.stateDir).toBe(STATE_DIR_CLAWDIS); ws.close(); await server.close(); }); diff --git a/src/gateway/server.ts b/src/gateway/server.ts index d5bbe3114..425481422 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -46,6 +46,7 @@ import { getStatusSummary } from "../commands/status.js"; import { type ClawdisConfig, CONFIG_PATH_CLAWDIS, + STATE_DIR_CLAWDIS, isNixMode, loadConfig, parseConfigJson5, @@ -638,6 +639,8 @@ function buildSnapshot(): Snapshot { health: emptyHealth, stateVersion: { presence: presenceVersion, health: healthVersion }, uptimeMs, + configPath: CONFIG_PATH_CLAWDIS, + stateDir: STATE_DIR_CLAWDIS, }; }