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.
|
- 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: 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.
|
- 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).
|
- 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.
|
- Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||||
GATEWAY_WINDOWS_TASK_NAME,
|
GATEWAY_WINDOWS_TASK_NAME,
|
||||||
resolveGatewayLaunchAgentLabel,
|
resolveGatewayLaunchAgentLabel,
|
||||||
|
resolveGatewayProfileSuffix,
|
||||||
resolveGatewaySystemdServiceName,
|
resolveGatewaySystemdServiceName,
|
||||||
resolveGatewayWindowsTaskName,
|
resolveGatewayWindowsTaskName,
|
||||||
} from "./constants.js";
|
} 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", () => {
|
describe("formatGatewayServiceDescription", () => {
|
||||||
it("returns default description when no profile/version", () => {
|
it("returns default description when no profile/version", () => {
|
||||||
expect(formatGatewayServiceDescription()).toBe("Clawdbot Gateway");
|
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_SYSTEMD_SERVICE_NAMES: string[] = [];
|
||||||
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
|
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
|
||||||
|
|
||||||
export function resolveGatewayLaunchAgentLabel(profile?: string): string {
|
export function normalizeGatewayProfile(profile?: string): string | null {
|
||||||
const trimmed = profile?.trim();
|
|
||||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
|
||||||
return GATEWAY_LAUNCH_AGENT_LABEL;
|
|
||||||
}
|
|
||||||
return `com.clawdbot.${trimmed}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeGatewayProfile(profile?: string): string | null {
|
|
||||||
const trimmed = profile?.trim();
|
const trimmed = profile?.trim();
|
||||||
if (!trimmed || trimmed.toLowerCase() === "default") return null;
|
if (!trimmed || trimmed.toLowerCase() === "default") return null;
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGatewaySystemdServiceName(profile?: string): string {
|
export function resolveGatewayProfileSuffix(profile?: string): string {
|
||||||
const trimmed = profile?.trim();
|
const normalized = normalizeGatewayProfile(profile);
|
||||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
return normalized ? `-${normalized}` : "";
|
||||||
return GATEWAY_SYSTEMD_SERVICE_NAME;
|
}
|
||||||
|
|
||||||
|
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 {
|
export function resolveGatewayWindowsTaskName(profile?: string): string {
|
||||||
const trimmed = profile?.trim();
|
const normalized = normalizeGatewayProfile(profile);
|
||||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
if (!normalized) return GATEWAY_WINDOWS_TASK_NAME;
|
||||||
return GATEWAY_WINDOWS_TASK_NAME;
|
return `Clawdbot Gateway (${normalized})`;
|
||||||
}
|
|
||||||
return `Clawdbot Gateway (${trimmed})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatGatewayServiceDescription(params?: {
|
export function formatGatewayServiceDescription(params?: {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "./launchd-plist.js";
|
} from "./launchd-plist.js";
|
||||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||||
|
import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -29,11 +30,6 @@ function resolveLaunchAgentLabel(args?: { env?: Record<string, string | undefine
|
|||||||
if (envLabel) return envLabel;
|
if (envLabel) return envLabel;
|
||||||
return resolveGatewayLaunchAgentLabel(args?.env?.CLAWDBOT_PROFILE);
|
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(
|
function resolveLaunchAgentPlistPathForLabel(
|
||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
@@ -53,12 +49,7 @@ export function resolveGatewayLogPaths(env: Record<string, string | undefined>):
|
|||||||
stdoutPath: string;
|
stdoutPath: string;
|
||||||
stderrPath: string;
|
stderrPath: string;
|
||||||
} {
|
} {
|
||||||
const home = resolveHomeDir(env);
|
const stateDir = resolveGatewayStateDir(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 logDir = path.join(stateDir, "logs");
|
const logDir = path.join(stateDir, "logs");
|
||||||
return {
|
return {
|
||||||
logDir,
|
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(
|
export async function readLaunchAgentProgramArguments(
|
||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
): Promise<{
|
): 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", () => {
|
it("handles case-insensitive 'Default' profile", () => {
|
||||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "Default" };
|
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "Default" };
|
||||||
expect(resolveTaskScriptPath(env)).toBe(
|
expect(resolveTaskScriptPath(env)).toBe(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
|||||||
|
|
||||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||||
import { formatGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
import { formatGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||||
|
import { resolveGatewayStateDir } from "./paths.js";
|
||||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||||
import type { GatewayServiceRuntime } from "./service-runtime.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)}`;
|
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 {
|
export function resolveTaskScriptPath(env: Record<string, string | undefined>): string {
|
||||||
const home = resolveHomeDir(env);
|
const stateDir = resolveGatewayStateDir(env);
|
||||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
return path.join(stateDir, "gateway.cmd");
|
||||||
const suffix = profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
|
||||||
return path.join(home, `.clawdbot${suffix}`, "gateway.cmd");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function quoteCmdArg(value: string): string {
|
function quoteCmdArg(value: string): string {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "./constants.js";
|
} from "./constants.js";
|
||||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||||
|
import { resolveHomeDir } from "./paths.js";
|
||||||
import {
|
import {
|
||||||
enableSystemdUserLinger,
|
enableSystemdUserLinger,
|
||||||
readSystemdUserLingerStatus,
|
readSystemdUserLingerStatus,
|
||||||
@@ -28,12 +29,6 @@ const formatLine = (label: string, value: string) => {
|
|||||||
return `${colorize(rich, theme.muted, `${label}:`)} ${colorize(rich, theme.command, value)}`;
|
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(
|
function resolveSystemdUnitPathForName(
|
||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
name: string,
|
name: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user