feat: add json output for daemon lifecycle
This commit is contained in:
@@ -209,6 +209,28 @@ describe("daemon-cli coverage", () => {
|
||||
expect(serviceInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("installs the daemon with json output", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceIsLoaded.mockResolvedValueOnce(false);
|
||||
serviceInstall.mockClear();
|
||||
|
||||
const { registerDaemonCli } = await import("./daemon-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerDaemonCli(program);
|
||||
|
||||
await program.parseAsync(["daemon", "install", "--port", "18789", "--json"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
|
||||
const parsed = JSON.parse(jsonLine ?? "{}") as { ok?: boolean; action?: string; result?: string };
|
||||
expect(parsed.ok).toBe(true);
|
||||
expect(parsed.action).toBe("install");
|
||||
expect(parsed.result).toBe("installed");
|
||||
});
|
||||
|
||||
it("starts and stops the daemon via service helpers", async () => {
|
||||
serviceRestart.mockClear();
|
||||
serviceStop.mockClear();
|
||||
@@ -225,4 +247,25 @@ describe("daemon-cli coverage", () => {
|
||||
expect(serviceRestart).toHaveBeenCalledTimes(1);
|
||||
expect(serviceStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("emits json for daemon start/stop", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceRestart.mockClear();
|
||||
serviceStop.mockClear();
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
|
||||
const { registerDaemonCli } = await import("./daemon-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerDaemonCli(program);
|
||||
|
||||
await program.parseAsync(["daemon", "start", "--json"], { from: "user" });
|
||||
await program.parseAsync(["daemon", "stop", "--json"], { from: "user" });
|
||||
|
||||
const jsonLines = runtimeLogs.filter((line) => line.trim().startsWith("{"));
|
||||
const parsed = jsonLines.map((line) => JSON.parse(line) as { action?: string; ok?: boolean });
|
||||
expect(parsed.some((entry) => entry.action === "start" && entry.ok === true)).toBe(true);
|
||||
expect(parsed.some((entry) => entry.action === "stop" && entry.ok === true)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,33 +15,59 @@ import {
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { buildServiceEnvironment } from "../../daemon/service-env.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
|
||||
import { parsePort } from "./shared.js";
|
||||
import type { DaemonInstallOptions } from "./types.js";
|
||||
|
||||
export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
if (resolveIsNixMode(process.env)) {
|
||||
defaultRuntime.error("Nix mode detected; daemon install is disabled.");
|
||||
const json = Boolean(opts.json);
|
||||
const warnings: string[] = [];
|
||||
const stdout = json ? createNullWriter() : process.stdout;
|
||||
const emit = (payload: {
|
||||
ok: boolean;
|
||||
result?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
service?: {
|
||||
label: string;
|
||||
loaded: boolean;
|
||||
loadedText: string;
|
||||
notLoadedText: string;
|
||||
};
|
||||
hints?: string[];
|
||||
warnings?: string[];
|
||||
}) => {
|
||||
if (!json) return;
|
||||
emitDaemonActionJson({ action: "install", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) {
|
||||
emit({ ok: false, error: message, warnings: warnings.length ? warnings : undefined });
|
||||
} else {
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
if (resolveIsNixMode(process.env)) {
|
||||
fail("Nix mode detected; daemon install is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const portOverride = parsePort(opts.port);
|
||||
if (opts.port !== undefined && portOverride === null) {
|
||||
defaultRuntime.error("Invalid port");
|
||||
defaultRuntime.exit(1);
|
||||
fail("Invalid port");
|
||||
return;
|
||||
}
|
||||
const port = portOverride ?? resolveGatewayPort(cfg);
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
defaultRuntime.error("Invalid port");
|
||||
defaultRuntime.exit(1);
|
||||
fail("Invalid port");
|
||||
return;
|
||||
}
|
||||
const runtimeRaw = opts.runtime ? String(opts.runtime) : DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
if (!isGatewayDaemonRuntime(runtimeRaw)) {
|
||||
defaultRuntime.error('Invalid --runtime (use "node" or "bun")');
|
||||
defaultRuntime.exit(1);
|
||||
fail('Invalid --runtime (use "node" or "bun")');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,14 +76,22 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway service check failed: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
if (loaded) {
|
||||
if (!opts.force) {
|
||||
defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
|
||||
defaultRuntime.log("Reinstall with: clawdbot daemon install --force");
|
||||
emit({
|
||||
ok: true,
|
||||
result: "already-installed",
|
||||
message: `Gateway service already ${service.loadedText}.`,
|
||||
service: buildDaemonServiceSnapshot(service, loaded),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
|
||||
defaultRuntime.log("Reinstall with: clawdbot daemon install --force");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +111,10 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
if (runtimeRaw === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
|
||||
if (warning) defaultRuntime.log(warning);
|
||||
if (warning) {
|
||||
if (json) warnings.push(warning);
|
||||
else defaultRuntime.log(warning);
|
||||
}
|
||||
}
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
@@ -92,13 +129,26 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
try {
|
||||
await service.install({
|
||||
env: process.env,
|
||||
stdout: process.stdout,
|
||||
stdout,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
});
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway install failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway install failed: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let installed = true;
|
||||
try {
|
||||
installed = await service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
installed = true;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "installed",
|
||||
service: buildDaemonServiceSnapshot(service, installed),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,72 +1,193 @@
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
|
||||
import { renderGatewayServiceStartHints } from "./shared.js";
|
||||
import type { DaemonLifecycleOptions } from "./types.js";
|
||||
|
||||
export async function runDaemonUninstall(opts: DaemonLifecycleOptions = {}) {
|
||||
const json = Boolean(opts.json);
|
||||
const stdout = json ? createNullWriter() : process.stdout;
|
||||
const emit = (payload: {
|
||||
ok: boolean;
|
||||
result?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
service?: {
|
||||
label: string;
|
||||
loaded: boolean;
|
||||
loadedText: string;
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
emitDaemonActionJson({ action: "uninstall", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) emit({ ok: false, error: message });
|
||||
else defaultRuntime.error(message);
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
export async function runDaemonUninstall() {
|
||||
if (resolveIsNixMode(process.env)) {
|
||||
defaultRuntime.error("Nix mode detected; daemon uninstall is disabled.");
|
||||
defaultRuntime.exit(1);
|
||||
fail("Nix mode detected; daemon uninstall is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const service = resolveGatewayService();
|
||||
try {
|
||||
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||
await service.uninstall({ env: process.env, stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway uninstall failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway uninstall failed: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
loaded = false;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "uninstalled",
|
||||
service: buildDaemonServiceSnapshot(service, loaded),
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDaemonStart() {
|
||||
export async function runDaemonStart(opts: DaemonLifecycleOptions = {}) {
|
||||
const json = Boolean(opts.json);
|
||||
const stdout = json ? createNullWriter() : process.stdout;
|
||||
const emit = (payload: {
|
||||
ok: boolean;
|
||||
result?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
hints?: string[];
|
||||
service?: {
|
||||
label: string;
|
||||
loaded: boolean;
|
||||
loadedText: string;
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
emitDaemonActionJson({ action: "start", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (json) emit({ ok: false, error: message, hints });
|
||||
else defaultRuntime.error(message);
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway service check failed: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
const hints = renderGatewayServiceStartHints();
|
||||
emit({
|
||||
ok: true,
|
||||
result: "not-loaded",
|
||||
message: `Gateway service ${service.notLoadedText}.`,
|
||||
hints,
|
||||
service: buildDaemonServiceSnapshot(service, loaded),
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of hints) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.restart({ env: process.env, stdout: process.stdout });
|
||||
await service.restart({ env: process.env, stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway start failed: ${String(err)}`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
defaultRuntime.error(`Start with: ${hint}`);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
const hints = renderGatewayServiceStartHints();
|
||||
fail(`Gateway start failed: ${String(err)}`, hints);
|
||||
return;
|
||||
}
|
||||
|
||||
let started = true;
|
||||
try {
|
||||
started = await service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
started = true;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "started",
|
||||
service: buildDaemonServiceSnapshot(service, started),
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDaemonStop() {
|
||||
export async function runDaemonStop(opts: DaemonLifecycleOptions = {}) {
|
||||
const json = Boolean(opts.json);
|
||||
const stdout = json ? createNullWriter() : process.stdout;
|
||||
const emit = (payload: {
|
||||
ok: boolean;
|
||||
result?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
service?: {
|
||||
label: string;
|
||||
loaded: boolean;
|
||||
loadedText: string;
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
emitDaemonActionJson({ action: "stop", ...payload });
|
||||
};
|
||||
const fail = (message: string) => {
|
||||
if (json) emit({ ok: false, error: message });
|
||||
else defaultRuntime.error(message);
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway service check failed: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
emit({
|
||||
ok: true,
|
||||
result: "not-loaded",
|
||||
message: `Gateway service ${service.notLoadedText}.`,
|
||||
service: buildDaemonServiceSnapshot(service, loaded),
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.stop({ env: process.env, stdout: process.stdout });
|
||||
await service.stop({ env: process.env, stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway stop failed: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let stopped = false;
|
||||
try {
|
||||
stopped = await service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
stopped = false;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "stopped",
|
||||
service: buildDaemonServiceSnapshot(service, stopped),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,29 +195,73 @@ export async function runDaemonStop() {
|
||||
* @returns `true` if restart succeeded, `false` if the service was not loaded.
|
||||
* Throws/exits on check or restart failures.
|
||||
*/
|
||||
export async function runDaemonRestart(): Promise<boolean> {
|
||||
export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promise<boolean> {
|
||||
const json = Boolean(opts.json);
|
||||
const stdout = json ? createNullWriter() : process.stdout;
|
||||
const emit = (payload: {
|
||||
ok: boolean;
|
||||
result?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
hints?: string[];
|
||||
service?: {
|
||||
label: string;
|
||||
loaded: boolean;
|
||||
loadedText: string;
|
||||
notLoadedText: string;
|
||||
};
|
||||
}) => {
|
||||
if (!json) return;
|
||||
emitDaemonActionJson({ action: "restart", ...payload });
|
||||
};
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
if (json) emit({ ok: false, error: message, hints });
|
||||
else defaultRuntime.error(message);
|
||||
defaultRuntime.exit(1);
|
||||
};
|
||||
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
fail(`Gateway service check failed: ${String(err)}`);
|
||||
return false;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
const hints = renderGatewayServiceStartHints();
|
||||
emit({
|
||||
ok: true,
|
||||
result: "not-loaded",
|
||||
message: `Gateway service ${service.notLoadedText}.`,
|
||||
hints,
|
||||
service: buildDaemonServiceSnapshot(service, loaded),
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of hints) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await service.restart({ env: process.env, stdout: process.stdout });
|
||||
await service.restart({ env: process.env, stdout });
|
||||
let restarted = true;
|
||||
try {
|
||||
restarted = await service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
restarted = true;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "restarted",
|
||||
service: buildDaemonServiceSnapshot(service, restarted),
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
const hints = renderGatewayServiceStartHints();
|
||||
fail(`Gateway restart failed: ${String(err)}`, hints);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export function registerDaemonCli(program: Command) {
|
||||
.option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node")
|
||||
.option("--token <token>", "Gateway token (token auth)")
|
||||
.option("--force", "Reinstall/overwrite if already installed", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonInstall(opts);
|
||||
});
|
||||
@@ -54,29 +55,33 @@ export function registerDaemonCli(program: Command) {
|
||||
daemon
|
||||
.command("uninstall")
|
||||
.description("Uninstall the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await runDaemonUninstall();
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonUninstall(opts);
|
||||
});
|
||||
|
||||
daemon
|
||||
.command("start")
|
||||
.description("Start the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await runDaemonStart();
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonStart(opts);
|
||||
});
|
||||
|
||||
daemon
|
||||
.command("stop")
|
||||
.description("Stop the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await runDaemonStop();
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonStop(opts);
|
||||
});
|
||||
|
||||
daemon
|
||||
.command("restart")
|
||||
.description("Restart the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await runDaemonRestart();
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
await runDaemonRestart(opts);
|
||||
});
|
||||
|
||||
// Build default deps (parity with other commands).
|
||||
|
||||
43
src/cli/daemon-cli/response.ts
Normal file
43
src/cli/daemon-cli/response.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Writable } from "node:stream";
|
||||
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
|
||||
export type DaemonAction = "install" | "uninstall" | "start" | "stop" | "restart";
|
||||
|
||||
export type DaemonActionResponse = {
|
||||
ok: boolean;
|
||||
action: DaemonAction;
|
||||
result?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
hints?: string[];
|
||||
warnings?: string[];
|
||||
service?: {
|
||||
label: string;
|
||||
loaded: boolean;
|
||||
loadedText: string;
|
||||
notLoadedText: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function emitDaemonActionJson(payload: DaemonActionResponse) {
|
||||
defaultRuntime.log(JSON.stringify(payload, null, 2));
|
||||
}
|
||||
|
||||
export function buildDaemonServiceSnapshot(service: GatewayService, loaded: boolean) {
|
||||
return {
|
||||
label: service.label,
|
||||
loaded,
|
||||
loadedText: service.loadedText,
|
||||
notLoadedText: service.notLoadedText,
|
||||
};
|
||||
}
|
||||
|
||||
export function createNullWriter(): Writable {
|
||||
return new Writable({
|
||||
write(_chunk, _encoding, callback) {
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -19,4 +19,9 @@ export type DaemonInstallOptions = {
|
||||
runtime?: string;
|
||||
token?: string;
|
||||
force?: boolean;
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
export type DaemonLifecycleOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user