feat(cli): add daemonless mode for container/cloud environments
Support DENCHCLAW_DAEMONLESS=1 env var and --skip-daemon-install flag across all commands (bootstrap, update, start, restart, stop) to skip gateway daemon management and LaunchAgent registration in environments without systemd/launchd.
This commit is contained in:
parent
4436e57d9a
commit
76f8838362
22
README.md
22
README.md
@ -57,6 +57,28 @@ openclaw --profile dench gateway restart
|
||||
openclaw --profile dench uninstall
|
||||
```
|
||||
|
||||
### Daemonless / Docker
|
||||
|
||||
For containers or environments without systemd/launchd, set the environment variable once:
|
||||
|
||||
```bash
|
||||
export DENCHCLAW_DAEMONLESS=1
|
||||
```
|
||||
|
||||
This skips all gateway daemon management (install/start/stop/restart) and launchd LaunchAgent installation across all commands. You must start the gateway yourself as a foreground process:
|
||||
|
||||
```bash
|
||||
openclaw --profile dench gateway --port 19001
|
||||
```
|
||||
|
||||
Alternatively, pass `--skip-daemon-install` to individual commands:
|
||||
|
||||
```bash
|
||||
npx denchclaw --skip-daemon-install
|
||||
npx denchclaw update --skip-daemon-install
|
||||
npx denchclaw start --skip-daemon-install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "denchclaw",
|
||||
"version": "2.3.4",
|
||||
"version": "2.3.5",
|
||||
"description": "Fully Managed OpenClaw Framework for managing your CRM, Sales Automation and Outreach agents. The only local productivity tool you need.",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/DenchHQ/DenchClaw#readme",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dench",
|
||||
"version": "2.3.4",
|
||||
"version": "2.3.5",
|
||||
"description": "Shorthand alias for denchclaw — AI-powered CRM platform CLI",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@ -16,7 +16,7 @@
|
||||
],
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"denchclaw": "^2.3.4"
|
||||
"denchclaw": "^2.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@ -13,6 +13,7 @@ import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { confirm, isCancel, select, spinner, text } from "@clack/prompts";
|
||||
import json5 from "json5";
|
||||
import { isDaemonlessMode } from "../config/paths.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { readTelemetryConfig, markNoticeShown } from "../telemetry/config.js";
|
||||
@ -95,6 +96,7 @@ export type BootstrapOptions = {
|
||||
denchCloudApiKey?: string;
|
||||
denchCloudModel?: string;
|
||||
denchGatewayUrl?: string;
|
||||
skipDaemonInstall?: boolean;
|
||||
};
|
||||
|
||||
type BootstrapSummary = {
|
||||
@ -2148,6 +2150,7 @@ export async function bootstrapCommand(
|
||||
runtime.log(theme.warn(appliedProfile.warning));
|
||||
}
|
||||
|
||||
const daemonless = isDaemonlessMode(opts);
|
||||
const bootstrapStartTime = Date.now();
|
||||
|
||||
if (!opts.json) {
|
||||
@ -2325,7 +2328,7 @@ export async function bootstrapCommand(
|
||||
"--profile",
|
||||
profile,
|
||||
"onboard",
|
||||
"--install-daemon",
|
||||
...(daemonless ? [] : ["--install-daemon"]),
|
||||
"--gateway-bind",
|
||||
"loopback",
|
||||
"--gateway-port",
|
||||
@ -2342,6 +2345,9 @@ export async function bootstrapCommand(
|
||||
}
|
||||
|
||||
onboardArgv.push("--accept-risk", "--skip-ui");
|
||||
if (daemonless) {
|
||||
onboardArgv.push("--skip-health");
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
await runOpenClawOrThrow({
|
||||
@ -2410,55 +2416,62 @@ export async function bootstrapCommand(
|
||||
postOnboardSpinner?.message("Configuring subagent defaults…");
|
||||
await ensureSubagentDefaults(openclawCommand, profile);
|
||||
|
||||
// ── Single post-config gateway restart ──
|
||||
// All Dench-owned config has been applied. Restart the gateway once so the
|
||||
// daemon picks up plugin, model, and subagent changes that were written after
|
||||
// onboard started it. No helper above triggers its own restart.
|
||||
postOnboardSpinner?.message("Restarting gateway…");
|
||||
try {
|
||||
await runOpenClawOrThrow({
|
||||
openclawCommand,
|
||||
args: ["--profile", profile, "gateway", "restart"],
|
||||
timeoutMs: 60_000,
|
||||
errorMessage: "Failed to restart gateway after config update.",
|
||||
});
|
||||
} catch {
|
||||
// Gateway may not be running (e.g. onboard daemon install failed on this
|
||||
// platform). The final readiness check below will catch this.
|
||||
}
|
||||
|
||||
// ── Final readiness verification ──
|
||||
// Give the gateway time to finish starting after the restart, then verify
|
||||
// readiness. The probe retries here replace the old pattern of probing
|
||||
// immediately (which raced gateway startup) and jumping straight into a
|
||||
// destructive stop/install/start auto-fix cycle.
|
||||
postOnboardSpinner?.message("Waiting for gateway…");
|
||||
let gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort);
|
||||
for (let attempt = 0; attempt < 4 && !gatewayProbe.ok; attempt += 1) {
|
||||
await sleep(750);
|
||||
postOnboardSpinner?.message(`Probing gateway health (attempt ${attempt + 2}/5)…`);
|
||||
gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort);
|
||||
}
|
||||
|
||||
// Repair is failure-only: only invoked when the retried final verification
|
||||
// still reports the gateway as unreachable.
|
||||
// ── Gateway daemon restart + readiness verification ──
|
||||
// Skipped entirely in daemonless mode — the user manages the gateway process
|
||||
// externally (e.g. `openclaw gateway --port <port>` as a foreground process).
|
||||
let gatewayProbe: { ok: boolean; detail?: string };
|
||||
let gatewayAutoFix: GatewayAutoFixResult | undefined;
|
||||
if (!gatewayProbe.ok) {
|
||||
postOnboardSpinner?.message("Gateway unreachable, attempting auto-fix…");
|
||||
gatewayAutoFix = await attemptGatewayAutoFix({
|
||||
openclawCommand,
|
||||
profile,
|
||||
stateDir,
|
||||
gatewayPort,
|
||||
});
|
||||
gatewayProbe = gatewayAutoFix.finalProbe;
|
||||
if (!gatewayProbe.ok && gatewayAutoFix.failureSummary) {
|
||||
gatewayProbe = {
|
||||
...gatewayProbe,
|
||||
detail: [gatewayProbe.detail, gatewayAutoFix.failureSummary]
|
||||
.filter((value, index, self) => value && self.indexOf(value) === index)
|
||||
.join(" | "),
|
||||
};
|
||||
|
||||
if (daemonless) {
|
||||
gatewayProbe = { ok: true, detail: "skipped (daemonless)" };
|
||||
} else {
|
||||
// All Dench-owned config has been applied. Restart the gateway once so the
|
||||
// daemon picks up plugin, model, and subagent changes that were written after
|
||||
// onboard started it. No helper above triggers its own restart.
|
||||
postOnboardSpinner?.message("Restarting gateway…");
|
||||
try {
|
||||
await runOpenClawOrThrow({
|
||||
openclawCommand,
|
||||
args: ["--profile", profile, "gateway", "restart"],
|
||||
timeoutMs: 60_000,
|
||||
errorMessage: "Failed to restart gateway after config update.",
|
||||
});
|
||||
} catch {
|
||||
// Gateway may not be running (e.g. onboard daemon install failed on this
|
||||
// platform). The final readiness check below will catch this.
|
||||
}
|
||||
|
||||
// Give the gateway time to finish starting after the restart, then verify
|
||||
// readiness. The probe retries here replace the old pattern of probing
|
||||
// immediately (which raced gateway startup) and jumping straight into a
|
||||
// destructive stop/install/start auto-fix cycle.
|
||||
postOnboardSpinner?.message("Waiting for gateway…");
|
||||
gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort);
|
||||
for (let attempt = 0; attempt < 4 && !gatewayProbe.ok; attempt += 1) {
|
||||
await sleep(750);
|
||||
postOnboardSpinner?.message(`Probing gateway health (attempt ${attempt + 2}/5)…`);
|
||||
gatewayProbe = await probeGateway(openclawCommand, profile, gatewayPort);
|
||||
}
|
||||
|
||||
// Repair is failure-only: only invoked when the retried final verification
|
||||
// still reports the gateway as unreachable.
|
||||
if (!gatewayProbe.ok) {
|
||||
postOnboardSpinner?.message("Gateway unreachable, attempting auto-fix…");
|
||||
gatewayAutoFix = await attemptGatewayAutoFix({
|
||||
openclawCommand,
|
||||
profile,
|
||||
stateDir,
|
||||
gatewayPort,
|
||||
});
|
||||
gatewayProbe = gatewayAutoFix.finalProbe;
|
||||
if (!gatewayProbe.ok && gatewayAutoFix.failureSummary) {
|
||||
gatewayProbe = {
|
||||
...gatewayProbe,
|
||||
detail: [gatewayProbe.detail, gatewayAutoFix.failureSummary]
|
||||
.filter((value, index, self) => value && self.indexOf(value) === index)
|
||||
.join(" | "),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const gatewayUrl = `ws://127.0.0.1:${gatewayPort}`;
|
||||
|
||||
@ -21,6 +21,7 @@ export function registerBootstrapCommand(program: Command) {
|
||||
.option("--dench-cloud-api-key <key>", "Dench Cloud API key for bootstrap-driven setup")
|
||||
.option("--dench-cloud-model <id>", "Stable or public Dench Cloud model id to use as default")
|
||||
.option("--dench-gateway-url <url>", "Override the Dench Cloud gateway base URL")
|
||||
.option("--skip-daemon-install", "Skip gateway daemon/service installation (for containers or environments without systemd/launchd)", false)
|
||||
.option("--no-open", "Do not open the browser automatically")
|
||||
.option("--json", "Output summary as JSON", false)
|
||||
.addHelpText(
|
||||
@ -43,6 +44,7 @@ export function registerBootstrapCommand(program: Command) {
|
||||
denchCloudApiKey: opts.denchCloudApiKey as string | undefined,
|
||||
denchCloudModel: opts.denchCloudModel as string | undefined,
|
||||
denchGatewayUrl: opts.denchGatewayUrl as string | undefined,
|
||||
skipDaemonInstall: Boolean(opts.skipDaemonInstall),
|
||||
noOpen: Boolean(opts.open === false),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ export function registerRestartCommand(program: Command) {
|
||||
.option("--profile <name>", "Compatibility flag; non-dench values are ignored with a warning")
|
||||
.option("--web-port <port>", "Web runtime port override")
|
||||
.option("--no-open", "Do not open the browser automatically")
|
||||
.option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false)
|
||||
.option("--json", "Output summary as JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
@ -17,6 +18,7 @@ export function registerRestartCommand(program: Command) {
|
||||
profile: opts.profile as string | undefined,
|
||||
webPort: opts.webPort as string | undefined,
|
||||
noOpen: Boolean(opts.open === false),
|
||||
skipDaemonInstall: Boolean(opts.skipDaemonInstall),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ export function registerStartCommand(program: Command) {
|
||||
.option("--profile <name>", "Compatibility flag; non-dench values are ignored with a warning")
|
||||
.option("--web-port <port>", "Web runtime port override")
|
||||
.option("--no-open", "Do not open the browser automatically")
|
||||
.option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false)
|
||||
.option("--json", "Output summary as JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
@ -17,6 +18,7 @@ export function registerStartCommand(program: Command) {
|
||||
profile: opts.profile as string | undefined,
|
||||
webPort: opts.webPort as string | undefined,
|
||||
noOpen: Boolean(opts.open === false),
|
||||
skipDaemonInstall: Boolean(opts.skipDaemonInstall),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,12 +9,14 @@ export function registerStopCommand(program: Command) {
|
||||
.description("Stop Dench managed web runtime on the configured port")
|
||||
.option("--profile <name>", "Compatibility flag; non-dench values are ignored with a warning")
|
||||
.option("--web-port <port>", "Web runtime port override")
|
||||
.option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false)
|
||||
.option("--json", "Output summary as JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
await stopWebRuntimeCommand({
|
||||
profile: opts.profile as string | undefined,
|
||||
webPort: opts.webPort as string | undefined,
|
||||
skipDaemonInstall: Boolean(opts.skipDaemonInstall),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ export function registerUpdateCommand(program: Command) {
|
||||
.option("--non-interactive", "Fail instead of prompting for major-gate approval", false)
|
||||
.option("--yes", "Approve mandatory major-gate OpenClaw update", false)
|
||||
.option("--no-open", "Do not open the browser automatically")
|
||||
.option("--skip-daemon-install", "Skip gateway daemon/service management (for containers or environments without systemd/launchd)", false)
|
||||
.option("--json", "Output summary as JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runCommandWithRuntime(defaultRuntime, async () => {
|
||||
@ -21,6 +22,7 @@ export function registerUpdateCommand(program: Command) {
|
||||
nonInteractive: Boolean(opts.nonInteractive),
|
||||
yes: Boolean(opts.yes),
|
||||
noOpen: Boolean(opts.open === false),
|
||||
skipDaemonInstall: Boolean(opts.skipDaemonInstall),
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
});
|
||||
|
||||
@ -273,6 +273,22 @@ describe("updateWebRuntimeCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips gateway daemon restart and LaunchAgent in daemonless mode", async () => {
|
||||
webRuntimeMocks.runOpenClawCommand.mockClear();
|
||||
launchdMocks.uninstallWebRuntimeLaunchAgent.mockClear();
|
||||
const runtime = runtimeStub();
|
||||
const summary = await updateWebRuntimeCommand(
|
||||
{ nonInteractive: true, skipDaemonInstall: true },
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(webRuntimeMocks.runOpenClawCommand).not.toHaveBeenCalled();
|
||||
expect(launchdMocks.uninstallWebRuntimeLaunchAgent).not.toHaveBeenCalled();
|
||||
expect(summary.gatewayRestarted).toBe(false);
|
||||
expect(summary.gatewayError).toBeUndefined();
|
||||
expect(summary.ready).toBe(true);
|
||||
});
|
||||
|
||||
it("skips OpenClaw update on minor upgrades while still refreshing runtime (avoids unnecessary blocking)", async () => {
|
||||
webRuntimeMocks.evaluateMajorVersionTransition.mockReturnValue({
|
||||
previousMajor: 2,
|
||||
@ -410,6 +426,30 @@ describe("startWebRuntimeCommand", () => {
|
||||
expect(summary.started).toBe(true);
|
||||
});
|
||||
|
||||
it("skips gateway daemon restart and LaunchAgent in daemonless mode, uses child process", async () => {
|
||||
webRuntimeMocks.runOpenClawCommand.mockClear();
|
||||
launchdMocks.installWebRuntimeLaunchAgent.mockClear();
|
||||
launchdMocks.uninstallWebRuntimeLaunchAgent.mockClear();
|
||||
webRuntimeMocks.startManagedWebRuntime.mockClear();
|
||||
const runtime = runtimeStub();
|
||||
const summary = await startWebRuntimeCommand(
|
||||
{ webPort: "3100", skipDaemonInstall: true },
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(webRuntimeMocks.runOpenClawCommand).not.toHaveBeenCalled();
|
||||
expect(launchdMocks.installWebRuntimeLaunchAgent).not.toHaveBeenCalled();
|
||||
expect(launchdMocks.uninstallWebRuntimeLaunchAgent).not.toHaveBeenCalled();
|
||||
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({
|
||||
stateDir: "/tmp/.openclaw-dench",
|
||||
port: 3100,
|
||||
gatewayPort: 19001,
|
||||
});
|
||||
expect(summary.started).toBe(true);
|
||||
expect(summary.gatewayRestarted).toBe(false);
|
||||
expect(summary.gatewayError).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to DenchClaw port 19001 when manifest has no lastGatewayPort (prevents 18789 hijack)", async () => {
|
||||
webRuntimeMocks.readManagedWebRuntimeManifest.mockReturnValue({
|
||||
schemaVersion: 1,
|
||||
|
||||
@ -6,7 +6,7 @@ import { confirm, isCancel, spinner } from "@clack/prompts";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { stylePromptMessage } from "../terminal/prompt-style.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { DENCHCLAW_DEFAULT_GATEWAY_PORT } from "../config/paths.js";
|
||||
import { DENCHCLAW_DEFAULT_GATEWAY_PORT, isDaemonlessMode } from "../config/paths.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { applyCliProfileEnv } from "./profile.js";
|
||||
import {
|
||||
@ -43,12 +43,14 @@ export type UpdateWebRuntimeOptions = {
|
||||
yes?: boolean;
|
||||
noOpen?: boolean;
|
||||
json?: boolean;
|
||||
skipDaemonInstall?: boolean;
|
||||
};
|
||||
|
||||
export type StopWebRuntimeOptions = {
|
||||
profile?: string;
|
||||
webPort?: string | number;
|
||||
json?: boolean;
|
||||
skipDaemonInstall?: boolean;
|
||||
};
|
||||
|
||||
export type StartWebRuntimeOptions = {
|
||||
@ -56,6 +58,7 @@ export type StartWebRuntimeOptions = {
|
||||
webPort?: string | number;
|
||||
noOpen?: boolean;
|
||||
json?: boolean;
|
||||
skipDaemonInstall?: boolean;
|
||||
};
|
||||
|
||||
export type UpdateWebRuntimeSummary = {
|
||||
@ -397,6 +400,7 @@ export async function updateWebRuntimeCommand(
|
||||
await runOpenClawUpdateWithProgress(openclawCommand);
|
||||
}
|
||||
|
||||
const daemonless = isDaemonlessMode(opts);
|
||||
const selectedPort =
|
||||
parseOptionalPort(opts.webPort) ??
|
||||
parseOptionalPort(previousManifest?.lastPort) ??
|
||||
@ -404,7 +408,7 @@ export async function updateWebRuntimeCommand(
|
||||
DEFAULT_WEB_APP_PORT;
|
||||
const gatewayPort = resolveGatewayPort(stateDir);
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
if (!daemonless && process.platform === "darwin") {
|
||||
uninstallWebRuntimeLaunchAgent();
|
||||
}
|
||||
|
||||
@ -414,11 +418,9 @@ export async function updateWebRuntimeCommand(
|
||||
includeLegacyStandalone: true,
|
||||
});
|
||||
|
||||
const gatewayResult = await restartGatewayDaemon({
|
||||
profile,
|
||||
gatewayPort,
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
const gatewayResult: { restarted: boolean; error?: string } = daemonless
|
||||
? { restarted: false, error: "skipped (daemonless)" }
|
||||
: await restartGatewayDaemon({ profile, gatewayPort, json: Boolean(opts.json) });
|
||||
|
||||
const workspaceDirs = discoverWorkspaceDirs(stateDir);
|
||||
const skillSyncResult = syncManagedSkills({ workspaceDirs, packageRoot });
|
||||
@ -430,7 +432,7 @@ export async function updateWebRuntimeCommand(
|
||||
port: selectedPort,
|
||||
gatewayPort,
|
||||
startFn:
|
||||
process.platform === "darwin"
|
||||
!daemonless && process.platform === "darwin"
|
||||
? (p) => installWebRuntimeLaunchAgent(p)
|
||||
: undefined,
|
||||
});
|
||||
@ -449,7 +451,7 @@ export async function updateWebRuntimeCommand(
|
||||
ready: ensureResult.ready,
|
||||
reason: ensureResult.reason,
|
||||
gatewayRestarted: gatewayResult.restarted,
|
||||
gatewayError: gatewayResult.error,
|
||||
gatewayError: daemonless ? undefined : gatewayResult.error,
|
||||
skillSync: skillSyncResult,
|
||||
};
|
||||
|
||||
@ -459,7 +461,11 @@ export async function updateWebRuntimeCommand(
|
||||
runtime.log(`Profile: ${profile}`);
|
||||
runtime.log(`Version: ${VERSION}`);
|
||||
runtime.log(`Web port: ${selectedPort}`);
|
||||
runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`);
|
||||
if (daemonless) {
|
||||
runtime.log(`Gateway: skipped (daemonless mode)`);
|
||||
} else {
|
||||
runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`);
|
||||
}
|
||||
if (summary.gatewayError) {
|
||||
runtime.log(theme.warn(`Gateway error: ${summary.gatewayError}`));
|
||||
}
|
||||
@ -508,7 +514,7 @@ export async function stopWebRuntimeCommand(
|
||||
const stateDir = resolveProfileStateDir(profile);
|
||||
const selectedPort = parseOptionalPort(opts.webPort) ?? readLastKnownWebPort(stateDir);
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
if (!isDaemonlessMode(opts) && process.platform === "darwin") {
|
||||
uninstallWebRuntimeLaunchAgent();
|
||||
}
|
||||
|
||||
@ -570,11 +576,12 @@ export async function startWebRuntimeCommand(
|
||||
runtime.log(theme.warn(appliedProfile.warning));
|
||||
}
|
||||
|
||||
const daemonless = isDaemonlessMode(opts);
|
||||
const stateDir = resolveProfileStateDir(profile);
|
||||
const selectedPort = parseOptionalPort(opts.webPort) ?? readLastKnownWebPort(stateDir);
|
||||
const gatewayPort = resolveGatewayPort(stateDir);
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
if (!daemonless && process.platform === "darwin") {
|
||||
uninstallWebRuntimeLaunchAgent();
|
||||
}
|
||||
|
||||
@ -590,14 +597,12 @@ export async function startWebRuntimeCommand(
|
||||
);
|
||||
}
|
||||
|
||||
const gatewayResult = await restartGatewayDaemon({
|
||||
profile,
|
||||
gatewayPort,
|
||||
json: Boolean(opts.json),
|
||||
});
|
||||
const gatewayResult: { restarted: boolean; error?: string } = daemonless
|
||||
? { restarted: false, error: "skipped (daemonless)" }
|
||||
: await restartGatewayDaemon({ profile, gatewayPort, json: Boolean(opts.json) });
|
||||
|
||||
let startResult;
|
||||
if (process.platform === "darwin") {
|
||||
if (!daemonless && process.platform === "darwin") {
|
||||
startResult = installWebRuntimeLaunchAgent({ stateDir, port: selectedPort, gatewayPort });
|
||||
if (!startResult.started && startResult.reason !== "runtime-missing") {
|
||||
startResult = startManagedWebRuntime({ stateDir, port: selectedPort, gatewayPort });
|
||||
@ -625,7 +630,7 @@ export async function startWebRuntimeCommand(
|
||||
started: probe.ok,
|
||||
reason: probe.reason,
|
||||
gatewayRestarted: gatewayResult.restarted,
|
||||
gatewayError: gatewayResult.error,
|
||||
gatewayError: daemonless ? undefined : gatewayResult.error,
|
||||
};
|
||||
|
||||
if (opts.json) {
|
||||
@ -637,7 +642,11 @@ export async function startWebRuntimeCommand(
|
||||
runtime.log(theme.heading(`Dench web ${label}`));
|
||||
runtime.log(`Profile: ${profile}`);
|
||||
runtime.log(`Web port: ${selectedPort}`);
|
||||
runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`);
|
||||
if (daemonless) {
|
||||
runtime.log(`Gateway: skipped (daemonless mode)`);
|
||||
} else {
|
||||
runtime.log(`Gateway: ${summary.gatewayRestarted ? "restarted" : "restart failed"}`);
|
||||
}
|
||||
if (summary.gatewayError) {
|
||||
runtime.log(theme.warn(`Gateway error: ${summary.gatewayError}`));
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
isDaemonlessMode,
|
||||
resolveGatewayPort,
|
||||
DEFAULT_GATEWAY_PORT,
|
||||
DENCHCLAW_DEFAULT_GATEWAY_PORT,
|
||||
@ -85,6 +86,32 @@ describe("resolveGatewayPort", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDaemonlessMode", () => {
|
||||
it("returns false when no opt and no env var", () => {
|
||||
expect(isDaemonlessMode(undefined, {})).toBe(false);
|
||||
expect(isDaemonlessMode({}, {})).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when opts.skipDaemonInstall is set", () => {
|
||||
expect(isDaemonlessMode({ skipDaemonInstall: true }, {})).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when DENCHCLAW_DAEMONLESS=1 env var is set", () => {
|
||||
expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "1" })).toBe(true);
|
||||
expect(isDaemonlessMode({}, { DENCHCLAW_DAEMONLESS: "1" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for non-'1' env values", () => {
|
||||
expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "true" })).toBe(false);
|
||||
expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "yes" })).toBe(false);
|
||||
expect(isDaemonlessMode(undefined, { DENCHCLAW_DAEMONLESS: "0" })).toBe(false);
|
||||
});
|
||||
|
||||
it("opts.skipDaemonInstall=true wins even without env var", () => {
|
||||
expect(isDaemonlessMode({ skipDaemonInstall: true }, { DENCHCLAW_DAEMONLESS: "0" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("port constants", () => {
|
||||
it("DenchClaw default port is distinct from OpenClaw default (prevents port collision)", () => {
|
||||
expect(DENCHCLAW_DEFAULT_GATEWAY_PORT).not.toBe(DEFAULT_GATEWAY_PORT);
|
||||
|
||||
@ -22,6 +22,13 @@ export function resolveIsNixMode(env: NodeJS.ProcessEnv = process.env): boolean
|
||||
|
||||
export const isNixMode = resolveIsNixMode();
|
||||
|
||||
export function isDaemonlessMode(
|
||||
opts?: { skipDaemonInstall?: boolean },
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
return Boolean(opts?.skipDaemonInstall) || env.DENCHCLAW_DAEMONLESS === "1";
|
||||
}
|
||||
|
||||
// Support historical (and occasionally misspelled) legacy state dirs.
|
||||
const LEGACY_STATE_DIRNAMES = [".clawdbot", ".moldbot", ".moltbot"] as const;
|
||||
const NEW_STATE_DIRNAME = ".openclaw-dench";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user