From d8f0421d230e35b605260d310dfd8eb22f9252fa Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Wed, 17 Jun 2026 22:47:56 -0700 Subject: [PATCH] Initial control-plane protocol package --- flake.nix | 20 + package.json | 13 + src/index.ts | 1974 +++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 19 + yarn.lock | 0 5 files changed, 2026 insertions(+) create mode 100644 flake.nix create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..598e21c --- /dev/null +++ b/flake.nix @@ -0,0 +1,20 @@ +{ + description = "Shared Quixos control-plane protocol types"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in { + devShells.default = pkgs.mkShell { + packages = [ + pkgs.nodejs_24 + ]; + }; + }); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2b4ea56 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "@quixos/control-plane-protocol", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "node --check src/index.ts" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ac89f9a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,1974 @@ +export const CONTROL_PLANE_DEFAULT_HOST = "127.0.0.1"; +export const CONTROL_PLANE_DEFAULT_PORT = 6246; +export const CONTROL_PLANE_DEFAULT_URL = `http://${CONTROL_PLANE_DEFAULT_HOST}:${CONTROL_PLANE_DEFAULT_PORT}`; + +export const CONTROL_PLANE_ROUTES = { + run: "/run", + status: "/status", + check: "/check", + cutover: "/cutover", + clearState: "/clear-state", + describe: "/describe", + logs: "/logs", + errors: "/errors", + devices: "/devices", + deviceAssign: "/devices/assign", + deviceAssignLive: "/devices/assign-live", + deviceRename: "/devices/rename", + deviceIdentify: "/devices/identify", + deviceStopIdentify: "/devices/stop-identify", + deviceRemove: "/devices/remove", + deviceSession: "/device/session", + deviceSocket: "/device-ws", + replEval: "/repl/eval", + replClose: "/repl/close", + browserSocket: "/ws", +} as const; + +export type ControlPlaneRunRequest = { + target: string; +}; + +export type ControlPlaneCheckRequest = { + packageName: string; + rev: string; +}; + +export type ControlPlaneCutoverRequest = { + targets: string[]; +}; + +export type ControlPlaneClearStateRequest = { + packageName: string; + recursive?: boolean; +}; + +export type ControlPlaneDescribeRequest = { + packageName: string; + rev: string; +}; + +export type ControlPlaneLogsRequest = { + packageName: string; + tail?: number; + stream?: "stdout" | "stderr" | "all"; +}; + +export type ControlPlaneErrorsRequest = { + packageName?: string; + stateContextId?: string; + tail?: number; + acknowledge?: boolean; +}; + +export type ControlPlaneReplEvalRequest = { + sessionId?: string; + code: string; + autoClose?: boolean; + defaultStateContextId?: string | null; +}; + +export type ControlPlaneReplCloseRequest = { + sessionId: string; +}; + +export type ControlPlaneDeviceTarget = { + packageName: string; + stateContextId: string; + surfaceId: string; + requestFullscreen?: boolean; +}; + +export type ControlPlaneDeviceViewport = { + width: number; + height: number; + pixelRatio: number | null; +}; + +export type ControlPlaneDevice = { + deviceId: string; + name: string | null; + createdAt: string; + lastSeenAt: string | null; + connected: boolean; + assignment: ControlPlaneDeviceTarget | null; + configuredAssignment: ControlPlaneDeviceTarget | null; + identifyUntil: string | null; + lastUserAgent: string | null; + viewport: ControlPlaneDeviceViewport | null; +}; + +export type ControlPlaneDevicesResponse = { + devices: ControlPlaneDevice[]; +}; + +export type ControlPlaneDeviceAssignRequest = { + deviceId: string; + target: ControlPlaneDeviceTarget | null; +}; + +export type ControlPlaneDeviceRenameRequest = { + deviceId: string; + name: string | null; +}; + +export type ControlPlaneDeviceRemoveRequest = { + deviceId: string; +}; + +export type ControlPlaneDeviceIdentifyRequest = { + deviceId: string; + durationMs?: number; +}; + +export type ControlPlaneDeviceSessionRequest = { + deviceId: string; + requestedTarget?: ControlPlaneDeviceTarget | null; + userAgent?: string | null; + viewport?: ControlPlaneDeviceViewport | null; +}; + +export type ControlPlaneDeviceSessionResponse = { + device: ControlPlaneDevice; +}; + +export type ControlPlaneWorkspacePackage = { + packageName: string; + headCommit: string; + ignored: boolean; +}; + +export type ControlPlaneStateContext = { + id: string; + packageName: string; + packageRef: string; + createdAt: string; + parentStateContextId: string | null; + namespace: string | null; + kind: "root" | "derived"; +}; + +export type ControlPlaneBrowserAuthMessage = { + type: "auth"; + secret: string; +}; + +export type ControlPlaneBrowserOpenRootContextParams = { + target: string; +}; + +export type ControlPlaneBrowserOpenDerivedContextParams = { + target: string; + targetPackageName?: string; + callerStateContextId: string; + contextNamespace?: string | null; +}; + +export type ControlPlaneBrowserCloseStateContextParams = { + stateContextId: string; +}; + +export type ControlPlaneBrowserCallParams = { + target: string; + functionName: string; + params: unknown; + stateContextId: string; +}; + +export type ControlPlaneBrowserValueGetParams = { + target: string; + valueName: string; + params: unknown; + stateContextId: string; +}; + +export type ControlPlaneBrowserSubscribeParams = { + target: string; + resourceName: string; + params: unknown; + stateContextId: string; + subscriptionNamespace?: string | null; +}; + +export type ControlPlaneBrowserUnsubscribeParams = { + subscriptionId: string; +}; + +export type ControlPlaneBrowserSubscriptionReadyParams = { + subscriptionId: string; +}; + +export type ControlPlaneBrowserRequestMessage = + | { + type: "request"; + requestId: string; + method: "open-root-context"; + params: ControlPlaneBrowserOpenRootContextParams; + } + | { + type: "request"; + requestId: string; + method: "open-derived-context"; + params: ControlPlaneBrowserOpenDerivedContextParams; + } + | { + type: "request"; + requestId: string; + method: "close-state-context"; + params: ControlPlaneBrowserCloseStateContextParams; + } + | { + type: "request"; + requestId: string; + method: "call"; + params: ControlPlaneBrowserCallParams; + } + | { + type: "request"; + requestId: string; + method: "value-get"; + params: ControlPlaneBrowserValueGetParams; + } + | { + type: "request"; + requestId: string; + method: "event-subscribe"; + params: ControlPlaneBrowserSubscribeParams; + } + | { + type: "request"; + requestId: string; + method: "value-watch"; + params: ControlPlaneBrowserSubscribeParams; + } + | { + type: "request"; + requestId: string; + method: "unsubscribe"; + params: ControlPlaneBrowserUnsubscribeParams; + } + | { + type: "request"; + requestId: string; + method: "subscription-ready"; + params: ControlPlaneBrowserSubscriptionReadyParams; + }; + +export type ControlPlaneBrowserClientMessage = + | ControlPlaneBrowserAuthMessage + | ControlPlaneBrowserRequestMessage; + +export type ControlPlaneBrowserResponseMessage = { + type: "response"; + requestId: string; + ok: boolean; + result?: unknown; + error?: string; +}; + +export type ControlPlaneBrowserSubscriptionDataMessage = { + type: "subscription-data"; + subscriptionId: string; + data: unknown; +}; + +export type ControlPlaneBrowserAuthAckMessage = { + type: "auth-ack"; +}; + +export type ControlPlaneBrowserStatusInvalidatedMessage = { + type: "status-invalidated"; + reason: + | "state-context-opened" + | "state-context-closed" + | "running-packages-changed" + | "repl-sessions-changed" + | "runtime-errors-changed" + | "devices-changed"; +}; + +export type ControlPlaneDeviceSocketConnectMessage = { + type: "device-connect"; + deviceId: string; + requestedTarget?: ControlPlaneDeviceTarget | null; + userAgent?: string | null; + viewport?: ControlPlaneDeviceViewport | null; +}; + +export type ControlPlaneDeviceSocketUpdateMessage = { + type: "device-update"; + viewport?: ControlPlaneDeviceViewport | null; +}; + +export type ControlPlaneDeviceSocketClientMessage = + | ControlPlaneDeviceSocketConnectMessage + | ControlPlaneDeviceSocketUpdateMessage; + +export type ControlPlaneDeviceSocketReadyMessage = { + type: "device-ready"; + device: ControlPlaneDevice; +}; + +export type ControlPlaneDeviceSocketStateMessage = { + type: "device-state"; + device: ControlPlaneDevice; +}; + +export type ControlPlaneDeviceSocketRemovedMessage = { + type: "device-removed"; + deviceId: string; +}; + +export type ControlPlaneDeviceSocketServerMessage = + | ControlPlaneDeviceSocketReadyMessage + | ControlPlaneDeviceSocketStateMessage + | ControlPlaneDeviceSocketRemovedMessage; + +export type ControlPlanePackageLogEntry = { + id: string; + createdAt: string; + packageName: string; + label: string; + processId: number | null; + stream: "stdout" | "stderr"; + text: string; +}; + +export type ControlPlaneRuntimeErrorPhase = + | "on-create" + | "on-destroy" + | "on-context-open" + | "on-context-close" + | "browser-surface" + | "function" + | "event-subscribe" + | "event-cleanup" + | "value-get" + | "value-watch" + | "value-cleanup" + | "subscription-publish" + | "internal"; + +export type ControlPlanePackageErrorEntry = { + id: string; + createdAt: string; + packageName: string; + label: string | null; + processId: number | null; + stateContextId: string | null; + phase: ControlPlaneRuntimeErrorPhase; + functionName: string | null; + resourceKind: "event" | "value" | null; + resourceName: string | null; + surfaceId: string | null; + hostSessionId: string | null; + message: string; + stack: string | null; + acknowledgedAt: string | null; +}; + +export type ControlPlaneLogsResponse = { + packageName: string; + entries: ControlPlanePackageLogEntry[]; +}; + +export type ControlPlaneErrorsResponse = { + entries: ControlPlanePackageErrorEntry[]; + acknowledgedCount: number; +}; + +export type ControlPlaneReplSession = { + sessionId: string; + createdAt: string; + lastActivityAt: string; + ownedStateContextIds: string[]; + borrowedStateContextIds: string[]; + subscriptionCount: number; + tempDirectory: string; +}; + +export type ControlPlaneBrowserServerMessage = + | ControlPlaneBrowserAuthAckMessage + | ControlPlaneBrowserStatusInvalidatedMessage + | ControlPlaneBrowserResponseMessage + | ControlPlaneBrowserSubscriptionDataMessage; + +export type ControlPlaneDependencyInput = { + inputName: string; + targetNodeId: string; +}; + +export type ControlPlaneDependencyNode = { + nodeId: string; + label: string; + originalRef: string | null; + lockedRef: string | null; + version: string | null; + inputs: ControlPlaneDependencyInput[]; +}; + +export type ControlPlaneDependencyGraph = { + rootNodeId: string; + nodes: ControlPlaneDependencyNode[]; +}; + +export type ControlPlaneRunningPackage = { + key: string; + label: string; + packageName: string; + flakeRef: string; + requestedRef: string | null; + connected: boolean; + processId: number | null; + openedStateContextIds: string[]; + rootStateContextIds: string[]; + resolvedRef: string | null; + version: string | null; + unreadErrorCount: number; + lastErrorAt: string | null; + dependencyGraph: ControlPlaneDependencyGraph | null; + dependencyGraphError: string | null; +}; + +export type ControlPlaneCheckStatus = "running" | "passed" | "failed"; + +export type ControlPlaneCheckResult = { + packageName: string; + rev: string; + status: ControlPlaneCheckStatus; + startedAt: string; + finishedAt: string | null; + summary: string | null; + logExcerpt: string | null; +}; + +export type ControlPlaneSchemaResourceDescription = { + name: string; + description: string | null; + annotations: Array & { type: string }>; + takesParams: boolean; + inputDescription: string | null; + inputJsonSchema: Record | null; + outputDescription: string | null; + outputJsonSchema: Record | null; + paramsDescription: string | null; + paramsJsonSchema: Record | null; + dataDescription: string | null; + dataJsonSchema: Record | null; + history?: boolean; + payloadMode: string | null; +}; + +export type ControlPlanePackageSchemaDescription = { + packageName: string; + schemaPackageName: string; + description: string | null; + annotations: Array & { type: string }>; + functions: ControlPlaneSchemaResourceDescription[]; + events: ControlPlaneSchemaResourceDescription[]; + values: ControlPlaneSchemaResourceDescription[]; +}; + +export type ControlPlaneSchemaDescriptionResult = { + packageName: string; + rev: string; + generatedAt: string; + status: "available" | "unavailable"; + data: ControlPlanePackageSchemaDescription | null; + error: string | null; +}; + +export type ControlPlaneRunResponse = { + stateContextId: string; + package: ControlPlaneRunningPackage; +}; + +export type ControlPlaneClearStateResponse = { + packageNames: string[]; + clearedDirectories: string[]; + packages: ControlPlaneRunningPackage[]; +}; + +export type ControlPlaneReplLogEntry = { + level: "log" | "info" | "warn" | "error"; + text: string; +}; + +export type ControlPlaneReplEvalResponse = { + session: ControlPlaneReplSession; + created: boolean; + closed: boolean; + ok: boolean; + result: string | null; + logs: ControlPlaneReplLogEntry[]; + error: string | null; +}; + +export type ControlPlaneReplCloseResponse = { + sessionId: string; + closed: boolean; +}; + +export type ControlPlaneRemoteDomPayload = { + protocol: string; + revision: number; + baseRevision: number | null; + fromEmpty: boolean; + mutations: unknown[]; +}; + +export type ControlPlaneStatusResponse = { + generatedAt: string; + workspacePackages: ControlPlaneWorkspacePackage[]; + stateContexts: ControlPlaneStateContext[]; + replSessions: ControlPlaneReplSession[]; + devices: ControlPlaneDevice[]; + packages: ControlPlaneRunningPackage[]; + checks: ControlPlaneCheckResult[]; + descriptions: ControlPlaneSchemaDescriptionResult[]; +}; + +export type ControlPlaneCutoverResponse = { + packages: ControlPlaneRunningPackage[]; +}; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +const isString = (value: unknown): value is string => typeof value === "string"; + +const isNullableString = (value: unknown): value is string | null => + value === null || typeof value === "string"; + +const isNullableRecord = ( + value: unknown, +): value is Record | null => + value === null || isRecord(value); + +const isPositiveInteger = (value: unknown): value is number => + typeof value === "number" && Number.isInteger(value) && value > 0; + +const isRuntimeErrorPhase = ( + value: unknown, +): value is ControlPlaneRuntimeErrorPhase => + value === "on-create" || + value === "on-destroy" || + value === "on-context-open" || + value === "on-context-close" || + value === "browser-surface" || + value === "function" || + value === "event-subscribe" || + value === "event-cleanup" || + value === "value-get" || + value === "value-watch" || + value === "value-cleanup" || + value === "subscription-publish" || + value === "internal"; + +const isSchemaAnnotation = ( + value: unknown, +): value is Record & { type: string } => + isRecord(value) && isString(value.type); + +const isSchemaAnnotationArray = ( + value: unknown, +): value is Array & { type: string }> => + Array.isArray(value) && value.every((item) => isSchemaAnnotation(item)); + +const isStringArray = (value: unknown): value is string[] => + Array.isArray(value) && value.every((item) => typeof item === "string"); + +const isCheckStatus = (value: unknown): value is ControlPlaneCheckStatus => + value === "running" || value === "passed" || value === "failed"; + +const isPositiveNumber = (value: unknown): value is number => + typeof value === "number" && Number.isFinite(value) && value > 0; + +const parseControlPlaneDeviceViewport = ( + value: unknown, +): ControlPlaneDeviceViewport | null => { + if ( + !isRecord(value) || + !isPositiveNumber(value.width) || + !isPositiveNumber(value.height) || + !( + value.pixelRatio === undefined || + value.pixelRatio === null || + isPositiveNumber(value.pixelRatio) + ) + ) { + return null; + } + return { + width: value.width, + height: value.height, + pixelRatio: value.pixelRatio ?? null, + }; +}; + +export const parseControlPlaneDeviceTarget = ( + value: unknown, +): ControlPlaneDeviceTarget | null => { + if ( + !isRecord(value) || + !isNonEmptyString(value.packageName) || + !isNonEmptyString(value.stateContextId) || + !isNonEmptyString(value.surfaceId) || + !( + value.requestFullscreen === undefined || + typeof value.requestFullscreen === "boolean" + ) + ) { + return null; + } + return { + packageName: value.packageName, + stateContextId: value.stateContextId, + surfaceId: value.surfaceId, + requestFullscreen: value.requestFullscreen, + }; +}; + +export const parseControlPlaneDevice = ( + value: unknown, +): ControlPlaneDevice | null => { + if ( + !isRecord(value) || + !isNonEmptyString(value.deviceId) || + !isNullableString(value.name) || + !isString(value.createdAt) || + !isNullableString(value.lastSeenAt) || + typeof value.connected !== "boolean" || + !isNullableString(value.identifyUntil) || + !isNullableString(value.lastUserAgent) + ) { + return null; + } + const assignment = + value.assignment === undefined || value.assignment === null + ? null + : parseControlPlaneDeviceTarget(value.assignment); + if (value.assignment !== undefined && value.assignment !== null && !assignment) { + return null; + } + const configuredAssignment = + value.configuredAssignment === undefined || value.configuredAssignment === null + ? null + : parseControlPlaneDeviceTarget(value.configuredAssignment); + if ( + value.configuredAssignment !== undefined && + value.configuredAssignment !== null && + !configuredAssignment + ) { + return null; + } + const viewport = + value.viewport === undefined || value.viewport === null + ? null + : parseControlPlaneDeviceViewport(value.viewport); + if (value.viewport !== undefined && value.viewport !== null && !viewport) { + return null; + } + return { + deviceId: value.deviceId, + name: value.name, + createdAt: value.createdAt, + lastSeenAt: value.lastSeenAt, + connected: value.connected, + assignment, + configuredAssignment, + identifyUntil: value.identifyUntil, + lastUserAgent: value.lastUserAgent, + viewport, + }; +}; + +export const parseControlPlaneRunRequest = ( + value: unknown, +): ControlPlaneRunRequest | null => { + if (!isRecord(value) || !isString(value.target) || value.target.length === 0) { + return null; + } + return { + target: value.target, + }; +}; + +export const parseControlPlaneCheckRequest = ( + value: unknown, +): ControlPlaneCheckRequest | null => { + if ( + !isRecord(value) || + !isString(value.packageName) || + value.packageName.length === 0 || + !isString(value.rev) || + value.rev.length === 0 + ) { + return null; + } + return { + packageName: value.packageName, + rev: value.rev, + }; +}; + +export const parseControlPlaneCutoverRequest = ( + value: unknown, +): ControlPlaneCutoverRequest | null => { + if ( + !isRecord(value) || + !Array.isArray(value.targets) || + !value.targets.every( + (target) => typeof target === "string" && target.length > 0, + ) + ) { + return null; + } + return { + targets: value.targets, + }; +}; + +export const parseControlPlaneClearStateRequest = ( + value: unknown, +): ControlPlaneClearStateRequest | null => { + if (!isRecord(value) || !isString(value.packageName)) { + return null; + } + if ( + value.recursive !== undefined && + typeof value.recursive !== "boolean" + ) { + return null; + } + return { + packageName: value.packageName, + recursive: value.recursive, + }; +}; + +export const parseControlPlaneDescribeRequest = ( + value: unknown, +): ControlPlaneDescribeRequest | null => { + if ( + !isRecord(value) || + !isString(value.packageName) || + value.packageName.length === 0 || + !isString(value.rev) || + value.rev.length === 0 + ) { + return null; + } + return { + packageName: value.packageName, + rev: value.rev, + }; +}; + +export const parseControlPlaneLogsRequest = ( + value: unknown, +): ControlPlaneLogsRequest | null => { + if (!isRecord(value) || !isString(value.packageName)) { + return null; + } + if ( + !( + value.tail === undefined || + value.tail === null || + isPositiveInteger(value.tail) + ) || + !( + value.stream === undefined || + value.stream === "stdout" || + value.stream === "stderr" || + value.stream === "all" + ) + ) { + return null; + } + return { + packageName: value.packageName, + tail: value.tail ?? undefined, + stream: value.stream ?? undefined, + }; +}; + +export const parseControlPlaneErrorsRequest = ( + value: unknown, +): ControlPlaneErrorsRequest | null => { + if (!isRecord(value)) { + return null; + } + if ( + !( + value.packageName === undefined || + value.packageName === null || + isString(value.packageName) + ) || + !( + value.stateContextId === undefined || + value.stateContextId === null || + isString(value.stateContextId) + ) || + !( + value.tail === undefined || + value.tail === null || + isPositiveInteger(value.tail) + ) || + !( + value.acknowledge === undefined || + typeof value.acknowledge === "boolean" + ) + ) { + return null; + } + return { + packageName: value.packageName ?? undefined, + stateContextId: value.stateContextId ?? undefined, + tail: value.tail ?? undefined, + acknowledge: value.acknowledge ?? undefined, + }; +}; + +export const parseControlPlaneDeviceAssignRequest = ( + value: unknown, +): ControlPlaneDeviceAssignRequest | null => { + if (!isRecord(value) || !isNonEmptyString(value.deviceId)) { + return null; + } + const target = + value.target === undefined || value.target === null + ? null + : parseControlPlaneDeviceTarget(value.target); + if (value.target !== undefined && value.target !== null && !target) { + return null; + } + return { + deviceId: value.deviceId, + target, + }; +}; + +export const parseControlPlaneDeviceRenameRequest = ( + value: unknown, +): ControlPlaneDeviceRenameRequest | null => { + if ( + !isRecord(value) || + !isNonEmptyString(value.deviceId) || + !( + value.name === undefined || + value.name === null || + typeof value.name === "string" + ) + ) { + return null; + } + return { + deviceId: value.deviceId, + name: value.name ?? null, + }; +}; + +export const parseControlPlaneDeviceRemoveRequest = ( + value: unknown, +): ControlPlaneDeviceRemoveRequest | null => { + if (!isRecord(value) || !isNonEmptyString(value.deviceId)) { + return null; + } + return { + deviceId: value.deviceId, + }; +}; + +export const parseControlPlaneDeviceIdentifyRequest = ( + value: unknown, +): ControlPlaneDeviceIdentifyRequest | null => { + if ( + !isRecord(value) || + !isNonEmptyString(value.deviceId) || + !( + value.durationMs === undefined || + (typeof value.durationMs === "number" && + Number.isInteger(value.durationMs) && + value.durationMs > 0) + ) + ) { + return null; + } + return { + deviceId: value.deviceId, + durationMs: value.durationMs, + }; +}; + +export const parseControlPlaneDeviceSessionRequest = ( + value: unknown, +): ControlPlaneDeviceSessionRequest | null => { + if ( + !isRecord(value) || + !isNonEmptyString(value.deviceId) || + !( + value.userAgent === undefined || + value.userAgent === null || + typeof value.userAgent === "string" + ) + ) { + return null; + } + const requestedTarget = + value.requestedTarget === undefined + ? undefined + : value.requestedTarget === null + ? null + : parseControlPlaneDeviceTarget(value.requestedTarget); + if ( + value.requestedTarget !== undefined && + value.requestedTarget !== null && + !requestedTarget + ) { + return null; + } + const viewport = + value.viewport === undefined || value.viewport === null + ? null + : parseControlPlaneDeviceViewport(value.viewport); + if (value.viewport !== undefined && value.viewport !== null && !viewport) { + return null; + } + return { + deviceId: value.deviceId, + ...(requestedTarget !== undefined ? { requestedTarget } : {}), + userAgent: value.userAgent ?? null, + viewport, + }; +}; + +export const parseControlPlaneReplEvalRequest = ( + value: unknown, +): ControlPlaneReplEvalRequest | null => { + if (!isRecord(value) || !isNonEmptyString(value.code)) { + return null; + } + if ( + !( + value.sessionId === undefined || + isNonEmptyString(value.sessionId) + ) || + !( + value.autoClose === undefined || + typeof value.autoClose === "boolean" + ) || + !( + value.defaultStateContextId === undefined || + value.defaultStateContextId === null || + isNonEmptyString(value.defaultStateContextId) + ) + ) { + return null; + } + return { + sessionId: value.sessionId, + code: value.code, + autoClose: value.autoClose, + defaultStateContextId: value.defaultStateContextId ?? null, + }; +}; + +export const parseControlPlaneReplCloseRequest = ( + value: unknown, +): ControlPlaneReplCloseRequest | null => { + if (!isRecord(value) || !isNonEmptyString(value.sessionId)) { + return null; + } + return { + sessionId: value.sessionId, + }; +}; + +const isNonEmptyString = (value: unknown): value is string => + typeof value === "string" && value.length > 0; + +const parseBrowserSubscribeParams = ( + value: unknown, +): ControlPlaneBrowserSubscribeParams | null => { + if ( + !isRecord(value) || + !isNonEmptyString(value.target) || + !isNonEmptyString(value.resourceName) || + !isNonEmptyString(value.stateContextId) || + !( + value.subscriptionNamespace === undefined || + value.subscriptionNamespace === null || + typeof value.subscriptionNamespace === "string" + ) + ) { + return null; + } + return { + target: value.target, + resourceName: value.resourceName, + params: value.params, + stateContextId: value.stateContextId, + subscriptionNamespace: value.subscriptionNamespace ?? null, + }; +}; + +export const parseControlPlaneDeviceSocketClientMessage = ( + value: unknown, +): ControlPlaneDeviceSocketClientMessage | null => { + if (!isRecord(value) || !isNonEmptyString(value.type)) { + return null; + } + if (value.type === "device-connect") { + const parsed = parseControlPlaneDeviceSessionRequest(value); + if (!parsed) { + return null; + } + return { + type: "device-connect", + deviceId: parsed.deviceId, + ...(parsed.requestedTarget !== undefined + ? { requestedTarget: parsed.requestedTarget } + : {}), + ...(parsed.userAgent !== undefined ? { userAgent: parsed.userAgent } : {}), + ...(parsed.viewport !== undefined ? { viewport: parsed.viewport } : {}), + }; + } + if (value.type === "device-update") { + const viewport = + value.viewport === undefined || value.viewport === null + ? value.viewport + : parseControlPlaneDeviceViewport(value.viewport); + if (value.viewport !== undefined && value.viewport !== null && !viewport) { + return null; + } + return { + type: "device-update", + ...(value.viewport !== undefined ? { viewport: viewport ?? null } : {}), + }; + } + return null; +}; + +export const parseControlPlaneDeviceSocketServerMessage = ( + value: unknown, +): ControlPlaneDeviceSocketServerMessage | null => { + if (!isRecord(value) || !isNonEmptyString(value.type)) { + return null; + } + if (value.type === "device-removed") { + if (!isNonEmptyString(value.deviceId)) { + return null; + } + return { + type: "device-removed", + deviceId: value.deviceId, + }; + } + if (value.type !== "device-ready" && value.type !== "device-state") { + return null; + } + const device = parseControlPlaneDevice(value.device); + if (!device) { + return null; + } + return { + type: value.type, + device, + }; +}; + +export const parseControlPlaneBrowserClientMessage = ( + value: unknown, +): ControlPlaneBrowserClientMessage | null => { + if (!isRecord(value) || !isNonEmptyString(value.type)) { + return null; + } + + if (value.type === "auth") { + if (!isNonEmptyString(value.secret)) { + return null; + } + return { + type: "auth", + secret: value.secret, + }; + } + + if (value.type !== "request") { + return null; + } + if (!isNonEmptyString(value.requestId) || !isNonEmptyString(value.method)) { + return null; + } + + switch (value.method) { + case "open-root-context": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.target) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "open-root-context", + params: { + target: value.params.target, + }, + }; + case "open-derived-context": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.target) || + !isNonEmptyString(value.params.callerStateContextId) || + !( + value.params.targetPackageName === undefined || + isNonEmptyString(value.params.targetPackageName) + ) || + !( + value.params.contextNamespace === undefined || + value.params.contextNamespace === null || + isNonEmptyString(value.params.contextNamespace) + ) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "open-derived-context", + params: { + target: value.params.target, + targetPackageName: value.params.targetPackageName, + callerStateContextId: value.params.callerStateContextId, + contextNamespace: value.params.contextNamespace ?? null, + }, + }; + case "close-state-context": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.stateContextId) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "close-state-context", + params: { + stateContextId: value.params.stateContextId, + }, + }; + case "call": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.target) || + !isNonEmptyString(value.params.functionName) || + !isNonEmptyString(value.params.stateContextId) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "call", + params: { + target: value.params.target, + functionName: value.params.functionName, + params: value.params.params, + stateContextId: value.params.stateContextId, + }, + }; + case "value-get": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.target) || + !isNonEmptyString(value.params.valueName) || + !isNonEmptyString(value.params.stateContextId) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "value-get", + params: { + target: value.params.target, + valueName: value.params.valueName, + params: value.params.params, + stateContextId: value.params.stateContextId, + }, + }; + case "event-subscribe": { + const params = parseBrowserSubscribeParams(value.params); + if (!params) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "event-subscribe", + params, + }; + } + case "value-watch": { + const params = parseBrowserSubscribeParams(value.params); + if (!params) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "value-watch", + params, + }; + } + case "unsubscribe": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.subscriptionId) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "unsubscribe", + params: { + subscriptionId: value.params.subscriptionId, + }, + }; + case "subscription-ready": + if ( + !isRecord(value.params) || + !isNonEmptyString(value.params.subscriptionId) + ) { + return null; + } + return { + type: "request", + requestId: value.requestId, + method: "subscription-ready", + params: { + subscriptionId: value.params.subscriptionId, + }, + }; + default: + return null; + } +}; + +const parseDependencyInput = ( + value: unknown, +): ControlPlaneDependencyInput | null => { + if (!isRecord(value)) { + return null; + } + if (!isString(value.inputName) || !isString(value.targetNodeId)) { + return null; + } + return { + inputName: value.inputName, + targetNodeId: value.targetNodeId, + }; +}; + +const parseDependencyNode = ( + value: unknown, +): ControlPlaneDependencyNode | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.nodeId) || + !isString(value.label) || + !isNullableString(value.originalRef) || + !isNullableString(value.lockedRef) || + !isNullableString(value.version) || + !Array.isArray(value.inputs) + ) { + return null; + } + + const inputs = value.inputs + .map((input) => parseDependencyInput(input)) + .filter((input): input is ControlPlaneDependencyInput => input !== null); + if (inputs.length !== value.inputs.length) { + return null; + } + + return { + nodeId: value.nodeId, + label: value.label, + originalRef: value.originalRef, + lockedRef: value.lockedRef, + version: value.version, + inputs, + }; +}; + +const parseControlPlaneWorkspacePackage = ( + value: unknown, +): ControlPlaneWorkspacePackage | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.packageName) || + !isString(value.headCommit) || + typeof value.ignored !== "boolean" + ) { + return null; + } + return { + packageName: value.packageName, + headCommit: value.headCommit, + ignored: value.ignored, + }; +}; + +const parseControlPlaneReplLogEntry = ( + value: unknown, +): ControlPlaneReplLogEntry | null => { + if ( + !isRecord(value) || + !( + value.level === "log" || + value.level === "info" || + value.level === "warn" || + value.level === "error" + ) || + !isString(value.text) + ) { + return null; + } + return { + level: value.level, + text: value.text, + }; +}; + +export const parseControlPlaneReplSession = ( + value: unknown, +): ControlPlaneReplSession | null => { + if ( + !isRecord(value) || + !isString(value.sessionId) || + !isString(value.createdAt) || + !isString(value.lastActivityAt) || + !isStringArray(value.ownedStateContextIds) || + !isStringArray(value.borrowedStateContextIds) || + typeof value.subscriptionCount !== "number" || + !isString(value.tempDirectory) + ) { + return null; + } + return { + sessionId: value.sessionId, + createdAt: value.createdAt, + lastActivityAt: value.lastActivityAt, + ownedStateContextIds: value.ownedStateContextIds, + borrowedStateContextIds: value.borrowedStateContextIds, + subscriptionCount: value.subscriptionCount, + tempDirectory: value.tempDirectory, + }; +}; + +const parseDependencyGraph = ( + value: unknown, +): ControlPlaneDependencyGraph | null => { + if (!isRecord(value)) { + return null; + } + if (!isString(value.rootNodeId) || !Array.isArray(value.nodes)) { + return null; + } + const nodes = value.nodes + .map((node) => parseDependencyNode(node)) + .filter((node): node is ControlPlaneDependencyNode => node !== null); + if (nodes.length !== value.nodes.length) { + return null; + } + return { + rootNodeId: value.rootNodeId, + nodes, + }; +}; + +export const parseControlPlaneRunningPackage = ( + value: unknown, +): ControlPlaneRunningPackage | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.key) || + !isString(value.label) || + !isString(value.packageName) || + !isString(value.flakeRef) || + !isNullableString(value.requestedRef) || + typeof value.connected !== "boolean" || + !(typeof value.processId === "number" || value.processId === null) || + !isStringArray(value.openedStateContextIds) || + !isStringArray(value.rootStateContextIds) || + !isNullableString(value.resolvedRef) || + !isNullableString(value.version) || + !( + typeof value.unreadErrorCount === "number" && + Number.isInteger(value.unreadErrorCount) && + value.unreadErrorCount >= 0 + ) || + !isNullableString(value.lastErrorAt) || + !isNullableString(value.dependencyGraphError) + ) { + return null; + } + + const dependencyGraph = + value.dependencyGraph === null + ? null + : parseDependencyGraph(value.dependencyGraph); + if (value.dependencyGraph !== null && !dependencyGraph) { + return null; + } + + return { + key: value.key, + label: value.label, + packageName: value.packageName, + flakeRef: value.flakeRef, + requestedRef: value.requestedRef, + connected: value.connected, + processId: value.processId, + openedStateContextIds: value.openedStateContextIds, + rootStateContextIds: value.rootStateContextIds, + resolvedRef: value.resolvedRef, + version: value.version, + unreadErrorCount: value.unreadErrorCount, + lastErrorAt: value.lastErrorAt, + dependencyGraph, + dependencyGraphError: value.dependencyGraphError, + }; +}; + +export const parseControlPlanePackageLogEntry = ( + value: unknown, +): ControlPlanePackageLogEntry | null => { + if ( + !isRecord(value) || + !isString(value.id) || + !isString(value.createdAt) || + !isString(value.packageName) || + !isString(value.label) || + !(typeof value.processId === "number" || value.processId === null) || + !(value.stream === "stdout" || value.stream === "stderr") || + !isString(value.text) + ) { + return null; + } + return { + id: value.id, + createdAt: value.createdAt, + packageName: value.packageName, + label: value.label, + processId: value.processId, + stream: value.stream, + text: value.text, + }; +}; + +export const parseControlPlanePackageErrorEntry = ( + value: unknown, +): ControlPlanePackageErrorEntry | null => { + if ( + !isRecord(value) || + !isString(value.id) || + !isString(value.createdAt) || + !isString(value.packageName) || + !isNullableString(value.label) || + !(typeof value.processId === "number" || value.processId === null) || + !isNullableString(value.stateContextId) || + !isRuntimeErrorPhase(value.phase) || + !isNullableString(value.functionName) || + !( + value.resourceKind === "event" || + value.resourceKind === "value" || + value.resourceKind === null + ) || + !isNullableString(value.resourceName) || + !( + value.surfaceId === undefined || + isNullableString(value.surfaceId) + ) || + !( + value.hostSessionId === undefined || + isNullableString(value.hostSessionId) + ) || + !isString(value.message) || + !isNullableString(value.stack) || + !isNullableString(value.acknowledgedAt) + ) { + return null; + } + return { + id: value.id, + createdAt: value.createdAt, + packageName: value.packageName, + label: value.label, + processId: value.processId, + stateContextId: value.stateContextId, + phase: value.phase, + functionName: value.functionName, + resourceKind: value.resourceKind, + resourceName: value.resourceName, + surfaceId: value.surfaceId ?? null, + hostSessionId: value.hostSessionId ?? null, + message: value.message, + stack: value.stack, + acknowledgedAt: value.acknowledgedAt, + }; +}; + +export const parseControlPlaneStateContext = ( + value: unknown, +): ControlPlaneStateContext | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.id) || + !isString(value.packageName) || + !isString(value.packageRef) || + !isString(value.createdAt) || + !isNullableString(value.parentStateContextId) || + !isNullableString(value.namespace) || + !(value.kind === "root" || value.kind === "derived") + ) { + return null; + } + return { + id: value.id, + packageName: value.packageName, + packageRef: value.packageRef, + createdAt: value.createdAt, + parentStateContextId: value.parentStateContextId, + namespace: value.namespace, + kind: value.kind, + }; +}; + +export const parseControlPlaneCheckResult = ( + value: unknown, +): ControlPlaneCheckResult | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.packageName) || + !isString(value.rev) || + !isCheckStatus(value.status) || + !isString(value.startedAt) || + !isNullableString(value.finishedAt) || + !isNullableString(value.summary) || + !isNullableString(value.logExcerpt) + ) { + return null; + } + return { + packageName: value.packageName, + rev: value.rev, + status: value.status, + startedAt: value.startedAt, + finishedAt: value.finishedAt, + summary: value.summary, + logExcerpt: value.logExcerpt, + }; +}; + +const parseControlPlaneSchemaResourceDescription = ( + value: unknown, +): ControlPlaneSchemaResourceDescription | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.name) || + !isNullableString(value.description) || + !(value.annotations === undefined || isSchemaAnnotationArray(value.annotations)) || + typeof value.takesParams !== "boolean" || + !isNullableString(value.inputDescription) || + !(value.inputJsonSchema === undefined || isNullableRecord(value.inputJsonSchema)) || + !isNullableString(value.outputDescription) || + !(value.outputJsonSchema === undefined || isNullableRecord(value.outputJsonSchema)) || + !isNullableString(value.paramsDescription) || + !(value.paramsJsonSchema === undefined || isNullableRecord(value.paramsJsonSchema)) || + !isNullableString(value.dataDescription) || + !(value.dataJsonSchema === undefined || isNullableRecord(value.dataJsonSchema)) || + !(typeof value.history === "boolean" || value.history === undefined) || + !isNullableString(value.payloadMode) + ) { + return null; + } + return { + name: value.name, + description: value.description, + annotations: value.annotations ?? [], + takesParams: value.takesParams, + inputDescription: value.inputDescription, + inputJsonSchema: value.inputJsonSchema ?? null, + outputDescription: value.outputDescription, + outputJsonSchema: value.outputJsonSchema ?? null, + paramsDescription: value.paramsDescription, + paramsJsonSchema: value.paramsJsonSchema ?? null, + dataDescription: value.dataDescription, + dataJsonSchema: value.dataJsonSchema ?? null, + history: value.history, + payloadMode: value.payloadMode, + }; +}; + +const parseControlPlanePackageSchemaDescription = ( + value: unknown, +): ControlPlanePackageSchemaDescription | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.packageName) || + !isString(value.schemaPackageName) || + !isNullableString(value.description) || + !(value.annotations === undefined || isSchemaAnnotationArray(value.annotations)) || + !Array.isArray(value.functions) || + !Array.isArray(value.events) || + !Array.isArray(value.values) + ) { + return null; + } + const functions = value.functions + .map((entry) => parseControlPlaneSchemaResourceDescription(entry)) + .filter( + (entry): entry is ControlPlaneSchemaResourceDescription => entry !== null, + ); + const events = value.events + .map((entry) => parseControlPlaneSchemaResourceDescription(entry)) + .filter( + (entry): entry is ControlPlaneSchemaResourceDescription => entry !== null, + ); + const values = value.values + .map((entry) => parseControlPlaneSchemaResourceDescription(entry)) + .filter( + (entry): entry is ControlPlaneSchemaResourceDescription => entry !== null, + ); + if ( + functions.length !== value.functions.length || + events.length !== value.events.length || + values.length !== value.values.length + ) { + return null; + } + return { + packageName: value.packageName, + schemaPackageName: value.schemaPackageName, + description: value.description, + annotations: value.annotations ?? [], + functions, + events, + values, + }; +}; + +export const parseControlPlaneSchemaDescriptionResult = ( + value: unknown, +): ControlPlaneSchemaDescriptionResult | null => { + if (!isRecord(value)) { + return null; + } + if ( + !isString(value.packageName) || + !isString(value.rev) || + !isString(value.generatedAt) || + !(value.status === "available" || value.status === "unavailable") || + !isNullableString(value.error) + ) { + return null; + } + const data = + value.data === null ? null : parseControlPlanePackageSchemaDescription(value.data); + if (value.data !== null && !data) { + return null; + } + return { + packageName: value.packageName, + rev: value.rev, + generatedAt: value.generatedAt, + status: value.status, + data, + error: value.error, + }; +}; + +export const parseControlPlaneRunResponse = ( + value: unknown, +): ControlPlaneRunResponse | null => { + if (!isRecord(value) || !isString(value.stateContextId)) { + return null; + } + const parsedPackage = parseControlPlaneRunningPackage(value.package); + if (!parsedPackage) { + return null; + } + return { + stateContextId: value.stateContextId, + package: parsedPackage, + }; +}; + +export const parseControlPlaneStatusResponse = ( + value: unknown, +): ControlPlaneStatusResponse | null => { + if ( + !isRecord(value) || + !isString(value.generatedAt) || + !( + value.workspacePackages === undefined || + Array.isArray(value.workspacePackages) + ) || + !( + value.stateContexts === undefined || + Array.isArray(value.stateContexts) + ) || + !( + value.replSessions === undefined || + Array.isArray(value.replSessions) + ) || + !( + value.devices === undefined || + Array.isArray(value.devices) + ) || + !Array.isArray(value.packages) || + !Array.isArray(value.checks) || + !Array.isArray(value.descriptions) + ) { + return null; + } + const workspacePackages = (value.workspacePackages ?? []) + .map((pkg) => parseControlPlaneWorkspacePackage(pkg)) + .filter((pkg): pkg is ControlPlaneWorkspacePackage => pkg !== null); + if (workspacePackages.length !== (value.workspacePackages ?? []).length) { + return null; + } + const stateContexts = (value.stateContexts ?? []) + .map((context) => parseControlPlaneStateContext(context)) + .filter((context): context is ControlPlaneStateContext => context !== null); + if (stateContexts.length !== (value.stateContexts ?? []).length) { + return null; + } + const replSessions = (value.replSessions ?? []) + .map((session) => parseControlPlaneReplSession(session)) + .filter((session): session is ControlPlaneReplSession => session !== null); + if (replSessions.length !== (value.replSessions ?? []).length) { + return null; + } + const devices = (value.devices ?? []) + .map((device) => parseControlPlaneDevice(device)) + .filter((device): device is ControlPlaneDevice => device !== null); + if (devices.length !== (value.devices ?? []).length) { + return null; + } + const packages = value.packages + .map((pkg) => parseControlPlaneRunningPackage(pkg)) + .filter((pkg): pkg is ControlPlaneRunningPackage => pkg !== null); + if (packages.length !== value.packages.length) { + return null; + } + const checks = value.checks + .map((result) => parseControlPlaneCheckResult(result)) + .filter((result): result is ControlPlaneCheckResult => result !== null); + if (checks.length !== value.checks.length) { + return null; + } + const descriptions = value.descriptions + .map((entry) => parseControlPlaneSchemaDescriptionResult(entry)) + .filter( + (entry): entry is ControlPlaneSchemaDescriptionResult => entry !== null, + ); + if (descriptions.length !== value.descriptions.length) { + return null; + } + return { + generatedAt: value.generatedAt, + workspacePackages, + stateContexts, + replSessions, + devices, + packages, + checks, + descriptions, + }; +}; + +export const parseControlPlaneCutoverResponse = ( + value: unknown, +): ControlPlaneCutoverResponse | null => { + if (!isRecord(value) || !Array.isArray(value.packages)) { + return null; + } + const packages = value.packages + .map((pkg) => parseControlPlaneRunningPackage(pkg)) + .filter((pkg): pkg is ControlPlaneRunningPackage => pkg !== null); + if (packages.length !== value.packages.length) { + return null; + } + return { + packages, + }; +}; + +export const parseControlPlaneDevicesResponse = ( + value: unknown, +): ControlPlaneDevicesResponse | null => { + if (!isRecord(value) || !Array.isArray(value.devices)) { + return null; + } + const devices = value.devices + .map((device) => parseControlPlaneDevice(device)) + .filter((device): device is ControlPlaneDevice => device !== null); + if (devices.length !== value.devices.length) { + return null; + } + return { devices }; +}; + +export const parseControlPlaneDeviceSessionResponse = ( + value: unknown, +): ControlPlaneDeviceSessionResponse | null => { + if (!isRecord(value)) { + return null; + } + const device = parseControlPlaneDevice(value.device); + if (!device) { + return null; + } + return { device }; +}; + +export const parseControlPlaneReplEvalResponse = ( + value: unknown, +): ControlPlaneReplEvalResponse | null => { + if ( + !isRecord(value) || + typeof value.created !== "boolean" || + typeof value.closed !== "boolean" || + typeof value.ok !== "boolean" || + !isNullableString(value.result) || + !Array.isArray(value.logs) || + !isNullableString(value.error) + ) { + return null; + } + const session = parseControlPlaneReplSession(value.session); + if (!session) { + return null; + } + const logs = value.logs + .map((entry) => parseControlPlaneReplLogEntry(entry)) + .filter((entry): entry is ControlPlaneReplLogEntry => entry !== null); + if (logs.length !== value.logs.length) { + return null; + } + return { + session, + created: value.created, + closed: value.closed, + ok: value.ok, + result: value.result, + logs, + error: value.error, + }; +}; + +export const parseControlPlaneReplCloseResponse = ( + value: unknown, +): ControlPlaneReplCloseResponse | null => { + if ( + !isRecord(value) || + !isString(value.sessionId) || + typeof value.closed !== "boolean" + ) { + return null; + } + return { + sessionId: value.sessionId, + closed: value.closed, + }; +}; + +export const parseControlPlaneLogsResponse = ( + value: unknown, +): ControlPlaneLogsResponse | null => { + if (!isRecord(value) || !isString(value.packageName) || !Array.isArray(value.entries)) { + return null; + } + const entries = value.entries + .map((entry) => parseControlPlanePackageLogEntry(entry)) + .filter((entry): entry is ControlPlanePackageLogEntry => entry !== null); + if (entries.length !== value.entries.length) { + return null; + } + return { + packageName: value.packageName, + entries, + }; +}; + +export const parseControlPlaneErrorsResponse = ( + value: unknown, +): ControlPlaneErrorsResponse | null => { + if ( + !isRecord(value) || + !Array.isArray(value.entries) || + !( + typeof value.acknowledgedCount === "number" && + Number.isInteger(value.acknowledgedCount) && + value.acknowledgedCount >= 0 + ) + ) { + return null; + } + const entries = value.entries + .map((entry) => parseControlPlanePackageErrorEntry(entry)) + .filter((entry): entry is ControlPlanePackageErrorEntry => entry !== null); + if (entries.length !== value.entries.length) { + return null; + } + return { + entries, + acknowledgedCount: value.acknowledgedCount, + }; +}; + +export const parseControlPlaneClearStateResponse = ( + value: unknown, +): ControlPlaneClearStateResponse | null => { + if ( + !isRecord(value) || + !isStringArray(value.packageNames) || + !isStringArray(value.clearedDirectories) || + !Array.isArray(value.packages) + ) { + return null; + } + const packages = value.packages + .map((pkg) => parseControlPlaneRunningPackage(pkg)) + .filter((pkg): pkg is ControlPlaneRunningPackage => pkg !== null); + if (packages.length !== value.packages.length) { + return null; + } + return { + packageNames: value.packageNames, + clearedDirectories: value.clearedDirectories, + packages, + }; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d402529 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", ".yarn", "dist"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e69de29