/** * Headless mode: this server owns one batch Aseprite (`aseprite -b`) running * headless/runner.lua, and speaks to it in JSON lines over stdin/stdout. * * The process is long-lived on purpose. One `aseprite -b` per call — the * obvious design — reopens and resaves every document on every call, loses * the active layer and frame between calls, and makes an unsaved sprite * impossible. A single process keeps documents in memory exactly as the editor * does, so every tool behaves the same in both modes and `save` means save. * * Opt-in only (`serve --headless`). See docs/adr/0008-headless-mode.md for why * this is never a fallback for a live session that went away. */ import { HelloFrame } from "../lib/protocol.js"; import type { AsepriteLink, CallOptions } from "./link.js"; import type { LiveClient } from "./client.js"; /** Prefix of every line the runner means as a frame; see headless/runner.lua. */ export declare const FRAME_MARK = "\u001Eaia "; /** * Which files the user's own Aseprite window has open, or null when no window * is attached. Headless edits to one of those would be overwritten by the next * save in the window — the exact failure the project exists to prevent. */ export interface EditorGuard { openFiles(): Promise; close(): void; } export interface HeadlessClientOptions { /** The executable to run. Null means none was found; every call refuses. */ asepritePath: string | null; /** Package root holding headless/runner.lua and extension/ai-artist.lua. */ root?: string; /** Working directory for the batch process; relative paths resolve here. */ cwd?: string; /** * Aseprite user folder for the batch process. Defaults to a scratch folder * so a headless session never loads the user's extensions or rewrites their * recent-files list; an explicit ASEPRITE_USER_FOLDER is honoured. */ userFolder?: string; /** * The operator enabled `lua.run` server-side. The extension gates the hatch * on its own environment, and this child is ours, so the flag has to reach * it as ASEPRITE_AI_ALLOW_LUA. */ allowLua?: boolean; guard?: EditorGuard | null; timeoutMs?: number; startupTimeoutMs?: number; log?: (msg: string) => void; } export declare class HeadlessClient implements AsepriteLink { readonly mode: "headless"; private readonly asepritePath; private readonly root; private readonly cwd; private readonly userFolder; private readonly allowLua; private readonly guard; private readonly timeoutMs; private readonly startupTimeoutMs; private readonly log; private child; private starting; private helloFrame; private pending; private seq; private stdoutBuffer; private diagnostics; private closed; /** * Set when the process died between calls. The next call reports it instead * of quietly starting a fresh process: every document the agent was working * on is gone, and "sprite not found" three calls later would hide why. */ private lostSession; constructor(opts: HeadlessClientOptions); get executable(): string | null; get bridgeConnected(): boolean; get pluginConnected(): boolean; get hello(): HelloFrame | null; get features(): string[]; hasFeature(name: string): boolean; /** Starts the batch process if it is not running. Never throws. */ waitForBridge(ms?: number): Promise; waitForPlugin(ms?: number): Promise; call(cmd: string, args?: Record, opts?: CallOptions): Promise; close(): void; private send; /** * The one place headless mode can hurt someone: a file that is also open in * their Aseprite window. Checked on every command that writes an * `.aseprite` document or adopts one, before it runs. */ private refuseEditorFiles; private documentTargets; private normalise; private start; private onStdout; private settle; private remember; private diagnosticTail; private rejectPending; } /** Reads which files the user's attached Aseprite window has open. */ export declare function editorGuard(live: LiveClient): EditorGuard;