feat: centralize config paths and expose in snapshot
This commit is contained in:
@@ -2,28 +2,17 @@ import Foundation
|
|||||||
|
|
||||||
enum ClawdisConfigFile {
|
enum ClawdisConfigFile {
|
||||||
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "config")
|
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 {
|
static func url() -> URL {
|
||||||
if let override = self.envPath(self.configPathEnv) {
|
ClawdisPaths.configURL
|
||||||
return URL(fileURLWithPath: override)
|
|
||||||
}
|
|
||||||
return self.stateDirURL()
|
|
||||||
.appendingPathComponent("clawdis.json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func stateDirURL() -> URL {
|
static func stateDirURL() -> URL {
|
||||||
if let override = self.envPath(self.stateDirEnv) {
|
ClawdisPaths.stateDirURL
|
||||||
return URL(fileURLWithPath: override, isDirectory: true)
|
|
||||||
}
|
|
||||||
return FileManager.default.homeDirectoryForCurrentUser
|
|
||||||
.appendingPathComponent(".clawdis", isDirectory: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func defaultWorkspaceURL() -> URL {
|
static func defaultWorkspaceURL() -> URL {
|
||||||
self.stateDirURL()
|
ClawdisPaths.workspaceURL
|
||||||
.appendingPathComponent("workspace", isDirectory: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func loadDict() -> [String: Any] {
|
static func loadDict() -> [String: Any] {
|
||||||
@@ -108,11 +97,4 @@ enum ClawdisConfigFile {
|
|||||||
self.logger.debug("agent workspace updated set=\(!trimmed.isEmpty)")
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
apps/macos/Sources/Clawdis/ClawdisPaths.swift
Normal file
37
apps/macos/Sources/Clawdis/ClawdisPaths.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,22 +129,6 @@ enum DeviceModelCatalog {
|
|||||||
if let bundle = self.bundleIfContainsDeviceModels(Bundle.main) {
|
if let bundle = self.bundleIfContainsDeviceModels(Bundle.main) {
|
||||||
return bundle
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,14 @@ actor GatewayConnection {
|
|||||||
return trimmed.isEmpty ? nil : trimmed
|
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<GatewayPush> {
|
func subscribe(bufferingNewest: Int = 100) -> AsyncStream<GatewayPush> {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
let snapshot = self.lastSnapshot
|
let snapshot = self.lastSnapshot
|
||||||
|
|||||||
@@ -105,8 +105,9 @@ struct SettingsRootView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var nixManagedBanner: some View {
|
private var nixManagedBanner: some View {
|
||||||
let configPath = ClawdisConfigFile.url().path
|
let snapshotPaths = GatewayConnection.shared.snapshotPaths()
|
||||||
let stateDir = ClawdisConfigFile.stateDirURL().path
|
let configPath = snapshotPaths.configPath ?? ClawdisPaths.configURL.path
|
||||||
|
let stateDir = snapshotPaths.stateDir ?? ClawdisPaths.stateDirURL.path
|
||||||
|
|
||||||
return VStack(alignment: .leading, spacing: 6) {
|
return VStack(alignment: .leading, spacing: 6) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
|
|||||||
@@ -250,23 +250,31 @@ public struct Snapshot: Codable {
|
|||||||
public let health: AnyCodable
|
public let health: AnyCodable
|
||||||
public let stateversion: StateVersion
|
public let stateversion: StateVersion
|
||||||
public let uptimems: Int
|
public let uptimems: Int
|
||||||
|
public let configpath: String?
|
||||||
|
public let statedir: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
presence: [PresenceEntry],
|
presence: [PresenceEntry],
|
||||||
health: AnyCodable,
|
health: AnyCodable,
|
||||||
stateversion: StateVersion,
|
stateversion: StateVersion,
|
||||||
uptimems: Int
|
uptimems: Int,
|
||||||
|
configpath: String?,
|
||||||
|
statedir: String?
|
||||||
) {
|
) {
|
||||||
self.presence = presence
|
self.presence = presence
|
||||||
self.health = health
|
self.health = health
|
||||||
self.stateversion = stateversion
|
self.stateversion = stateversion
|
||||||
self.uptimems = uptimems
|
self.uptimems = uptimems
|
||||||
|
self.configpath = configpath
|
||||||
|
self.statedir = statedir
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case presence
|
case presence
|
||||||
case health
|
case health
|
||||||
case stateversion = "stateVersion"
|
case stateversion = "stateVersion"
|
||||||
case uptimems = "uptimeMs"
|
case uptimems = "uptimeMs"
|
||||||
|
case configpath = "configPath"
|
||||||
|
case statedir = "stateDir"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const SnapshotSchema = Type.Object(
|
|||||||
health: HealthSnapshotSchema,
|
health: HealthSnapshotSchema,
|
||||||
stateVersion: StateVersionSchema,
|
stateVersion: StateVersionSchema,
|
||||||
uptimeMs: Type.Integer({ minimum: 0 }),
|
uptimeMs: Type.Integer({ minimum: 0 }),
|
||||||
|
configPath: Type.Optional(NonEmptyString),
|
||||||
|
stateDir: Type.Optional(NonEmptyString),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import path from "node:path";
|
|||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
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 { emitAgentEvent } from "../infra/agent-events.js";
|
||||||
import { GatewayLockError } from "../infra/gateway-lock.js";
|
import { GatewayLockError } from "../infra/gateway-lock.js";
|
||||||
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
||||||
@@ -190,6 +195,7 @@ vi.mock("../config/config.js", () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
CONFIG_PATH_CLAWDIS: resolveConfigPath(),
|
CONFIG_PATH_CLAWDIS: resolveConfigPath(),
|
||||||
|
STATE_DIR_CLAWDIS: path.dirname(resolveConfigPath()),
|
||||||
isNixMode: false,
|
isNixMode: false,
|
||||||
loadConfig: () => ({
|
loadConfig: () => ({
|
||||||
agent: {
|
agent: {
|
||||||
@@ -2046,9 +2052,12 @@ describe("gateway server", () => {
|
|||||||
(o) => o.type === "res" && o.id === id,
|
(o) => o.type === "res" && o.id === id,
|
||||||
);
|
);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
expect((res.payload as { type?: unknown } | undefined)?.type).toBe(
|
const payload = res.payload as
|
||||||
"hello-ok",
|
| { 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();
|
ws.close();
|
||||||
await server.close();
|
await server.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { getStatusSummary } from "../commands/status.js";
|
|||||||
import {
|
import {
|
||||||
type ClawdisConfig,
|
type ClawdisConfig,
|
||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
|
STATE_DIR_CLAWDIS,
|
||||||
isNixMode,
|
isNixMode,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
parseConfigJson5,
|
parseConfigJson5,
|
||||||
@@ -638,6 +639,8 @@ function buildSnapshot(): Snapshot {
|
|||||||
health: emptyHealth,
|
health: emptyHealth,
|
||||||
stateVersion: { presence: presenceVersion, health: healthVersion },
|
stateVersion: { presence: presenceVersion, health: healthVersion },
|
||||||
uptimeMs,
|
uptimeMs,
|
||||||
|
configPath: CONFIG_PATH_CLAWDIS,
|
||||||
|
stateDir: STATE_DIR_CLAWDIS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user