feat: add json output for daemon lifecycle

This commit is contained in:
Peter Steinberger
2026-01-16 05:40:35 +00:00
parent 41d44021e7
commit 2b8ce3f06b
14 changed files with 506 additions and 383 deletions

View File

@@ -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;
}
}