import type { HookDispatcher } from '../hooks/dispatcher.js'; import type { RuntimeEventBus } from './events/index.js'; import type { RuntimeServices } from './services.js'; import { type DaemonIdentityProbeResult } from './daemon-adoption-policy.js'; interface DaemonService { enable(config: { daemon: boolean; }, token?: string): boolean; start(): Promise; stop(): Promise; listRecentControlPlaneEvents(limit: number): readonly import('../control-plane/gateway.js').ControlPlaneRecentEvent[]; } interface HttpListenerService { enable(config: { httpListener: boolean; }, token?: string): boolean; start(): Promise; stop(): Promise; } /** * Options passed to the injectable detached-daemon spawn seam. Mirrors the subset * of `child_process.SpawnOptions` the detached spawn relies on. `detached: true` * and the caller's subsequent `unref()` are what let the daemon outlive this * surface, tests assert both are present. */ export interface DetachedDaemonSpawnOptions { readonly detached: boolean; readonly stdio: 'ignore' | ReadonlyArray<'ignore' | number>; readonly cwd?: string | undefined; readonly env?: NodeJS.ProcessEnv | undefined; } /** Minimal child-process shape the detached spawn seam must return. */ export interface DetachedDaemonChild { readonly pid?: number | undefined; unref(): void; once?(event: 'error' | 'exit', listener: (arg: unknown) => void): void; } /** * One-time hint surfaced ONCE by the TUI after a successful detached spawn. * The daemon promotes itself to a supervised service at its first idle * moment when a service manager is available (see the facade lifecycle's * boot promotion), so this mostly narrates what happens on its own; where * promotion is not possible, it names the one-command path, never a raw * HTTP instruction. */ export declare const DETACHED_DAEMON_INSTALL_HINT = "daemon started for this session, it installs itself as a system service at its first idle moment when the platform supports it; if it stays session-only, run: goodvibes-daemon --install-service"; interface ServiceFactories { /** * Injectable spawn seam for the detached standalone daemon, the only way this * host starts a daemon. Defaults to `child_process.spawn`. Tests stub this to * assert the spawn happens and that the options carry `detached: true` (the * caller then `unref()`s the returned child). */ spawnDetachedDaemon?: (command: string, args: readonly string[], options: DetachedDaemonSpawnOptions) => DetachedDaemonChild; /** Command used to launch the detached daemon. Default: `GOODVIBES_DAEMON_BINARY` env or `goodvibes-daemon`. */ daemonLaunchCommand?: string | undefined; /** Extra CLI args appended after the resolved `--daemon-home/--hostname/--port` flags. */ daemonLaunchArgs?: readonly string[] | undefined; /** Daemon home directory passed via `--daemon-home`. Default: `os.homedir()`. */ daemonHomeDir?: string | undefined; /** Directory where the detached daemon records pid/port + log. Default `/.goodvibes/daemon`. */ daemonRuntimeDir?: string | undefined; /** Bounded time to wait for the detached daemon to bind + pass the identity probe. */ detachedSpawnProbeTimeoutMs?: number | undefined; /** Poll interval while waiting for the detached daemon to become reachable. */ detachedSpawnProbeIntervalMs?: number | undefined; /** Sleep function (injectable so tests drive the probe loop deterministically). */ sleep?: ((ms: number) => Promise) | undefined; /** * Injectable HTTP-listener constructor. There is no default: the listener * class lives in the daemon product's composition surface, and a client host * does not reach into it. With `danger.httpListener` enabled and no factory * here, the listener reports `unavailable` with that reason rather than * silently appearing to be off. */ createHttpListener?: (hookDispatcher: HookDispatcher, userAuth: RuntimeServices['localUserAuthManager'], configManager: RuntimeServices['configManager']) => HttpListenerService; startupTimeoutMs?: number | undefined; probeDaemonPortInUse?: ((host: string, port: number) => Promise) | undefined; probeDaemonIdentity?: (host: string, port: number, token?: string) => Promise; probeHttpListenerPortInUse?: ((host: string, port: number) => Promise) | undefined; /** * Shared bearer token used when probing and when launching the detached * daemon, so requests carrying `Authorization: Bearer ` authenticate * without a login session. Surfaces that generate companion-app pairing tokens * pass the token here so the daemon they adopt accepts scanned QR credentials. */ sharedDaemonToken?: string | undefined; /** * Shared bearer token for the HTTP listener (webhook-style surfaces). * Independent from `sharedDaemonToken`; different surfaces may issue * different tokens, or both may share the same bearer. */ sharedHttpListenerToken?: string | undefined; /** * This surface's own SDK version, used to band-check a daemon found on the * configured port before adopting it. Defaults to the SDK `VERSION`. Injected * mainly so tests can drive the compatibility gate deterministically. */ localDaemonVersion?: string | undefined; /** * Override for the version-compatibility predicate. Defaults to the shared * `isDaemonVersionCompatible` band policy. Tests inject a stub to exercise the * incompatible-adoption refusal without constructing skewed daemons. */ isDaemonVersionCompatible?: ((localVersion: string, remoteVersion: string | undefined) => boolean) | undefined; /** * Adopt-only policy: attach to a compatible running daemon but NEVER spawn one * when the port is free. Expresses the "this surface does not own the daemon * lifecycle" stance (e.g. the agent connecting to an externally-owned host) as * configuration rather than a wholesale override of this function. The version * band-check still applies before adopting. Default false. */ adoptOnly?: boolean | undefined; } export type HostServiceMode = 'disabled' | 'embedded' | 'external' | 'blocked' | 'incompatible' | 'unavailable'; export interface HostServiceStatus { readonly mode: HostServiceMode; readonly host: string; readonly port: number; readonly baseUrl: string; readonly reason?: string | undefined; readonly status?: string | undefined; readonly version?: string | undefined; readonly authenticated?: boolean | undefined; } export interface HostServicesHandle { /** * Always null from this function: a host does not compose a daemon. It adopts * an external one or spawns the detached `goodvibes-daemon` binary, so there * is no in-process daemon object to hand back. The field stays so callers that * build their own pre-start handle keep one shape. */ readonly daemonServer: DaemonService | null; readonly httpListener: HttpListenerService | null; readonly daemonStatus: HostServiceStatus; readonly httpListenerStatus: HostServiceStatus; /** * One-time, honest hint the surface can display ONCE after this host instance * spawned a detached daemon for the session. Present only when a detached * daemon was just started (Layer 2); undefined when a daemon was adopted, * disabled, or unavailable. See {@link DETACHED_DAEMON_INSTALL_HINT}. */ readonly daemonStartHint?: string | undefined; listRecentControlPlaneEvents(limit: number): readonly import('../control-plane/gateway.js').ControlPlaneRecentEvent[]; stop(): Promise; } export interface HostServicesConfig { get(key: 'daemon.enabled' | 'danger.httpListener' | 'controlPlane.host' | 'controlPlane.port' | 'httpListener.host' | 'httpListener.port'): boolean | string | number | undefined; } /** * Start the host-side services a client surface can own: none of them is the * daemon itself. * * This module reaches `platform/daemon` neither statically nor dynamically. The * daemon's composition (DaemonServer, HttpListener) belongs to the * `goodvibes-daemon` product, which imports those classes directly in its own * entrypoint. A client that could reach them, even behind an `await import()` * never taken at runtime, drags the whole daemon graph into its bundle, and the * lazy module wrappers a bundler emits to make that possible are what left * shared platform constants uninitialized when hoisted functions read them. * So the daemon paths here are: adopt one already running, or spawn the detached * standalone binary. Nothing constructs one in this process. */ export declare function startHostServices(config: HostServicesConfig, runtimeBus: RuntimeEventBus, hookDispatcher: HookDispatcher, runtimeServices: RuntimeServices, factories?: ServiceFactories): Promise; export {}; //# sourceMappingURL=bootstrap-services.d.ts.map