* Add custom headers to direct connections * Address custom header review feedback * Fix direct connection header integration * Fix desktop relay E2E transport setup --------- Co-authored-by: Claude <claude@Claudes-Mac-mini.local> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
595 lines
19 KiB
TypeScript
595 lines
19 KiB
TypeScript
import { type ChildProcess } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { app, ipcMain, powerMonitor } from "electron";
|
|
import log from "electron-log/main";
|
|
import { resolvePaseoHome, spawnProcess } from "@getpaseo/server";
|
|
import {
|
|
copyAttachmentFileToManagedStorage,
|
|
deleteManagedAttachmentFile,
|
|
garbageCollectManagedAttachmentFiles,
|
|
readManagedFileBase64,
|
|
writeAttachmentBase64,
|
|
writeAttachmentBytes,
|
|
} from "../features/attachments.js";
|
|
import {
|
|
checkForAppUpdate,
|
|
downloadAndInstallUpdate,
|
|
type AppUpdateCheckIntent,
|
|
type AppReleaseChannel,
|
|
} from "../features/auto-updater.js";
|
|
import {
|
|
getBundledCliShimPath,
|
|
getCliInstallStatus,
|
|
installCli,
|
|
} from "../integrations/cli-install/index.js";
|
|
import { createSkillsCommandHandlers, getSkillsController } from "../integrations/skills/index.js";
|
|
import {
|
|
openLocalTransportSession,
|
|
openWebSocketTransportSession,
|
|
sendLocalTransportMessage,
|
|
closeLocalTransportSession,
|
|
type WebSocketTransportTarget,
|
|
} from "./local-transport.js";
|
|
import { createNodeEntrypointInvocation, resolveDaemonRunnerEntrypoint } from "./runtime-paths.js";
|
|
import { runExternalCliJsonCommand, runExternalCliTextCommand } from "./cli/external.js";
|
|
import {
|
|
createDesktopSettingsCommandHandlers,
|
|
type DesktopCommandHandler,
|
|
} from "../settings/desktop-settings-commands.js";
|
|
import type { DesktopSettings } from "../settings/desktop-settings.js";
|
|
import { getDesktopSettingsStore } from "../settings/desktop-settings-electron.js";
|
|
import { isRunningUnderARM64Translation } from "../system/arm64-translation.js";
|
|
import { getDesktopAppLogs } from "../diagnostics/app-logs.js";
|
|
import { tailFile } from "../diagnostics/tail-file.js";
|
|
|
|
const DAEMON_LOG_FILENAME = "daemon.log";
|
|
const STARTUP_POLL_INTERVAL_MS = 200;
|
|
const STARTUP_POLL_MAX_ATTEMPTS = 150;
|
|
const DETACHED_STARTUP_GRACE_MS = 1200;
|
|
|
|
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
|
|
const DESKTOP_DAEMON_STOP_REASON_VALUES = [
|
|
"manual_ipc",
|
|
"settings",
|
|
"host_remove",
|
|
"quit",
|
|
"app_update",
|
|
"version_mismatch",
|
|
"restart",
|
|
] as const;
|
|
export type DesktopDaemonStopReason = (typeof DESKTOP_DAEMON_STOP_REASON_VALUES)[number];
|
|
|
|
const DESKTOP_DAEMON_STOP_REASONS = new Set<string>(DESKTOP_DAEMON_STOP_REASON_VALUES);
|
|
const DEFAULT_DESKTOP_DAEMON_STOP_REASON: DesktopDaemonStopReason = "manual_ipc";
|
|
|
|
export interface DesktopDaemonStatus {
|
|
serverId: string;
|
|
status: DesktopDaemonState;
|
|
listen: string | null;
|
|
hostname: string | null;
|
|
pid: number | null;
|
|
home: string;
|
|
version: string | null;
|
|
desktopManaged: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface DesktopDaemonLogs {
|
|
logPath: string;
|
|
contents: string;
|
|
}
|
|
|
|
function parseReleaseChannel(
|
|
args: Record<string, unknown> | undefined,
|
|
): AppReleaseChannel | undefined {
|
|
if (args?.releaseChannel === "beta") {
|
|
return "beta";
|
|
}
|
|
if (args?.releaseChannel === "stable") {
|
|
return "stable";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function parseAppUpdateCheckIntent(
|
|
args: Record<string, unknown> | undefined,
|
|
): AppUpdateCheckIntent {
|
|
return args?.intent === "manual" ? "manual" : "automatic";
|
|
}
|
|
|
|
function parseDesktopDaemonStopReason(
|
|
args: Record<string, unknown> | undefined,
|
|
): DesktopDaemonStopReason {
|
|
const reason = args?.reason;
|
|
if (typeof reason === "string" && DESKTOP_DAEMON_STOP_REASONS.has(reason)) {
|
|
return reason as DesktopDaemonStopReason;
|
|
}
|
|
return DEFAULT_DESKTOP_DAEMON_STOP_REASON;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Utilities
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function getPaseoHome(): string {
|
|
return resolvePaseoHome(process.env);
|
|
}
|
|
|
|
function logFilePath(): string {
|
|
return path.join(getPaseoHome(), DAEMON_LOG_FILENAME);
|
|
}
|
|
|
|
export function isDesktopManagedDaemonRunningSync(): boolean {
|
|
try {
|
|
const raw = readFileSync(path.join(getPaseoHome(), "paseo.pid"), "utf-8");
|
|
const lock = JSON.parse(raw) as { pid?: unknown; desktopManaged?: unknown };
|
|
if (lock.desktopManaged !== true) return false;
|
|
if (typeof lock.pid !== "number" || !Number.isInteger(lock.pid)) return false;
|
|
return isProcessRunning(lock.pid);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function summarizeDesktopDaemonStatus(status: DesktopDaemonStatus): Record<string, unknown> {
|
|
return {
|
|
status: status.status,
|
|
pid: status.pid,
|
|
listen: status.listen,
|
|
serverId: status.serverId || null,
|
|
version: status.version,
|
|
desktopManaged: status.desktopManaged,
|
|
error: status.error,
|
|
};
|
|
}
|
|
|
|
const DESKTOP_DAEMON_STOP_CLI_ARGS = [
|
|
"daemon",
|
|
"stop",
|
|
"--json",
|
|
"--timeout",
|
|
"5",
|
|
"--force",
|
|
"--kill-timeout",
|
|
"5",
|
|
];
|
|
|
|
async function runDesktopDaemonStopViaCli({
|
|
reason,
|
|
statusBefore,
|
|
resolveStatusAfter = false,
|
|
}: {
|
|
reason: DesktopDaemonStopReason;
|
|
statusBefore?: DesktopDaemonStatus | null;
|
|
resolveStatusAfter?: boolean;
|
|
}): Promise<{
|
|
cliResult: unknown;
|
|
statusAfter: DesktopDaemonStatus | null;
|
|
}> {
|
|
logDesktopDaemonLifecycle("desktop daemon stop requested", {
|
|
reason,
|
|
statusBefore: statusBefore ? summarizeDesktopDaemonStatus(statusBefore) : null,
|
|
});
|
|
|
|
const cliResult = await runExternalCliJsonCommand(DESKTOP_DAEMON_STOP_CLI_ARGS);
|
|
const statusAfter = resolveStatusAfter ? await resolveDesktopDaemonStatus() : null;
|
|
|
|
logDesktopDaemonLifecycle("desktop daemon stop completed", {
|
|
reason,
|
|
cliResult,
|
|
statusAfter: statusAfter ? summarizeDesktopDaemonStatus(statusAfter) : null,
|
|
});
|
|
|
|
return { cliResult, statusAfter };
|
|
}
|
|
|
|
export async function stopDesktopDaemonViaCli(
|
|
reason: DesktopDaemonStopReason = DEFAULT_DESKTOP_DAEMON_STOP_REASON,
|
|
): Promise<void> {
|
|
await runDesktopDaemonStopViaCli({ reason });
|
|
}
|
|
|
|
function isProcessRunning(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (err) {
|
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "EPERM") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
function logDesktopDaemonLifecycle(message: string, details?: Record<string, unknown>): void {
|
|
log.info("[desktop daemon]", message, {
|
|
pid: process.pid,
|
|
...details,
|
|
});
|
|
}
|
|
|
|
function resolveDesktopAppVersion(): string {
|
|
if (app.isPackaged) {
|
|
return app.getVersion();
|
|
}
|
|
|
|
try {
|
|
const packageJsonPath = path.join(__dirname, "..", "..", "package.json");
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|
version?: unknown;
|
|
};
|
|
if (typeof pkg.version === "string" && pkg.version.trim().length > 0) {
|
|
return pkg.version.trim();
|
|
}
|
|
} catch {
|
|
// Fall back to Electron's default version if the package metadata is unavailable.
|
|
}
|
|
|
|
return app.getVersion();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Daemon lifecycle
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function resolveDesktopDaemonStatus(): Promise<DesktopDaemonStatus> {
|
|
const home = getPaseoHome();
|
|
|
|
try {
|
|
const payload = (await runExternalCliJsonCommand(["daemon", "status", "--json"])) as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
const localDaemon = typeof payload.localDaemon === "string" ? payload.localDaemon : "stopped";
|
|
const connectedDaemon =
|
|
typeof payload.connectedDaemon === "string" ? payload.connectedDaemon : "not_probed";
|
|
const hasRunningLocalProcess = localDaemon === "running";
|
|
const hasLocalProcess = hasRunningLocalProcess || localDaemon === "unresponsive";
|
|
const desktopManaged = payload.desktopManaged === true;
|
|
const apiReachable = connectedDaemon === "reachable";
|
|
let status: DesktopDaemonState = "stopped";
|
|
if (apiReachable || hasRunningLocalProcess) {
|
|
status = "running";
|
|
} else if (localDaemon === "unresponsive") {
|
|
status = "errored";
|
|
}
|
|
|
|
return {
|
|
serverId: typeof payload.serverId === "string" ? payload.serverId : "",
|
|
status,
|
|
listen: typeof payload.listen === "string" ? payload.listen : null,
|
|
hostname:
|
|
status === "running" && typeof payload.hostname === "string" ? payload.hostname : null,
|
|
pid: hasLocalProcess && typeof payload.pid === "number" ? payload.pid : null,
|
|
home,
|
|
version: typeof payload.daemonVersion === "string" ? payload.daemonVersion : null,
|
|
desktopManaged,
|
|
error: null,
|
|
};
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logDesktopDaemonLifecycle("resolveStatus CLI command failed", { error: errorMessage });
|
|
return {
|
|
serverId: "",
|
|
status: "stopped",
|
|
listen: null,
|
|
hostname: null,
|
|
pid: null,
|
|
home,
|
|
version: null,
|
|
desktopManaged: false,
|
|
error: errorMessage,
|
|
};
|
|
}
|
|
}
|
|
|
|
function normalizeVersion(version: string | null): string | null {
|
|
const trimmed = version?.trim();
|
|
if (!trimmed) return null;
|
|
return trimmed.replace(/^v/i, "");
|
|
}
|
|
|
|
function shouldRestartForVersion(current: DesktopDaemonStatus): boolean {
|
|
if (!current.desktopManaged) return false;
|
|
const appVersion = normalizeVersion(resolveDesktopAppVersion());
|
|
const daemonVersion = normalizeVersion(current.version);
|
|
return Boolean(appVersion && daemonVersion && appVersion !== daemonVersion);
|
|
}
|
|
|
|
function assertBuiltInDaemonManagementEnabled(settings: DesktopSettings): void {
|
|
if (!settings.daemon.manageBuiltInDaemon) {
|
|
throw new Error("Built-in daemon management is disabled.");
|
|
}
|
|
}
|
|
|
|
function buildStartupFailureError(result: {
|
|
code: number | null;
|
|
signal: string | null;
|
|
error?: Error;
|
|
}): Error {
|
|
const reason = result.error
|
|
? result.error.message
|
|
: `exit code ${result.code ?? "unknown"}${result.signal ? ` (${result.signal})` : ""}`;
|
|
const parts = [`Daemon failed to start: ${reason}`];
|
|
const logs = tailFile(logFilePath(), 15);
|
|
if (logs) parts.push(`Recent logs (${logFilePath()}):\n${logs}`);
|
|
return new Error(parts.join("\n\n"));
|
|
}
|
|
|
|
async function pollForRunningDaemon(): Promise<DesktopDaemonStatus> {
|
|
async function poll(attempt: number): Promise<DesktopDaemonStatus> {
|
|
if (attempt >= STARTUP_POLL_MAX_ATTEMPTS) return resolveDesktopDaemonStatus();
|
|
const status = await resolveDesktopDaemonStatus();
|
|
if (attempt === 0 || attempt === STARTUP_POLL_MAX_ATTEMPTS - 1 || attempt % 10 === 9) {
|
|
logDesktopDaemonLifecycle("polling daemon status after detached start", {
|
|
attempt: attempt + 1,
|
|
status: status.status,
|
|
pid: status.pid,
|
|
listen: status.listen,
|
|
serverId: status.serverId || null,
|
|
});
|
|
}
|
|
if (status.status === "running" && status.serverId && status.listen) return status;
|
|
await sleep(STARTUP_POLL_INTERVAL_MS);
|
|
return poll(attempt + 1);
|
|
}
|
|
return poll(0);
|
|
}
|
|
|
|
async function startDaemon(): Promise<DesktopDaemonStatus> {
|
|
assertBuiltInDaemonManagementEnabled(await getDesktopSettingsStore().get());
|
|
|
|
const current = await resolveDesktopDaemonStatus();
|
|
logDesktopDaemonLifecycle("initial status check before start", {
|
|
status: current.status,
|
|
pid: current.pid,
|
|
listen: current.listen,
|
|
serverId: current.serverId || null,
|
|
error: current.error,
|
|
desktopManaged: current.desktopManaged,
|
|
});
|
|
if (current.status === "running") {
|
|
if (shouldRestartForVersion(current)) {
|
|
logDesktopDaemonLifecycle("daemon version mismatch, restarting", {
|
|
appVersion: normalizeVersion(resolveDesktopAppVersion()),
|
|
daemonVersion: normalizeVersion(current.version),
|
|
});
|
|
await stopDesktopDaemon("version_mismatch");
|
|
} else {
|
|
return current;
|
|
}
|
|
}
|
|
|
|
const daemonRunner = resolveDaemonRunnerEntrypoint();
|
|
const reclaimStalePidLock =
|
|
current.status === "errored" && current.desktopManaged && current.error === null;
|
|
const invocation = createNodeEntrypointInvocation({
|
|
entrypoint: daemonRunner,
|
|
argvMode: "node-script",
|
|
args: reclaimStalePidLock ? ["--reclaim-stale-pid-lock"] : [],
|
|
baseEnv: process.env,
|
|
});
|
|
|
|
logDesktopDaemonLifecycle("starting detached daemon", {
|
|
appIsPackaged: app.isPackaged,
|
|
daemonRunnerEntry: daemonRunner.entryPath,
|
|
daemonRunnerExecArgv: daemonRunner.execArgv,
|
|
command: invocation.command,
|
|
args: invocation.args,
|
|
electronRunAsNode: invocation.env.ELECTRON_RUN_AS_NODE ?? null,
|
|
parentExecPath: process.execPath,
|
|
parentElectronRunAsNode: process.env.ELECTRON_RUN_AS_NODE ?? null,
|
|
electronVersion: process.versions.electron ?? null,
|
|
nodeVersion: process.versions.node,
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
});
|
|
|
|
const child: ChildProcess = spawnProcess(invocation.command, invocation.args, {
|
|
detached: true,
|
|
envMode: "internal",
|
|
env: invocation.env,
|
|
envOverlay: {
|
|
PASEO_DESKTOP_MANAGED: "1",
|
|
PASEO_CLI: getBundledCliShimPath(),
|
|
PASEO_WEB_UI_ENABLED: "false",
|
|
},
|
|
stdio: ["ignore", "ignore", "ignore"],
|
|
});
|
|
|
|
logDesktopDaemonLifecycle("detached spawn returned", {
|
|
childPid: child.pid ?? null,
|
|
spawnfile: child.spawnfile,
|
|
spawnargs: child.spawnargs,
|
|
});
|
|
|
|
child.unref();
|
|
|
|
type GraceResult =
|
|
| { exitedEarly: false }
|
|
| { exitedEarly: true; code: number | null; signal: string | null; error?: Error };
|
|
|
|
const result = await new Promise<GraceResult>((resolve) => {
|
|
let settled = false;
|
|
const finish = (value: GraceResult) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
resolve(value);
|
|
};
|
|
|
|
const timer = setTimeout(() => finish({ exitedEarly: false }), DETACHED_STARTUP_GRACE_MS);
|
|
|
|
child.once("error", (error) => {
|
|
clearTimeout(timer);
|
|
finish({ exitedEarly: true, code: null, signal: null, error });
|
|
});
|
|
child.once("exit", (code, signal) => {
|
|
clearTimeout(timer);
|
|
finish({ exitedEarly: true, code, signal });
|
|
});
|
|
});
|
|
|
|
logDesktopDaemonLifecycle("detached startup grace period completed", {
|
|
childPid: child.pid ?? null,
|
|
exitedEarly: result.exitedEarly,
|
|
...(result.exitedEarly
|
|
? {
|
|
exitCode: result.code,
|
|
signal: result.signal,
|
|
error: result.error?.message ?? null,
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
if (result.exitedEarly) {
|
|
throw buildStartupFailureError(result);
|
|
}
|
|
|
|
return pollForRunningDaemon();
|
|
}
|
|
|
|
export async function stopDesktopDaemon(
|
|
reason: DesktopDaemonStopReason = DEFAULT_DESKTOP_DAEMON_STOP_REASON,
|
|
): Promise<DesktopDaemonStatus> {
|
|
const status = await resolveDesktopDaemonStatus();
|
|
if (status.status !== "running") {
|
|
logDesktopDaemonLifecycle("desktop daemon stop skipped", {
|
|
reason,
|
|
statusBefore: summarizeDesktopDaemonStatus(status),
|
|
});
|
|
return status;
|
|
}
|
|
|
|
const { statusAfter } = await runDesktopDaemonStopViaCli({
|
|
reason,
|
|
statusBefore: status,
|
|
resolveStatusAfter: true,
|
|
});
|
|
return statusAfter ?? (await resolveDesktopDaemonStatus());
|
|
}
|
|
|
|
async function restartDaemon(): Promise<DesktopDaemonStatus> {
|
|
assertBuiltInDaemonManagementEnabled(await getDesktopSettingsStore().get());
|
|
await stopDesktopDaemon("restart");
|
|
return startDaemon();
|
|
}
|
|
|
|
function getDaemonLogs(): DesktopDaemonLogs {
|
|
const logPath = logFilePath();
|
|
return {
|
|
logPath,
|
|
contents: tailFile(logPath, 100),
|
|
};
|
|
}
|
|
|
|
async function getCliDaemonStatus(): Promise<string> {
|
|
return await runExternalCliTextCommand(["daemon", "status"]);
|
|
}
|
|
|
|
async function getLocalDaemonVersion(): Promise<{ version: string | null; error: string | null }> {
|
|
const status = await resolveDesktopDaemonStatus();
|
|
if (status.status !== "running") {
|
|
return { version: null, error: "Daemon is not running." };
|
|
}
|
|
return {
|
|
version: status.version,
|
|
error: status.version ? null : "Running daemon did not report a version.",
|
|
};
|
|
}
|
|
|
|
async function resolveRequestedReleaseChannel(
|
|
args: Record<string, unknown> | undefined,
|
|
): Promise<AppReleaseChannel> {
|
|
return parseReleaseChannel(args) ?? (await getDesktopSettingsStore().get()).releaseChannel;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// IPC registration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function createDaemonCommandHandlers(): Record<string, DesktopCommandHandler> {
|
|
return {
|
|
...createDesktopSettingsCommandHandlers({ settingsStore: getDesktopSettingsStore() }),
|
|
desktop_get_runtime_info: () => ({
|
|
appVersion: resolveDesktopAppVersion(),
|
|
runningUnderARM64Translation: isRunningUnderARM64Translation(),
|
|
}),
|
|
desktop_daemon_status: () => resolveDesktopDaemonStatus(),
|
|
start_desktop_daemon: () => startDaemon(),
|
|
stop_desktop_daemon: (args) => stopDesktopDaemon(parseDesktopDaemonStopReason(args)),
|
|
restart_desktop_daemon: () => restartDaemon(),
|
|
desktop_daemon_logs: () => getDaemonLogs(),
|
|
desktop_app_logs: () => getDesktopAppLogs(),
|
|
desktop_get_system_idle_time: () => powerMonitor.getSystemIdleTime() * 1000,
|
|
cli_daemon_status: () => getCliDaemonStatus(),
|
|
write_attachment_base64: (args) => writeAttachmentBase64(args ?? {}),
|
|
write_attachment_bytes: (args) => writeAttachmentBytes(args ?? {}),
|
|
copy_attachment_file: (args) => copyAttachmentFileToManagedStorage(args ?? {}),
|
|
read_file_base64: (args) => readManagedFileBase64(args ?? {}),
|
|
delete_attachment_file: (args) => deleteManagedAttachmentFile(args ?? {}),
|
|
garbage_collect_attachment_files: (args) => garbageCollectManagedAttachmentFiles(args ?? {}),
|
|
open_local_daemon_transport: async (args) => {
|
|
const target = args as { transportType: "socket" | "pipe"; transportPath: string };
|
|
return await openLocalTransportSession(target);
|
|
},
|
|
open_websocket_daemon_transport: async (args) => {
|
|
return await openWebSocketTransportSession(args as unknown as WebSocketTransportTarget);
|
|
},
|
|
send_local_daemon_transport_message: async (args) => {
|
|
await sendLocalTransportMessage(
|
|
args as { sessionId: string; text?: string; binaryBase64?: string },
|
|
);
|
|
},
|
|
close_local_daemon_transport: (args) => {
|
|
const sessionId =
|
|
typeof args === "object" && args !== null && "sessionId" in args
|
|
? (args as { sessionId: string }).sessionId
|
|
: "";
|
|
if (sessionId) closeLocalTransportSession(sessionId);
|
|
},
|
|
check_app_update: async (args) => {
|
|
const currentVersion = resolveDesktopAppVersion();
|
|
return checkForAppUpdate({
|
|
currentVersion,
|
|
releaseChannel: await resolveRequestedReleaseChannel(args),
|
|
intent: parseAppUpdateCheckIntent(args),
|
|
});
|
|
},
|
|
install_app_update: async (args) => {
|
|
const currentVersion = resolveDesktopAppVersion();
|
|
return downloadAndInstallUpdate(
|
|
{ currentVersion, releaseChannel: await resolveRequestedReleaseChannel(args) },
|
|
async () => {
|
|
await stopDesktopDaemon("app_update");
|
|
},
|
|
);
|
|
},
|
|
get_local_daemon_version: () => getLocalDaemonVersion(),
|
|
install_cli: () => installCli(),
|
|
get_cli_install_status: () => getCliInstallStatus(),
|
|
...createSkillsCommandHandlers({ controller: getSkillsController() }),
|
|
};
|
|
}
|
|
|
|
export function registerDaemonManager(): void {
|
|
const handlers = createDaemonCommandHandlers();
|
|
|
|
ipcMain.handle(
|
|
"paseo:invoke",
|
|
async (_event, command: string, args?: Record<string, unknown>) => {
|
|
const handler = handlers[command];
|
|
if (!handler) {
|
|
throw new Error(`Unknown desktop command: ${command}`);
|
|
}
|
|
return await handler(args);
|
|
},
|
|
);
|
|
}
|