refactor: centralize daemon path resolution
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
||||
- Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.
|
||||
- Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts.
|
||||
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
|
||||
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
|
||||
- Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields.
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewayProfileSuffix,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
@@ -153,6 +154,25 @@ describe("resolveGatewayWindowsTaskName", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewayProfileSuffix", () => {
|
||||
it("returns empty string when no profile is set", () => {
|
||||
expect(resolveGatewayProfileSuffix()).toBe("");
|
||||
});
|
||||
|
||||
it("returns empty string for default profiles", () => {
|
||||
expect(resolveGatewayProfileSuffix("default")).toBe("");
|
||||
expect(resolveGatewayProfileSuffix(" Default ")).toBe("");
|
||||
});
|
||||
|
||||
it("returns a hyphenated suffix for custom profiles", () => {
|
||||
expect(resolveGatewayProfileSuffix("dev")).toBe("-dev");
|
||||
});
|
||||
|
||||
it("trims whitespace from profiles", () => {
|
||||
expect(resolveGatewayProfileSuffix(" staging ")).toBe("-staging");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatGatewayServiceDescription", () => {
|
||||
it("returns default description when no profile/version", () => {
|
||||
expect(formatGatewayServiceDescription()).toBe("Clawdbot Gateway");
|
||||
|
||||
@@ -8,34 +8,35 @@ export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = ["com.steipete.clawdbot.gatewa
|
||||
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES: string[] = [];
|
||||
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
|
||||
|
||||
export function resolveGatewayLaunchAgentLabel(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
}
|
||||
return `com.clawdbot.${trimmed}`;
|
||||
}
|
||||
|
||||
function normalizeGatewayProfile(profile?: string): string | null {
|
||||
export function normalizeGatewayProfile(profile?: string): string | null {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveGatewaySystemdServiceName(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_SYSTEMD_SERVICE_NAME;
|
||||
export function resolveGatewayProfileSuffix(profile?: string): string {
|
||||
const normalized = normalizeGatewayProfile(profile);
|
||||
return normalized ? `-${normalized}` : "";
|
||||
}
|
||||
|
||||
export function resolveGatewayLaunchAgentLabel(profile?: string): string {
|
||||
const normalized = normalizeGatewayProfile(profile);
|
||||
if (!normalized) {
|
||||
return GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
}
|
||||
return `clawdbot-gateway-${trimmed}`;
|
||||
return `com.clawdbot.${normalized}`;
|
||||
}
|
||||
|
||||
export function resolveGatewaySystemdServiceName(profile?: string): string {
|
||||
const suffix = resolveGatewayProfileSuffix(profile);
|
||||
if (!suffix) return GATEWAY_SYSTEMD_SERVICE_NAME;
|
||||
return `clawdbot-gateway${suffix}`;
|
||||
}
|
||||
|
||||
export function resolveGatewayWindowsTaskName(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_WINDOWS_TASK_NAME;
|
||||
}
|
||||
return `Clawdbot Gateway (${trimmed})`;
|
||||
const normalized = normalizeGatewayProfile(profile);
|
||||
if (!normalized) return GATEWAY_WINDOWS_TASK_NAME;
|
||||
return `Clawdbot Gateway (${normalized})`;
|
||||
}
|
||||
|
||||
export function formatGatewayServiceDescription(params?: {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "./launchd-plist.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -29,11 +30,6 @@ function resolveLaunchAgentLabel(args?: { env?: Record<string, string | undefine
|
||||
if (envLabel) return envLabel;
|
||||
return resolveGatewayLaunchAgentLabel(args?.env?.CLAWDBOT_PROFILE);
|
||||
}
|
||||
function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
||||
if (!home) throw new Error("Missing HOME");
|
||||
return home;
|
||||
}
|
||||
|
||||
function resolveLaunchAgentPlistPathForLabel(
|
||||
env: Record<string, string | undefined>,
|
||||
@@ -53,12 +49,7 @@ export function resolveGatewayLogPaths(env: Record<string, string | undefined>):
|
||||
stdoutPath: string;
|
||||
stderrPath: string;
|
||||
} {
|
||||
const home = resolveHomeDir(env);
|
||||
const stateOverride = env.CLAWDBOT_STATE_DIR?.trim();
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
||||
const suffix = profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
const defaultStateDir = path.join(home, `.clawdbot${suffix}`);
|
||||
const stateDir = stateOverride ? resolveUserPathWithHome(stateOverride, home) : defaultStateDir;
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
const logDir = path.join(stateDir, "logs");
|
||||
return {
|
||||
logDir,
|
||||
@@ -67,16 +58,6 @@ export function resolveGatewayLogPaths(env: Record<string, string | undefined>):
|
||||
};
|
||||
}
|
||||
|
||||
function resolveUserPathWithHome(input: string, home: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (trimmed.startsWith("~")) {
|
||||
const expanded = trimmed.replace(/^~(?=$|[\\/])/, home);
|
||||
return path.resolve(expanded);
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
export async function readLaunchAgentProgramArguments(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<{
|
||||
|
||||
37
src/daemon/paths.test.ts
Normal file
37
src/daemon/paths.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
|
||||
describe("resolveGatewayStateDir", () => {
|
||||
it("uses the default state dir when no overrides are set", () => {
|
||||
const env = { HOME: "/Users/test" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".clawdbot"));
|
||||
});
|
||||
|
||||
it("appends the profile suffix when set", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "rescue" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".clawdbot-rescue"));
|
||||
});
|
||||
|
||||
it("treats default profiles as the base state dir", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "Default" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".clawdbot"));
|
||||
});
|
||||
|
||||
it("uses CLAWDBOT_STATE_DIR when provided", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_STATE_DIR: "/var/lib/clawdbot" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.resolve("/var/lib/clawdbot"));
|
||||
});
|
||||
|
||||
it("expands ~ in CLAWDBOT_STATE_DIR", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_STATE_DIR: "~/clawdbot-state" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.resolve("/Users/test/clawdbot-state"));
|
||||
});
|
||||
|
||||
it("preserves Windows absolute paths without HOME", () => {
|
||||
const env = { CLAWDBOT_STATE_DIR: "C:\\State\\clawdbot" };
|
||||
expect(resolveGatewayStateDir(env)).toBe("C:\\State\\clawdbot");
|
||||
});
|
||||
});
|
||||
37
src/daemon/paths.ts
Normal file
37
src/daemon/paths.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveGatewayProfileSuffix } from "./constants.js";
|
||||
|
||||
const windowsAbsolutePath = /^[a-zA-Z]:[\\/]/;
|
||||
const windowsUncPath = /^\\\\/;
|
||||
|
||||
export function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
||||
if (!home) throw new Error("Missing HOME");
|
||||
return home;
|
||||
}
|
||||
|
||||
export function resolveUserPathWithHome(input: string, home?: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (trimmed.startsWith("~")) {
|
||||
if (!home) throw new Error("Missing HOME");
|
||||
const expanded = trimmed.replace(/^~(?=$|[\\/])/, home);
|
||||
return path.resolve(expanded);
|
||||
}
|
||||
if (windowsAbsolutePath.test(trimmed) || windowsUncPath.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
export function resolveGatewayStateDir(env: Record<string, string | undefined>): string {
|
||||
const override = env.CLAWDBOT_STATE_DIR?.trim();
|
||||
if (override) {
|
||||
const home = override.startsWith("~") ? resolveHomeDir(env) : undefined;
|
||||
return resolveUserPathWithHome(override, home);
|
||||
}
|
||||
const home = resolveHomeDir(env);
|
||||
const suffix = resolveGatewayProfileSuffix(env.CLAWDBOT_PROFILE);
|
||||
return path.join(home, `.clawdbot${suffix}`);
|
||||
}
|
||||
@@ -58,6 +58,15 @@ describe("resolveTaskScriptPath", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers CLAWDBOT_STATE_DIR over profile-derived defaults", () => {
|
||||
const env = {
|
||||
USERPROFILE: "C:\\Users\\test",
|
||||
CLAWDBOT_PROFILE: "rescue",
|
||||
CLAWDBOT_STATE_DIR: "C:\\State\\clawdbot",
|
||||
};
|
||||
expect(resolveTaskScriptPath(env)).toBe(path.join("C:\\State\\clawdbot", "gateway.cmd"));
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'Default' profile", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "Default" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
||||
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import { formatGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
|
||||
@@ -15,17 +16,9 @@ const formatLine = (label: string, value: string) => {
|
||||
return `${colorize(rich, theme.muted, `${label}:`)} ${colorize(rich, theme.command, value)}`;
|
||||
};
|
||||
|
||||
function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||
const home = env.USERPROFILE?.trim() || env.HOME?.trim();
|
||||
if (!home) throw new Error("Missing HOME");
|
||||
return home;
|
||||
}
|
||||
|
||||
export function resolveTaskScriptPath(env: Record<string, string | undefined>): string {
|
||||
const home = resolveHomeDir(env);
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
||||
const suffix = profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
return path.join(home, `.clawdbot${suffix}`, "gateway.cmd");
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
return path.join(stateDir, "gateway.cmd");
|
||||
}
|
||||
|
||||
function quoteCmdArg(value: string): string {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
import { resolveHomeDir } from "./paths.js";
|
||||
import {
|
||||
enableSystemdUserLinger,
|
||||
readSystemdUserLingerStatus,
|
||||
@@ -28,12 +29,6 @@ const formatLine = (label: string, value: string) => {
|
||||
return `${colorize(rich, theme.muted, `${label}:`)} ${colorize(rich, theme.command, value)}`;
|
||||
};
|
||||
|
||||
function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
||||
if (!home) throw new Error("Missing HOME");
|
||||
return home;
|
||||
}
|
||||
|
||||
function resolveSystemdUnitPathForName(
|
||||
env: Record<string, string | undefined>,
|
||||
name: string,
|
||||
|
||||
Reference in New Issue
Block a user