first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
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<Record<(typeof SIDECAR_STAMP_FIELDS)[number], unknown>>;
|
||||
export type SidecarStampCriteria = Partial<SidecarStamp>;
|
||||
|
||||
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<string, unknown> {
|
||||
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
||||
throw new Error(`${label} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function assertKnownKeys(value: Record<string, unknown>, allowed: readonly string[], label: string): void {
|
||||
const allowedSet = new Set<string>(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<string, unknown>, 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);
|
||||
Reference in New Issue
Block a user