| import os from "node:os"; |
| import path from "node:path"; |
| import { VERSION } from "../version.js"; |
| import { |
| GATEWAY_SERVICE_KIND, |
| GATEWAY_SERVICE_MARKER, |
| resolveGatewayLaunchAgentLabel, |
| resolveGatewaySystemdServiceName, |
| resolveGatewayWindowsTaskName, |
| NODE_SERVICE_KIND, |
| NODE_SERVICE_MARKER, |
| NODE_WINDOWS_TASK_SCRIPT_NAME, |
| resolveNodeLaunchAgentLabel, |
| resolveNodeSystemdServiceName, |
| resolveNodeWindowsTaskName, |
| } from "./constants.js"; |
|
|
| export type MinimalServicePathOptions = { |
| platform?: NodeJS.Platform; |
| extraDirs?: string[]; |
| home?: string; |
| env?: Record<string, string | undefined>; |
| }; |
|
|
| type BuildServicePathOptions = MinimalServicePathOptions & { |
| env?: Record<string, string | undefined>; |
| }; |
|
|
| type SharedServiceEnvironmentFields = { |
| stateDir: string | undefined; |
| configPath: string | undefined; |
| tmpDir: string; |
| minimalPath: string | undefined; |
| proxyEnv: Record<string, string | undefined>; |
| nodeCaCerts: string | undefined; |
| nodeUseSystemCa: string | undefined; |
| }; |
|
|
| const SERVICE_PROXY_ENV_KEYS = [ |
| "HTTP_PROXY", |
| "HTTPS_PROXY", |
| "NO_PROXY", |
| "ALL_PROXY", |
| "http_proxy", |
| "https_proxy", |
| "no_proxy", |
| "all_proxy", |
| ] as const; |
|
|
| function readServiceProxyEnvironment( |
| env: Record<string, string | undefined>, |
| ): Record<string, string | undefined> { |
| const out: Record<string, string | undefined> = {}; |
| for (const key of SERVICE_PROXY_ENV_KEYS) { |
| const value = env[key]; |
| if (typeof value !== "string") { |
| continue; |
| } |
| const trimmed = value.trim(); |
| if (!trimmed) { |
| continue; |
| } |
| out[key] = trimmed; |
| } |
| return out; |
| } |
|
|
| function addNonEmptyDir(dirs: string[], dir: string | undefined): void { |
| if (dir) { |
| dirs.push(dir); |
| } |
| } |
|
|
| function appendSubdir(base: string | undefined, subdir: string): string | undefined { |
| if (!base) { |
| return undefined; |
| } |
| return base.endsWith(`/${subdir}`) ? base : path.posix.join(base, subdir); |
| } |
|
|
| function addCommonUserBinDirs(dirs: string[], home: string): void { |
| dirs.push(`${home}/.local/bin`); |
| dirs.push(`${home}/.npm-global/bin`); |
| dirs.push(`${home}/bin`); |
| dirs.push(`${home}/.volta/bin`); |
| dirs.push(`${home}/.asdf/shims`); |
| dirs.push(`${home}/.bun/bin`); |
| } |
|
|
| function addCommonEnvConfiguredBinDirs( |
| dirs: string[], |
| env: Record<string, string | undefined> | undefined, |
| ): void { |
| addNonEmptyDir(dirs, env?.PNPM_HOME); |
| addNonEmptyDir(dirs, appendSubdir(env?.NPM_CONFIG_PREFIX, "bin")); |
| addNonEmptyDir(dirs, appendSubdir(env?.BUN_INSTALL, "bin")); |
| addNonEmptyDir(dirs, appendSubdir(env?.VOLTA_HOME, "bin")); |
| addNonEmptyDir(dirs, appendSubdir(env?.ASDF_DATA_DIR, "shims")); |
| } |
|
|
| function resolveSystemPathDirs(platform: NodeJS.Platform): string[] { |
| if (platform === "darwin") { |
| return ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]; |
| } |
| if (platform === "linux") { |
| return ["/usr/local/bin", "/usr/bin", "/bin"]; |
| } |
| return []; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function resolveDarwinUserBinDirs( |
| home: string | undefined, |
| env?: Record<string, string | undefined>, |
| ): string[] { |
| if (!home) { |
| return []; |
| } |
|
|
| const dirs: string[] = []; |
|
|
| |
| |
| |
| addCommonEnvConfiguredBinDirs(dirs, env); |
| |
| |
| addNonEmptyDir(dirs, env?.NVM_DIR); |
| |
| addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "aliases/default/bin")); |
| |
|
|
| |
| addCommonUserBinDirs(dirs, home); |
|
|
| |
| |
| |
| dirs.push(`${home}/Library/Application Support/fnm/aliases/default/bin`); |
| dirs.push(`${home}/.fnm/aliases/default/bin`); |
| |
| dirs.push(`${home}/Library/pnpm`); |
| dirs.push(`${home}/.local/share/pnpm`); |
|
|
| return dirs; |
| } |
|
|
| |
| |
| |
| |
| export function resolveLinuxUserBinDirs( |
| home: string | undefined, |
| env?: Record<string, string | undefined>, |
| ): string[] { |
| if (!home) { |
| return []; |
| } |
|
|
| const dirs: string[] = []; |
|
|
| |
| addCommonEnvConfiguredBinDirs(dirs, env); |
| addNonEmptyDir(dirs, appendSubdir(env?.NVM_DIR, "current/bin")); |
| addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "current/bin")); |
|
|
| |
| addCommonUserBinDirs(dirs, home); |
|
|
| |
| dirs.push(`${home}/.nvm/current/bin`); |
| dirs.push(`${home}/.fnm/current/bin`); |
| dirs.push(`${home}/.local/share/pnpm`); |
|
|
| return dirs; |
| } |
|
|
| export function getMinimalServicePathParts(options: MinimalServicePathOptions = {}): string[] { |
| const platform = options.platform ?? process.platform; |
| if (platform === "win32") { |
| return []; |
| } |
|
|
| const parts: string[] = []; |
| const extraDirs = options.extraDirs ?? []; |
| const systemDirs = resolveSystemPathDirs(platform); |
|
|
| |
| const userDirs = |
| platform === "linux" |
| ? resolveLinuxUserBinDirs(options.home, options.env) |
| : platform === "darwin" |
| ? resolveDarwinUserBinDirs(options.home, options.env) |
| : []; |
|
|
| const add = (dir: string) => { |
| if (!dir) { |
| return; |
| } |
| if (!parts.includes(dir)) { |
| parts.push(dir); |
| } |
| }; |
|
|
| for (const dir of extraDirs) { |
| add(dir); |
| } |
| |
| for (const dir of userDirs) { |
| add(dir); |
| } |
| for (const dir of systemDirs) { |
| add(dir); |
| } |
|
|
| return parts; |
| } |
|
|
| export function getMinimalServicePathPartsFromEnv(options: BuildServicePathOptions = {}): string[] { |
| const env = options.env ?? process.env; |
| return getMinimalServicePathParts({ |
| ...options, |
| home: options.home ?? env.HOME, |
| env, |
| }); |
| } |
|
|
| export function buildMinimalServicePath(options: BuildServicePathOptions = {}): string { |
| const env = options.env ?? process.env; |
| const platform = options.platform ?? process.platform; |
| if (platform === "win32") { |
| return env.PATH ?? ""; |
| } |
|
|
| return getMinimalServicePathPartsFromEnv({ ...options, env }).join(path.posix.delimiter); |
| } |
|
|
| export function buildServiceEnvironment(params: { |
| env: Record<string, string | undefined>; |
| port: number; |
| launchdLabel?: string; |
| platform?: NodeJS.Platform; |
| }): Record<string, string | undefined> { |
| const { env, port, launchdLabel } = params; |
| const platform = params.platform ?? process.platform; |
| const sharedEnv = resolveSharedServiceEnvironmentFields(env, platform); |
| const profile = env.OPENCLAW_PROFILE; |
| const resolvedLaunchdLabel = |
| launchdLabel || (platform === "darwin" ? resolveGatewayLaunchAgentLabel(profile) : undefined); |
| const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`; |
| return { |
| ...buildCommonServiceEnvironment(env, sharedEnv), |
| OPENCLAW_PROFILE: profile, |
| OPENCLAW_GATEWAY_PORT: String(port), |
| OPENCLAW_LAUNCHD_LABEL: resolvedLaunchdLabel, |
| OPENCLAW_SYSTEMD_UNIT: systemdUnit, |
| OPENCLAW_WINDOWS_TASK_NAME: resolveGatewayWindowsTaskName(profile), |
| OPENCLAW_SERVICE_MARKER: GATEWAY_SERVICE_MARKER, |
| OPENCLAW_SERVICE_KIND: GATEWAY_SERVICE_KIND, |
| OPENCLAW_SERVICE_VERSION: VERSION, |
| }; |
| } |
|
|
| export function buildNodeServiceEnvironment(params: { |
| env: Record<string, string | undefined>; |
| platform?: NodeJS.Platform; |
| }): Record<string, string | undefined> { |
| const { env } = params; |
| const platform = params.platform ?? process.platform; |
| const sharedEnv = resolveSharedServiceEnvironmentFields(env, platform); |
| const gatewayToken = |
| env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim() || undefined; |
| return { |
| ...buildCommonServiceEnvironment(env, sharedEnv), |
| OPENCLAW_GATEWAY_TOKEN: gatewayToken, |
| OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(), |
| OPENCLAW_SYSTEMD_UNIT: resolveNodeSystemdServiceName(), |
| OPENCLAW_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(), |
| OPENCLAW_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME, |
| OPENCLAW_LOG_PREFIX: "node", |
| OPENCLAW_SERVICE_MARKER: NODE_SERVICE_MARKER, |
| OPENCLAW_SERVICE_KIND: NODE_SERVICE_KIND, |
| OPENCLAW_SERVICE_VERSION: VERSION, |
| }; |
| } |
|
|
| function buildCommonServiceEnvironment( |
| env: Record<string, string | undefined>, |
| sharedEnv: SharedServiceEnvironmentFields, |
| ): Record<string, string | undefined> { |
| const serviceEnv: Record<string, string | undefined> = { |
| HOME: env.HOME, |
| TMPDIR: sharedEnv.tmpDir, |
| ...sharedEnv.proxyEnv, |
| NODE_EXTRA_CA_CERTS: sharedEnv.nodeCaCerts, |
| NODE_USE_SYSTEM_CA: sharedEnv.nodeUseSystemCa, |
| OPENCLAW_STATE_DIR: sharedEnv.stateDir, |
| OPENCLAW_CONFIG_PATH: sharedEnv.configPath, |
| }; |
| if (sharedEnv.minimalPath) { |
| serviceEnv.PATH = sharedEnv.minimalPath; |
| } |
| return serviceEnv; |
| } |
|
|
| function resolveSharedServiceEnvironmentFields( |
| env: Record<string, string | undefined>, |
| platform: NodeJS.Platform, |
| ): SharedServiceEnvironmentFields { |
| const stateDir = env.OPENCLAW_STATE_DIR; |
| const configPath = env.OPENCLAW_CONFIG_PATH; |
| |
| const tmpDir = env.TMPDIR?.trim() || os.tmpdir(); |
| const proxyEnv = readServiceProxyEnvironment(env); |
| |
| |
| |
| const nodeCaCerts = |
| env.NODE_EXTRA_CA_CERTS ?? (platform === "darwin" ? "/etc/ssl/cert.pem" : undefined); |
| const nodeUseSystemCa = env.NODE_USE_SYSTEM_CA ?? (platform === "darwin" ? "1" : undefined); |
| return { |
| stateDir, |
| configPath, |
| tmpDir, |
| |
| |
| minimalPath: platform === "win32" ? undefined : buildMinimalServicePath({ env, platform }), |
| proxyEnv, |
| nodeCaCerts, |
| nodeUseSystemCa, |
| }; |
| } |
|
|