export const APP_KEYS = Object.freeze({ DAEMON: "daemon", DESKTOP: "desktop", WEB: "web", } as const); export type AppKey = (typeof APP_KEYS)[keyof typeof APP_KEYS]; export const SIDECAR_MODES = Object.freeze({ DEV: "dev", RUNTIME: "runtime", } as const); export type SidecarMode = (typeof SIDECAR_MODES)[keyof typeof SIDECAR_MODES]; export const SIDECAR_SOURCES = Object.freeze({ PACKAGED: "packaged", TOOLS_DEV: "tools-dev", TOOLS_PACK: "tools-pack", } as const); export type SidecarSource = (typeof SIDECAR_SOURCES)[keyof typeof SIDECAR_SOURCES]; export const SIDECAR_ENV = Object.freeze({ BASE: "OD_SIDECAR_BASE", DAEMON_PORT: "OD_PORT", IPC_BASE: "OD_SIDECAR_IPC_BASE", IPC_PATH: "OD_SIDECAR_IPC_PATH", NAMESPACE: "OD_SIDECAR_NAMESPACE", SOURCE: "OD_SIDECAR_SOURCE", TOOLS_DEV_PARENT_PID: "OD_TOOLS_DEV_PARENT_PID", WEB_DIST_DIR: "OD_WEB_DIST_DIR", WEB_PORT: "OD_WEB_PORT", WEB_TSCONFIG_PATH: "OD_WEB_TSCONFIG_PATH", } as const); export const SIDECAR_RUNTIME_ENV = Object.freeze({ base: SIDECAR_ENV.BASE, ipcBase: SIDECAR_ENV.IPC_BASE, ipcPath: SIDECAR_ENV.IPC_PATH, namespace: SIDECAR_ENV.NAMESPACE, source: SIDECAR_ENV.SOURCE, } as const); export const SIDECAR_STAMP_FLAGS = Object.freeze({ app: "--od-stamp-app", ipc: "--od-stamp-ipc", mode: "--od-stamp-mode", namespace: "--od-stamp-namespace", source: "--od-stamp-source", } as const); export const STAMP_APP_FLAG = SIDECAR_STAMP_FLAGS.app; export const STAMP_IPC_FLAG = SIDECAR_STAMP_FLAGS.ipc; export const STAMP_MODE_FLAG = SIDECAR_STAMP_FLAGS.mode; export const STAMP_NAMESPACE_FLAG = SIDECAR_STAMP_FLAGS.namespace; export const STAMP_SOURCE_FLAG = SIDECAR_STAMP_FLAGS.source; export const SIDECAR_STAMP_FIELDS = ["app", "mode", "namespace", "ipc", "source"] as const; export const SIDECAR_DEFAULTS = Object.freeze({ host: "127.0.0.1", ipcBase: "/tmp/open-design/ipc", namespace: "default", projectTmpDirName: ".tmp", windowsPipePrefix: "open-design", } as const); export const SIDECAR_MESSAGES = Object.freeze({ CLICK: "click", CONSOLE: "console", EVAL: "eval", SCREENSHOT: "screenshot", SHUTDOWN: "shutdown", STATUS: "status", } as const); export const SIDECAR_ERROR_CODES = Object.freeze({ INVALID_MESSAGE: "SIDECAR_INVALID_MESSAGE", UNKNOWN_MESSAGE: "SIDECAR_UNKNOWN_MESSAGE", } as const); export type SidecarErrorCode = (typeof SIDECAR_ERROR_CODES)[keyof typeof SIDECAR_ERROR_CODES]; export class SidecarContractError extends Error { readonly code: SidecarErrorCode; constructor(code: SidecarErrorCode, message: string) { super(message); this.name = "SidecarContractError"; this.code = code; } } export type ServiceRuntimeState = "idle" | "running" | "starting" | "stopped" | "unknown"; export type DaemonStatusSnapshot = { pid?: number | null; state: ServiceRuntimeState; updatedAt?: string; url: string | null; }; export type WebStatusSnapshot = { pid?: number | null; state: ServiceRuntimeState; updatedAt?: string; url: string | null; }; export type DesktopRuntimeState = "idle" | "running" | "unknown"; export type DesktopStatusSnapshot = { pid?: number | null; state: DesktopRuntimeState; title?: string | null; updatedAt?: string; url?: string | null; windowVisible?: boolean; }; export type DesktopEvalInput = { expression: string; }; export type DesktopEvalResult = { error?: string; ok: boolean; value?: unknown; }; export type DesktopScreenshotInput = { path: string; }; export type DesktopScreenshotResult = { path: string; }; export type DesktopConsoleEntry = { level: string; text: string; timestamp: string; }; export type DesktopConsoleResult = { entries: DesktopConsoleEntry[]; }; export type DesktopClickInput = { selector: string; }; export type DesktopClickResult = { clicked: boolean; found: boolean; }; export type SidecarStatusMessage = { type: typeof SIDECAR_MESSAGES.STATUS }; export type SidecarShutdownMessage = { type: typeof SIDECAR_MESSAGES.SHUTDOWN }; export type DesktopEvalMessage = { input: DesktopEvalInput; type: typeof SIDECAR_MESSAGES.EVAL }; export type DesktopScreenshotMessage = { input: DesktopScreenshotInput; type: typeof SIDECAR_MESSAGES.SCREENSHOT }; export type DesktopConsoleMessage = { type: typeof SIDECAR_MESSAGES.CONSOLE }; export type DesktopClickMessage = { input: DesktopClickInput; type: typeof SIDECAR_MESSAGES.CLICK }; export type DaemonSidecarMessage = SidecarStatusMessage | SidecarShutdownMessage; export type WebSidecarMessage = SidecarStatusMessage | SidecarShutdownMessage; export type DesktopSidecarMessage = | SidecarStatusMessage | SidecarShutdownMessage | DesktopEvalMessage | DesktopScreenshotMessage | DesktopConsoleMessage | DesktopClickMessage; export type ShutdownResult = { accepted: true; }; export type SidecarStamp = { app: AppKey; ipc: string; mode: SidecarMode; namespace: string; source: SidecarSource; }; export type SidecarStampInput = Partial>; export type SidecarStampCriteria = Partial; export type OpenDesignSidecarContract = { appKeys: typeof APP_KEYS; defaults: typeof SIDECAR_DEFAULTS; env: typeof SIDECAR_RUNTIME_ENV; errorCodes: typeof SIDECAR_ERROR_CODES; messages: typeof SIDECAR_MESSAGES; modes: typeof SIDECAR_MODES; normalizeApp: typeof normalizeAppKey; normalizeNamespace: typeof normalizeNamespace; normalizeSource: typeof normalizeSidecarSource; normalizeStamp: typeof normalizeSidecarStamp; normalizeStampCriteria: typeof normalizeSidecarStampCriteria; sources: typeof SIDECAR_SOURCES; stampFields: typeof SIDECAR_STAMP_FIELDS; stampFlags: typeof SIDECAR_STAMP_FLAGS; }; function assertObject(value: unknown, label: string): Record { if (typeof value !== "object" || value == null || Array.isArray(value)) { throw new Error(`${label} must be an object`); } return value as Record; } function assertKnownKeys(value: Record, allowed: readonly string[], label: string): void { const allowedSet = new Set(allowed); const unexpected = Object.keys(value).filter((key) => !allowedSet.has(key)); if (unexpected.length > 0) { throw new Error(`${label} contains unsupported fields: ${unexpected.join(", ")}`); } } function normalizeNonEmptyString(value: unknown, label: string): string { if (typeof value !== "string") throw new Error(`${label} must be a string`); if (value.length === 0) throw new Error(`${label} must not be empty`); return value; } export function normalizeNamespace(namespace: unknown): string { if (typeof namespace !== "string") throw new Error("namespace must be a string"); const value = namespace.trim(); if (value.length === 0) throw new Error("namespace must not be empty"); if (value !== namespace) throw new Error("namespace must not contain leading or trailing whitespace"); if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) { throw new Error(`namespace contains unsupported characters: ${value}`); } if (/[\\/]/.test(value)) throw new Error(`namespace must not contain path separators: ${value}`); return value; } export function isSidecarMode(value: unknown): value is SidecarMode { return Object.values(SIDECAR_MODES).includes(value as SidecarMode); } export function normalizeSidecarMode(mode: unknown): SidecarMode { if (!isSidecarMode(mode)) { throw new Error("sidecar mode must be dev or runtime"); } return mode; } export function isAppKey(value: unknown): value is AppKey { return Object.values(APP_KEYS).includes(value as AppKey); } export function normalizeAppKey(app: unknown): AppKey { if (!isAppKey(app)) throw new Error(`unsupported sidecar app: ${String(app)}`); return app; } export function isSidecarSource(value: unknown): value is SidecarSource { return Object.values(SIDECAR_SOURCES).includes(value as SidecarSource); } export function normalizeSidecarSource(source: unknown): SidecarSource { if (!isSidecarSource(source)) { throw new Error(`unsupported sidecar source: ${String(source)}`); } return source; } export function isWindowsNamedPipePath(value: unknown): boolean { return typeof value === "string" && value.startsWith("\\\\.\\pipe\\"); } export function normalizeIpcPath(ipc: unknown): string { if (typeof ipc !== "string") throw new Error("sidecar ipc path must be a string"); if (ipc.length === 0) throw new Error("sidecar ipc path must not be empty"); if (ipc.trim() !== ipc) throw new Error("sidecar ipc path must not contain leading or trailing whitespace"); if (ipc.includes("\0")) throw new Error("sidecar ipc path must not contain null bytes"); if (isWindowsNamedPipePath(ipc)) return ipc; if (!ipc.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(ipc)) { throw new Error(`sidecar ipc path must be absolute: ${ipc}`); } return ipc; } function assertKnownStampKeys(value: Record, label: string): void { assertKnownKeys(value, SIDECAR_STAMP_FIELDS, label); } export function normalizeSidecarStamp(input: unknown): SidecarStamp { const value = assertObject(input, "sidecar stamp"); assertKnownStampKeys(value, "sidecar stamp"); return { app: normalizeAppKey(value.app), ipc: normalizeIpcPath(value.ipc), mode: normalizeSidecarMode(value.mode), namespace: normalizeNamespace(value.namespace), source: normalizeSidecarSource(value.source), }; } export function normalizeSidecarStampCriteria(input: unknown = {}): SidecarStampCriteria { const value = assertObject(input, "sidecar stamp criteria"); assertKnownStampKeys(value, "sidecar stamp criteria"); return { ...(value.app == null ? {} : { app: normalizeAppKey(value.app) }), ...(value.ipc == null ? {} : { ipc: normalizeIpcPath(value.ipc) }), ...(value.mode == null ? {} : { mode: normalizeSidecarMode(value.mode) }), ...(value.namespace == null ? {} : { namespace: normalizeNamespace(value.namespace) }), ...(value.source == null ? {} : { source: normalizeSidecarSource(value.source) }), }; } export function assertSidecarStamp(input: unknown): asserts input is SidecarStamp { normalizeSidecarStamp(input); } function normalizeDesktopEvalInput(input: unknown): DesktopEvalInput { const value = assertObject(input, "desktop eval input"); assertKnownKeys(value, ["expression"], "desktop eval input"); return { expression: normalizeNonEmptyString(value.expression, "desktop eval expression") }; } function normalizeDesktopScreenshotInput(input: unknown): DesktopScreenshotInput { const value = assertObject(input, "desktop screenshot input"); assertKnownKeys(value, ["path"], "desktop screenshot input"); return { path: normalizeNonEmptyString(value.path, "desktop screenshot path") }; } function normalizeDesktopClickInput(input: unknown): DesktopClickInput { const value = assertObject(input, "desktop click input"); assertKnownKeys(value, ["selector"], "desktop click input"); return { selector: normalizeNonEmptyString(value.selector, "desktop click selector") }; } function normalizeMessageType(value: unknown, label: string): string { if (typeof value !== "string" || value.length === 0) { throw new SidecarContractError(SIDECAR_ERROR_CODES.INVALID_MESSAGE, `${label} type must be a non-empty string`); } return value; } export function normalizeDaemonSidecarMessage(input: unknown): DaemonSidecarMessage { const value = assertObject(input, "daemon sidecar message"); const type = normalizeMessageType(value.type, "daemon sidecar message"); if (type === SIDECAR_MESSAGES.STATUS || type === SIDECAR_MESSAGES.SHUTDOWN) { assertKnownKeys(value, ["type"], "daemon sidecar message"); return { type }; } throw new SidecarContractError(SIDECAR_ERROR_CODES.UNKNOWN_MESSAGE, `unknown daemon sidecar message: ${type}`); } export function normalizeWebSidecarMessage(input: unknown): WebSidecarMessage { const value = assertObject(input, "web sidecar message"); const type = normalizeMessageType(value.type, "web sidecar message"); if (type === SIDECAR_MESSAGES.STATUS || type === SIDECAR_MESSAGES.SHUTDOWN) { assertKnownKeys(value, ["type"], "web sidecar message"); return { type }; } throw new SidecarContractError(SIDECAR_ERROR_CODES.UNKNOWN_MESSAGE, `unknown web sidecar message: ${type}`); } export function normalizeDesktopSidecarMessage(input: unknown): DesktopSidecarMessage { const value = assertObject(input, "desktop sidecar message"); const type = normalizeMessageType(value.type, "desktop sidecar message"); switch (type) { case SIDECAR_MESSAGES.STATUS: case SIDECAR_MESSAGES.SHUTDOWN: case SIDECAR_MESSAGES.CONSOLE: assertKnownKeys(value, ["type"], "desktop sidecar message"); return { type }; case SIDECAR_MESSAGES.EVAL: assertKnownKeys(value, ["input", "type"], "desktop sidecar message"); return { input: normalizeDesktopEvalInput(value.input), type }; case SIDECAR_MESSAGES.SCREENSHOT: assertKnownKeys(value, ["input", "type"], "desktop sidecar message"); return { input: normalizeDesktopScreenshotInput(value.input), type }; case SIDECAR_MESSAGES.CLICK: assertKnownKeys(value, ["input", "type"], "desktop sidecar message"); return { input: normalizeDesktopClickInput(value.input), type }; default: throw new SidecarContractError(SIDECAR_ERROR_CODES.UNKNOWN_MESSAGE, `unknown desktop sidecar message: ${type}`); } } export const OPEN_DESIGN_SIDECAR_CONTRACT = Object.freeze({ appKeys: APP_KEYS, defaults: SIDECAR_DEFAULTS, env: SIDECAR_RUNTIME_ENV, errorCodes: SIDECAR_ERROR_CODES, messages: SIDECAR_MESSAGES, modes: SIDECAR_MODES, normalizeApp: normalizeAppKey, normalizeNamespace, normalizeSource: normalizeSidecarSource, normalizeStamp: normalizeSidecarStamp, normalizeStampCriteria: normalizeSidecarStampCriteria, sources: SIDECAR_SOURCES, stampFields: SIDECAR_STAMP_FIELDS, stampFlags: SIDECAR_STAMP_FLAGS, } as const satisfies OpenDesignSidecarContract);