feat: add gateway daemon runtime selector
This commit is contained in:
@@ -228,6 +228,7 @@ export function buildProgram() {
|
||||
.option("--tailscale <mode>", "Tailscale: off|serve|funnel")
|
||||
.option("--tailscale-reset-on-exit", "Reset tailscale serve/funnel on exit")
|
||||
.option("--install-daemon", "Install gateway daemon")
|
||||
.option("--daemon-runtime <runtime>", "Daemon runtime: node|bun")
|
||||
.option("--skip-skills", "Skip skills setup")
|
||||
.option("--skip-health", "Skip health check")
|
||||
.option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun")
|
||||
@@ -268,6 +269,7 @@ export function buildProgram() {
|
||||
tailscale: opts.tailscale as "off" | "serve" | "funnel" | undefined,
|
||||
tailscaleResetOnExit: Boolean(opts.tailscaleResetOnExit),
|
||||
installDaemon: Boolean(opts.installDaemon),
|
||||
daemonRuntime: opts.daemonRuntime as "node" | "bun" | undefined,
|
||||
skipSkills: Boolean(opts.skipSkills),
|
||||
skipHealth: Boolean(opts.skipHealth),
|
||||
nodeManager: opts.nodeManager as "npm" | "pnpm" | "bun" | undefined,
|
||||
|
||||
@@ -59,6 +59,11 @@ import {
|
||||
import { setupProviders } from "./onboard-providers.js";
|
||||
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
|
||||
import { setupSkills } from "./onboard-skills.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "./daemon-runtime.js";
|
||||
import {
|
||||
applyOpenAICodexModelDefault,
|
||||
OPENAI_CODEX_DEFAULT_MODEL,
|
||||
@@ -502,11 +507,13 @@ async function maybeInstallDaemon(params: {
|
||||
runtime: RuntimeEnv;
|
||||
port: number;
|
||||
gatewayToken?: string;
|
||||
daemonRuntime?: GatewayDaemonRuntime;
|
||||
}) {
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
let shouldCheckLinger = false;
|
||||
let shouldInstall = true;
|
||||
let daemonRuntime = params.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
if (loaded) {
|
||||
const action = guardCancel(
|
||||
await select({
|
||||
@@ -531,11 +538,25 @@ async function maybeInstallDaemon(params: {
|
||||
}
|
||||
|
||||
if (shouldInstall) {
|
||||
if (!params.daemonRuntime) {
|
||||
daemonRuntime = guardCancel(
|
||||
await select({
|
||||
message: "Gateway daemon runtime",
|
||||
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
}),
|
||||
params.runtime,
|
||||
) as GatewayDaemonRuntime;
|
||||
}
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({ port: params.port, dev: devMode });
|
||||
await resolveGatewayProgramArguments({
|
||||
port: params.port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
});
|
||||
const environment: Record<string, string | undefined> = {
|
||||
PATH: process.env.PATH,
|
||||
CLAWDBOT_GATEWAY_TOKEN: params.gatewayToken,
|
||||
|
||||
27
src/commands/daemon-runtime.ts
Normal file
27
src/commands/daemon-runtime.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export type GatewayDaemonRuntime = "node" | "bun";
|
||||
|
||||
export const DEFAULT_GATEWAY_DAEMON_RUNTIME: GatewayDaemonRuntime = "node";
|
||||
|
||||
export const GATEWAY_DAEMON_RUNTIME_OPTIONS: Array<{
|
||||
value: GatewayDaemonRuntime;
|
||||
label: string;
|
||||
hint?: string;
|
||||
}> = [
|
||||
{
|
||||
value: "node",
|
||||
label: "Node (recommended)",
|
||||
hint:
|
||||
"Required for WhatsApp (Baileys WebSocket + Bun can corrupt memory on reconnect)",
|
||||
},
|
||||
{
|
||||
value: "bun",
|
||||
label: "Bun (faster)",
|
||||
hint: "Use only when WhatsApp is disabled",
|
||||
},
|
||||
];
|
||||
|
||||
export function isGatewayDaemonRuntime(
|
||||
value: string | undefined,
|
||||
): value is GatewayDaemonRuntime {
|
||||
return value === "node" || value === "bun";
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { confirm, intro, note, outro } from "@clack/prompts";
|
||||
import { confirm, intro, note, outro, select } from "@clack/prompts";
|
||||
|
||||
import {
|
||||
DEFAULT_SANDBOX_BROWSER_IMAGE,
|
||||
@@ -45,6 +45,11 @@ import {
|
||||
guardCancel,
|
||||
printWizardHeader,
|
||||
} from "./onboard-helpers.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "./daemon-runtime.js";
|
||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||
|
||||
function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
||||
@@ -768,12 +773,24 @@ async function maybeMigrateLegacyGatewayService(
|
||||
);
|
||||
if (!install) return;
|
||||
|
||||
const daemonRuntime = guardCancel(
|
||||
await select({
|
||||
message: "Gateway daemon runtime",
|
||||
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
}),
|
||||
runtime,
|
||||
) as GatewayDaemonRuntime;
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
const port = resolveGatewayPort(cfg, process.env);
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({ port, dev: devMode });
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
});
|
||||
const environment: Record<string, string | undefined> = {
|
||||
PATH: process.env.PATH,
|
||||
CLAWDBOT_GATEWAY_TOKEN:
|
||||
|
||||
@@ -30,6 +30,10 @@ import type {
|
||||
OnboardMode,
|
||||
OnboardOptions,
|
||||
} from "./onboard-types.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
isGatewayDaemonRuntime,
|
||||
} from "./daemon-runtime.js";
|
||||
import { ensureSystemdUserLingerNonInteractive } from "./systemd-linger.js";
|
||||
|
||||
export async function runNonInteractiveOnboarding(
|
||||
@@ -223,13 +227,24 @@ export async function runNonInteractiveOnboarding(
|
||||
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
|
||||
});
|
||||
|
||||
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
|
||||
if (opts.installDaemon) {
|
||||
if (!isGatewayDaemonRuntime(daemonRuntimeRaw)) {
|
||||
runtime.error("Invalid --daemon-runtime (use node or bun)");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const service = resolveGatewayService();
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({ port, dev: devMode });
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntimeRaw,
|
||||
});
|
||||
const environment: Record<string, string | undefined> = {
|
||||
PATH: process.env.PATH,
|
||||
CLAWDBOT_GATEWAY_TOKEN: gatewayToken,
|
||||
@@ -260,6 +275,7 @@ export async function runNonInteractiveOnboarding(
|
||||
authChoice,
|
||||
gateway: { port, bind, authMode, tailscaleMode },
|
||||
installDaemon: Boolean(opts.installDaemon),
|
||||
daemonRuntime: opts.installDaemon ? daemonRuntimeRaw : undefined,
|
||||
skipSkills: Boolean(opts.skipSkills),
|
||||
skipHealth: Boolean(opts.skipHealth),
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||
|
||||
export type OnboardMode = "local" | "remote";
|
||||
export type AuthChoice =
|
||||
| "oauth"
|
||||
@@ -33,6 +35,7 @@ export type OnboardOptions = {
|
||||
tailscale?: TailscaleMode;
|
||||
tailscaleResetOnExit?: boolean;
|
||||
installDaemon?: boolean;
|
||||
daemonRuntime?: GatewayDaemonRuntime;
|
||||
skipSkills?: boolean;
|
||||
skipHealth?: boolean;
|
||||
nodeManager?: NodeManagerChoice;
|
||||
|
||||
@@ -6,6 +6,8 @@ type GatewayProgramArgs = {
|
||||
workingDirectory?: string;
|
||||
};
|
||||
|
||||
type GatewayRuntimePreference = "auto" | "node" | "bun";
|
||||
|
||||
function isNodeRuntime(execPath: string): boolean {
|
||||
const base = path.basename(execPath).toLowerCase();
|
||||
return base === "node" || base === "node.exe";
|
||||
@@ -114,23 +116,69 @@ function resolveRepoRootForDev(): string {
|
||||
}
|
||||
|
||||
async function resolveBunPath(): Promise<string> {
|
||||
// Bun is expected to be in PATH, resolve via which/where
|
||||
const bunPath = await resolveBinaryPath("bun");
|
||||
return bunPath;
|
||||
}
|
||||
|
||||
async function resolveNodePath(): Promise<string> {
|
||||
const nodePath = await resolveBinaryPath("node");
|
||||
return nodePath;
|
||||
}
|
||||
|
||||
async function resolveBinaryPath(binary: string): Promise<string> {
|
||||
const { execSync } = await import("node:child_process");
|
||||
const cmd = process.platform === "win32" ? "where" : "which";
|
||||
try {
|
||||
const bunPath = execSync("which bun", { encoding: "utf8" }).trim();
|
||||
await fs.access(bunPath);
|
||||
return bunPath;
|
||||
const output = execSync(`${cmd} ${binary}`, { encoding: "utf8" }).trim();
|
||||
const resolved = output.split(/\r?\n/)[0]?.trim();
|
||||
if (!resolved) throw new Error("empty");
|
||||
await fs.access(resolved);
|
||||
return resolved;
|
||||
} catch {
|
||||
throw new Error("Bun not found in PATH. Install bun: https://bun.sh");
|
||||
if (binary === "bun") {
|
||||
throw new Error("Bun not found in PATH. Install bun: https://bun.sh");
|
||||
}
|
||||
throw new Error("Node not found in PATH. Install Node 22+.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveGatewayProgramArguments(params: {
|
||||
port: number;
|
||||
dev?: boolean;
|
||||
runtime?: GatewayRuntimePreference;
|
||||
}): Promise<GatewayProgramArgs> {
|
||||
const gatewayArgs = ["gateway-daemon", "--port", String(params.port)];
|
||||
const execPath = process.execPath;
|
||||
const runtime = params.runtime ?? "auto";
|
||||
|
||||
if (runtime === "node") {
|
||||
const nodePath = isNodeRuntime(execPath)
|
||||
? execPath
|
||||
: await resolveNodePath();
|
||||
const cliEntrypointPath = await resolveCliEntrypointPathForService();
|
||||
return {
|
||||
programArguments: [nodePath, cliEntrypointPath, ...gatewayArgs],
|
||||
};
|
||||
}
|
||||
|
||||
if (runtime === "bun") {
|
||||
if (params.dev) {
|
||||
const repoRoot = resolveRepoRootForDev();
|
||||
const devCliPath = path.join(repoRoot, "src", "index.ts");
|
||||
await fs.access(devCliPath);
|
||||
const bunPath = isBunRuntime(execPath) ? execPath : await resolveBunPath();
|
||||
return {
|
||||
programArguments: [bunPath, devCliPath, ...gatewayArgs],
|
||||
workingDirectory: repoRoot,
|
||||
};
|
||||
}
|
||||
|
||||
const bunPath = isBunRuntime(execPath) ? execPath : await resolveBunPath();
|
||||
const cliEntrypointPath = await resolveCliEntrypointPathForService();
|
||||
return {
|
||||
programArguments: [bunPath, cliEntrypointPath, ...gatewayArgs],
|
||||
};
|
||||
}
|
||||
|
||||
if (!params.dev) {
|
||||
try {
|
||||
|
||||
@@ -52,6 +52,11 @@ import type {
|
||||
OnboardOptions,
|
||||
ResetScope,
|
||||
} from "../commands/onboard-types.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "../commands/daemon-runtime.js";
|
||||
import {
|
||||
applyOpenAICodexModelDefault,
|
||||
OPENAI_CODEX_DEFAULT_MODEL,
|
||||
@@ -629,6 +634,11 @@ export async function runOnboardingWizard(
|
||||
});
|
||||
|
||||
if (installDaemon) {
|
||||
const daemonRuntime = (await prompter.select({
|
||||
message: "Gateway daemon runtime",
|
||||
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||
initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
})) as GatewayDaemonRuntime;
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
if (loaded) {
|
||||
@@ -655,7 +665,11 @@ export async function runOnboardingWizard(
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({ port, dev: devMode });
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: daemonRuntime,
|
||||
});
|
||||
const environment: Record<string, string | undefined> = {
|
||||
PATH: process.env.PATH,
|
||||
CLAWDBOT_GATEWAY_TOKEN: gatewayToken,
|
||||
|
||||
Reference in New Issue
Block a user