/** * `spectral desktop` — the thin, headless replacement for the Electron shell. * * What the Electron app actually contributed was four things, all reproduced * here without a bundled Chromium: * * 1. single-instance guarantee → `~/.spectral/serve.pid` + liveness check * 1. single-instance guarantee → `~/.spectral/serve.pid`, claimed * atomically (`O_EXCL`) before the spawn * 2. start the local relay daemon → detached `spectral serve`, unref'd, * stdout+stderr appended to serve.log * 3. "wait until it is really up" → the `SPECTRAL_SERVE_READY` stdout marker * and `serve-ready.json` (see * src/utils/serve-state.ts), replacing the * old "assume ready after 3 s" heuristic * 4. show the UI → open the system browser at * https://aexol.ai/agent * (override: SPECTRAL_LANDING_URL) * * The PATH augmentation the app performed for its child process now happens in * `augmentPath()` (src/utils/augment-path.ts) and is applied to the spawned * daemon, so MCP servers launched via `npx` keep working. * * What is deliberately NOT reproduced (see the removal notes): the splash * window, in-app auto-update (use `spectral update`), the native app menu, and * the JWT-via-IPC login bridge — authentication goes through the existing * `spectral login` browser flow, which already writes `userJwt` to * `~/.spectral/config.json`. * * Exit codes: 0 success / already running, 1 failure, 3 stopped-state for * `status` when the daemon is not running. */ import { type SpectralConfig } from "../config.js"; import { type OpenBrowserResult } from "../utils/open-browser.js"; import { type ServeReadyResult, type ServeGenealogy } from "../utils/serve-state.js"; /** Where the Studio agent UI lives. */ export declare const DEFAULT_LANDING_URL = "https://aexol.ai/agent"; /** How long to wait for the daemon's readiness signal. */ export declare const DEFAULT_READY_TIMEOUT_MS = 15000; /** Grace period between SIGTERM and SIGKILL in `desktop stop`. */ export declare const DEFAULT_STOP_TIMEOUT_MS = 10000; /** * Set in the daemon's env to the wrapper's own pid. The daemon sees the * placeholder we left in `serve.pid` and must not read it as "another serve is * running" — it is the claim of the process that just launched it. */ export declare const SERVE_PID_CLAIM_ENV = "SPECTRAL_SERVE_PID_CLAIM"; export declare function getLandingUrl(env?: NodeJS.ProcessEnv): string; /** Render a grace window the way it is actually configured (10s, 250ms). */ export declare function formatGraceMs(ms: number): string; export interface ServeSpawnPlan { command: string; args: string[]; cwd: string; env: NodeJS.ProcessEnv; logFile: string; } interface ResolveCommandInput { execPath?: string; execArgv?: readonly string[]; argv1?: string; standalone?: boolean; /** Entry points to try, in order (already-resolved absolute paths). */ candidates?: string[]; exists?: (path: string) => boolean; } /** * Decide how to launch a detached `spectral serve`. * * Three launch shapes must all work: * - standalone Bun binary → exec the binary itself with `serve` * - npm install (`spectral` bin → dist/cli.js) → `node serve` * - `npm run dev` (tsx src/cli.ts) → re-run the very entry the user invoked, * with `process.execArgv` so the tsx loader is carried over * * @returns the spawn plan's command/args portion. Falls back to whatever * `spectral` is on PATH when no entry point can be identified. */ export declare function resolveServeCommand(input?: ResolveCommandInput): { command: string; args: string[]; }; /** Compose the full plan for spawning the daemon. */ export declare function planServeSpawn(env?: NodeJS.ProcessEnv, input?: ResolveCommandInput): ServeSpawnPlan; /** * Will this spawn re-exec into the daemon instead of becoming it directly? * * Matters for readiness: a standalone binary and `node dist/cli.js serve` are * the daemon, so their exit is a hard failure we can report instantly. Under * `npm run dev` (`tsx src/cli.ts`) the child is a loader wrapper that execs * something else — its death tells us nothing, and only the ready signal or the * timeout may decide. */ export declare function spawnExpectsReExec(plan: ServeSpawnPlan): boolean; /** * Launch the daemon detached from this terminal so it survives the wrapper * exiting (that is the whole point: close the shell, the agent stays up). * Returns the child pid, or null when the spawn itself failed. */ export declare function spawnDetachedServe(plan: ServeSpawnPlan): number | null; export interface DesktopDeps { env?: NodeJS.ProcessEnv; spawnServe?: (plan: ServeSpawnPlan) => number | null; waitForReady?: (opts: { pid: number; logFile: string; logOffset: number; /** Timestamp captured before the spawn, for ready-signal freshness. */ sinceMs: number; /** True when the child re-execs, so its own exit proves nothing. */ expectReExec: boolean; timeoutMs: number; }) => Promise; openUrl?: (url: string) => Promise; /** Interactive `spectral login`. */ runLogin?: () => Promise; readCfg?: () => Promise; signal?: (pid: number, signal: NodeJS.Signals) => void; alive?: (pid: number) => boolean; sleep?: (ms: number) => Promise; /** Override the SIGTERM→SIGKILL grace window (tests, exotic supervisors). */ stopGraceMs?: number; /** * Injectable genealogy verdict for the serve.pid hand-off confirmation. The * real probe walks `ps -o ppid=` and can transiently fail under parallel * test load, so tests pin the verdict to keep the * `daemon`/`foreign`/`unknown` classification deterministic. */ genealogy?: (pid: number, ancestors: number[]) => ServeGenealogy; planInput?: ResolveCommandInput; out?: (text: string) => void; err?: (text: string) => void; isInteractive?: () => boolean; } export interface TerminateAgentOptions { /** Pid to stop — always the one from the pid file, never a stale handle. */ pid: number; /** SIGTERM grace window before SIGKILL. */ graceMs: number; alive: (pid: number) => boolean; signal: (pid: number, signal: NodeJS.Signals) => void; sleep: (ms: number) => Promise; out?: (text: string) => void; /** Poll interval while waiting for the daemon to drain. */ pollMs?: number; /** How long to wait after SIGKILL before giving up. */ killWaitMs?: number; } export type TerminateAgentOutcome = "stopped" | "killed" | "failed"; /** * SIGTERM → wait out the drain → SIGKILL. Shared by `desktop stop` and by the * start path's "it never came up, do not leave it half-started" branch — the * old start path fired a bare SIGTERM and walked away, which left a detached * daemon that ignored TERM (or won the race) running behind a failed command. * * The wait is bounded by a poll COUNT rather than a wall-clock deadline, so a * caller with a synchronous `sleep` (tests) is both fast and deterministic. */ export declare function terminateAgentPid(opts: TerminateAgentOptions): Promise; /** * `spectral desktop [start|status|stop]` * * @returns process exit code; `cli.ts` assigns it to `process.exitCode`. */ export declare function runDesktop(args?: string[], deps?: DesktopDeps): Promise; export {}; //# sourceMappingURL=desktop.d.ts.map