import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import type { AiAgent, AiSession, Flavor, Image, Machine, Network, NewFlavor, ProbeResult, RunSpec, DockerContainer, DockerStatus, Settings, Snapshot, SystemStats, VersionEntry, Volume, } from "./types"; export const api = { getSettings: () => invoke("get_settings"), setSettings: (binaryPath: string, cachePath: string, token: string) => invoke("set_settings", { binaryPath, cachePath, token }), defaultCache: () => invoke("default_cache"), probe: () => invoke("probe"), setTrayStatus: (ok: boolean, detail: string) => invoke("set_tray_status", { ok, detail }), listMachines: (all: boolean) => invoke("list_machines", { all }), listImages: () => invoke("list_images"), listVolumes: () => invoke("list_volumes"), listVersions: (os: string) => invoke("list_versions", { os }), listFlavors: () => invoke("list_flavors"), listNetworks: () => invoke("list_networks"), createNetwork: (name: string) => invoke("create_network", { name }), removeNetwork: (name: string, force: boolean) => invoke("remove_network", { name, force }), syncNetwork: (name: string) => invoke("sync_network", { name }), systemStats: () => invoke("system_stats"), runFlavor: (name: string, ports: string[], volume: string | null) => invoke("run_flavor", { name, ports, volume }), launchFlavor: ( launchId: string, name: string, ports: string[], volume: string | null, repo: string | null, ) => invoke("launch_flavor", { launchId, name, ports, volume, repo }), buildFlavor: (launchId: string, name: string) => invoke("build_flavor", { launchId, name }), createFlavor: (spec: NewFlavor) => invoke("create_flavor", { ...spec }), commitMachine: (id: string, name: string, description: string) => invoke("commit_machine", { id, name, description }), // AI agent sandboxes. A sandbox is a machine, so its terminal is the // ordinary `termOpen` with the agent's argv. aiAgents: () => invoke("ai_agents"), aiSessions: () => invoke("ai_sessions"), /** Starts or reuses a sandbox; returns its machine id. */ aiStart: ( agent: string, workspace: string | null, newSession: boolean, name?: string, repo?: string, ) => invoke("ai_start", { agent, workspace, newSession, name: name ?? null, repo: repo ?? null, }), /** Same, but streams the first-run flavor build into the progress modal. */ launchAgent: ( launchId: string, agent: string, workspace: string | null, newSession: boolean, name?: string, repo?: string, ) => invoke("launch_agent", { launchId, agent, workspace, newSession, name: name ?? null, repo: repo ?? null, }), /** The CI workflows in a repository, and whether each matches an event. */ ciWorkflows: (dir: string, event: string) => invoke("ci_workflows", { dir, event }), /** Run CI workflows, streaming spindle LogLine JSON via flavor:// events. */ ciRun: ( launchId: string, dir: string, names: string[], event: string, secrets?: string, ) => invoke("ci_run", { launchId, dir, names, event, secrets: secrets ?? null, }), /** Clone (or update) a repository for CI; returns the checkout path. */ ciClone: (url: string) => invoke("ci_clone", { url }), /** Kill the CI run started with this launch id. */ ciCancel: (launchId: string) => invoke("launch_cancel", { launchId }), /** Resume one stopped sandbox, streaming its boot. */ resumeAgent: (launchId: string, machine: string) => invoke("resume_agent", { launchId, machine }), /** The argv that starts the agent's TUI in a sandbox. */ aiShellCommand: (agent: string, machineId: string) => invoke("ai_shell_command", { agent, machineId }), aiStop: (agent: string) => invoke("ai_stop", { agent }), aiRemove: (agent: string, keepHome: boolean) => invoke("ai_remove", { agent, keepHome }), // Docker: one dind microVM the host's own `docker` CLI drives. dockerStatus: () => invoke("docker_status"), dockerContainers: (all: boolean) => invoke("docker_containers", { all }), /** Starts or resumes the engine — waits until dockerd answers. */ dockerStart: (cpus?: number, mem?: number, diskSize?: string) => invoke("docker_start", { cpus: cpus ?? null, mem: mem ?? null, diskSize: diskSize ?? null, }), dockerStop: () => invoke("docker_stop"), /** start | stop | restart | kill | pause | unpause | rm */ dockerContainer: (action: string, id: string) => invoke("docker_container", { action, id }), dockerLogs: (id: string, tail: number) => invoke("docker_logs", { id, tail }), // Snapshots: copy-on-write captures of a machine's disk state. `machine` // narrows the list to one machine's; omit it for every snapshot on the host. listSnapshots: (machine?: string | null) => invoke("list_snapshots", { machine: machine ?? null }), /** Returns the snapshot's name — generated when none was given. */ snapshotMachine: (id: string, name: string | null, description: string) => invoke("snapshot_machine", { id, name, description }), removeSnapshot: (name: string) => invoke("remove_snapshot", { name }), /** Leaves the machine stopped; start it to run the restored state. */ restoreMachine: (id: string, snapshot: string, backup: boolean) => invoke("restore_machine", { id, snapshot, backup }), rollbackMachine: (id: string, backup: boolean) => invoke("rollback_machine", { id, backup }), /** Boots a new machine from a snapshot; returns its id. */ branchSnapshot: (snapshot: string, name: string | null, ports: string[]) => invoke("branch_snapshot", { snapshot, name, ports }), removeFlavor: (name: string, force: boolean) => invoke("remove_flavor", { name, force }), runMachine: (spec: RunSpec) => invoke("run_machine", { spec }), launchMachine: (launchId: string, spec: RunSpec) => invoke("launch_machine", { launchId, spec }), updateMachine: (id: string, cpus: number, mem: number) => invoke("update_machine", { id, cpus, mem }), updateMachineNetwork: (id: string, network: string | null) => invoke("update_machine_network", { id, network }), stopMachine: (id: string) => invoke("stop_machine", { id }), restartMachine: (id: string) => invoke("restart_machine", { id }), removeMachine: (id: string, force: boolean) => invoke("remove_machine", { id, force }), /** Only a dangling image can go; the engine refuses one still in use. */ removeImage: (id: string) => invoke("remove_image", { id }), removeVolume: (name: string, force: boolean) => invoke("remove_volume", { name, force }), machineLogs: (id: string, boot: boolean) => invoke("machine_logs", { id, boot }), sshAction: (id: string, args: string[]) => invoke("ssh_action", { id, args }), tailscaleAction: (id: string, args: string[]) => invoke("tailscale_action", { id, args }), updateAgent: (id: string) => invoke("update_agent", { id }), startLogStream: (id: string) => invoke("start_log_stream", { id }), stopLogStream: (id: string) => invoke("stop_log_stream", { id }), termOpen: (id: string, command: string[], rows: number, cols: number) => invoke("term_open", { id, command, rows, cols }), termOpenHost: (rows: number, cols: number) => invoke("term_open_host", { rows, cols }), termWrite: (session: string, data: string) => invoke("term_write", { session, data }), termResize: (session: string, rows: number, cols: number) => invoke("term_resize", { session, rows, cols }), termClose: (session: string) => invoke("term_close", { session }), }; // ---- event payloads -------------------------------------------------------- export interface TermData { session: string; bytes: number[]; } export interface TermExit { session: string; code: number | null; } export interface LogLine { id: string; line: string; } export interface LogEnd { id: string; } export interface FlavorLog { launch_id: string; line: string; } export interface FlavorDone { launch_id: string; id: string | null; error: string | null; } export const onTermData = (cb: (p: TermData) => void): Promise => listen("term://data", (e) => cb(e.payload)); export const onTermExit = (cb: (p: TermExit) => void): Promise => listen("term://exit", (e) => cb(e.payload)); export const onLogLine = (cb: (p: LogLine) => void): Promise => listen("log://line", (e) => cb(e.payload)); export const onLogEnd = (cb: (p: LogEnd) => void): Promise => listen("log://end", (e) => cb(e.payload)); export const onFlavorLog = (cb: (p: FlavorLog) => void): Promise => listen("flavor://log", (e) => cb(e.payload)); export const onFlavorDone = (cb: (p: FlavorDone) => void): Promise => listen("flavor://done", (e) => cb(e.payload)); /** This build can open a native directory picker (see `pickWorkspace`). */ export const HAS_NATIVE_FOLDER_PICKER = true; /** * Ask the user for a folder to share with an agent. `null` when cancelled — * distinct from `""`, which would mean "share nothing". */ export async function pickWorkspace(): Promise { const { open } = await import("@tauri-apps/plugin-dialog"); const picked = await open({ directory: true, multiple: false }); return typeof picked === "string" ? picked : null; } export const onMenuAction = (cb: (action: string) => void): Promise => listen("menu://action", (e) => cb(e.payload));